477 lines
19 KiB
JavaScript
477 lines
19 KiB
JavaScript
async function pidCreateUsuario(payload) {
|
|
return pidApiRequest("/usuarios/", { method: "POST", body: payload });
|
|
}
|
|
|
|
async function pidUpdateUsuario(id, payload) {
|
|
return pidApiRequest(`/usuarios/${id}/`, { method: "PATCH", body: payload });
|
|
}
|
|
|
|
async function pidDeleteUsuario(id) {
|
|
return pidApiRequest(`/usuarios/${id}/`, { method: "DELETE" });
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
const listView = document.getElementById("ua-list-view");
|
|
const editView = document.getElementById("ua-edit-view");
|
|
if (!listView || !editView) return;
|
|
|
|
const me = await pidGetMe();
|
|
if (!me) return;
|
|
if (!me.gerencia_permissoes) {
|
|
window.location.href = "portal.html";
|
|
return;
|
|
}
|
|
|
|
let users = await pidFetchUsuarios();
|
|
let editingId = null;
|
|
|
|
const tableBody = document.getElementById("ua-table-body");
|
|
const footerCount = document.getElementById("ua-footer-count");
|
|
const searchInput = document.getElementById("ua-search-input");
|
|
const statusFiltros = document.getElementById("ua-status-filtros");
|
|
let statusFiltro = "ativos";
|
|
const sortHeaders = Array.from(document.querySelectorAll("#ua-table th[data-sort]"));
|
|
let sortKey = "nome";
|
|
let sortDir = 1;
|
|
const newBtn = document.getElementById("ua-new-btn");
|
|
const backBtn = document.getElementById("ua-back-btn");
|
|
const cancelBtn = document.getElementById("ua-cancel-btn");
|
|
const saveBtn = document.getElementById("ua-save-btn");
|
|
const loginInput = document.getElementById("ua-login");
|
|
const nomeInput = document.getElementById("ua-nome");
|
|
const senhaInput = document.getElementById("ua-senha");
|
|
const senhaToggleBtn = document.getElementById("ua-senha-toggle");
|
|
const profileChecklist = document.getElementById("ua-profile-checklist");
|
|
const formError = document.getElementById("ua-form-error");
|
|
const departamentoChecklist = document.getElementById("ua-departamento-checklist");
|
|
const departamentoNewInput = document.getElementById("ua-departamento-new");
|
|
const departamentoAddBtn = document.getElementById("ua-departamento-add-btn");
|
|
const departamentoError = document.getElementById("ua-departamento-error");
|
|
const emailInput = document.getElementById("ua-email");
|
|
const codigoFolhaInput = document.getElementById("ua-codigo-folha");
|
|
const codigoQuestorInput = document.getElementById("ua-codigo-questor");
|
|
const codigoTareffaInput = document.getElementById("ua-codigo-tareffa");
|
|
const codigoContabitInput = document.getElementById("ua-codigo-contabit");
|
|
const ramalInput = document.getElementById("ua-ramal");
|
|
const dataAniversarioInput = document.getElementById("ua-data-aniversario");
|
|
const ativoInput = document.getElementById("ua-ativo");
|
|
const liderancaInput = document.getElementById("ua-lideranca");
|
|
const lideradosField = document.getElementById("ua-liderados-field");
|
|
|
|
function departamentosTexto(usuario) {
|
|
return (usuario.departamentos || []).map((d) => d.nome).join(", ");
|
|
}
|
|
|
|
const seletorLiderados = pidCriarSeletorDuplo({
|
|
available: {
|
|
selectAll: document.getElementById("ua-liderados-available-select-all"),
|
|
sortHeader: document.getElementById("ua-liderados-available-sort-nome"),
|
|
searchPrincipal: document.getElementById("ua-liderados-available-search-nome"),
|
|
searchSecundaria: document.getElementById("ua-liderados-available-search-departamento"),
|
|
body: document.getElementById("ua-liderados-available-body"),
|
|
footer: document.getElementById("ua-liderados-available-footer"),
|
|
acaoBtn: document.getElementById("ua-liderados-available-link-btn"),
|
|
},
|
|
linked: {
|
|
selectAll: document.getElementById("ua-liderados-linked-select-all"),
|
|
sortHeader: document.getElementById("ua-liderados-linked-sort-nome"),
|
|
searchPrincipal: document.getElementById("ua-liderados-linked-search-nome"),
|
|
searchSecundaria: document.getElementById("ua-liderados-linked-search-departamento"),
|
|
body: document.getElementById("ua-liderados-linked-body"),
|
|
footer: document.getElementById("ua-liderados-linked-footer"),
|
|
acaoBtn: document.getElementById("ua-liderados-linked-unlink-btn"),
|
|
},
|
|
secundariaValor: departamentosTexto,
|
|
});
|
|
|
|
liderancaInput.addEventListener("change", () => {
|
|
lideradosField.hidden = !liderancaInput.checked;
|
|
});
|
|
|
|
pidBindPasswordToggle(senhaToggleBtn, senhaInput);
|
|
|
|
function profileNomes(perfis) {
|
|
return perfis.length ? perfis.map((p) => p.nome).join(", ") : "Sem perfil vinculado";
|
|
}
|
|
|
|
// Compara duas strings tratando conteúdo numérico como número (ex.:
|
|
// "92" < "150") e caindo pra comparação alfabética quando não são os dois
|
|
// numéricos — mesmo critério de comparaValoresLista() em
|
|
// importacao-plano-saude.js.
|
|
function comparaValoresUsuario(a, b) {
|
|
const an = Number(a);
|
|
const bn = Number(b);
|
|
const ambosNumeros = a !== "" && b !== "" && !Number.isNaN(an) && !Number.isNaN(bn);
|
|
if (ambosNumeros) return an - bn;
|
|
return String(a).localeCompare(String(b), "pt-BR", { sensitivity: "base" });
|
|
}
|
|
|
|
const PID_UA_SORT_VALOR = {
|
|
username: (u) => u.username || "",
|
|
nome: (u) => u.nome || "",
|
|
codigo_folha: (u) => u.codigo_folha || "",
|
|
codigo_questor: (u) => u.codigo_questor || "",
|
|
codigo_tareffa: (u) => u.codigo_tareffa || "",
|
|
codigo_contabit: (u) => u.codigo_contabit || "",
|
|
ramal: (u) => u.ramal || "",
|
|
status: (u) => (u.is_active ? 1 : 0),
|
|
};
|
|
|
|
// Célula vazia fica destacada (itálico, cor apagada) — ordenar a coluna
|
|
// agrupa quem ainda não tem aquele código/ramal cadastrado.
|
|
function celulaCodigo(valor) {
|
|
return valor ? valor : '<span class="ua-campo-vazio">—</span>';
|
|
}
|
|
|
|
function renderList() {
|
|
const term = searchInput.value.trim().toLowerCase();
|
|
const extrator = PID_UA_SORT_VALOR[sortKey];
|
|
const rows = users
|
|
.filter((u) => statusFiltro === "todos" || (statusFiltro === "ativos") === u.is_active)
|
|
.filter((u) => !term || u.nome.toLowerCase().includes(term) || u.username.toLowerCase().includes(term))
|
|
.slice()
|
|
.sort((a, b) => comparaValoresUsuario(extrator(a), extrator(b)) * sortDir);
|
|
|
|
sortHeaders.forEach((th) => {
|
|
const key = th.getAttribute("data-sort");
|
|
th.classList.toggle("is-sort-asc", key === sortKey && sortDir === 1);
|
|
th.classList.toggle("is-sort-desc", key === sortKey && sortDir === -1);
|
|
});
|
|
|
|
tableBody.innerHTML = rows.length
|
|
? rows
|
|
.map(
|
|
(u) => `
|
|
<tr>
|
|
<td>${u.username}</td>
|
|
<td><a href="#" class="pa-name-link" data-ua-edit="${u.id}">${u.nome}</a></td>
|
|
<td>${celulaCodigo(u.codigo_folha)}</td>
|
|
<td>${celulaCodigo(u.codigo_questor)}</td>
|
|
<td>${celulaCodigo(u.codigo_tareffa)}</td>
|
|
<td>${celulaCodigo(u.codigo_contabit)}</td>
|
|
<td>${celulaCodigo(u.ramal)}</td>
|
|
<td>${profileNomes(u.perfis)}</td>
|
|
<td><span class="status-pill ${u.is_active ? "status-pill--ativo" : "status-pill--inativo"}">${u.is_active ? "Ativo" : "Inativo"}</span></td>
|
|
<td>
|
|
<div class="pa-row-actions">
|
|
<button type="button" class="icon-btn" data-ua-edit="${u.id}" aria-label="Editar usuário">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9" stroke-linecap="round"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
<button type="button" class="icon-btn" data-ua-toggle-active="${u.id}" aria-label="${u.is_active ? "Desativar usuário" : "Ativar usuário"}">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v8" stroke-linecap="round"/><path d="M6.2 6.2a8 8 0 1 0 11.6 0" stroke-linecap="round"/></svg>
|
|
</button>
|
|
<button type="button" class="icon-btn" data-ua-delete="${u.id}" aria-label="Excluir usuário">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m-8 0 1 12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1l1-12" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>`
|
|
)
|
|
.join("")
|
|
: `<tr class="pa-empty-row"><td colspan="10">Nenhum usuário encontrado.</td></tr>`;
|
|
|
|
footerCount.textContent = `${rows.length} ${rows.length === 1 ? "item" : "itens"}`;
|
|
}
|
|
|
|
async function fillProfileChecklist(selectedCodigos) {
|
|
const perfis = (await pidFetchPerfis()).slice().sort((a, b) => a.nome.localeCompare(b.nome));
|
|
profileChecklist.innerHTML = perfis
|
|
.map(
|
|
(p) => `
|
|
<label class="checklist-item">
|
|
<input type="checkbox" value="${p.codigo}" ${selectedCodigos.includes(p.codigo) ? "checked" : ""} />
|
|
<span>${p.nome}</span>
|
|
</label>`
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function fillLideradosChecklist(selectedIds, excludeId) {
|
|
// Usuário inativo não entra como candidato (nem aparece do lado
|
|
// vinculado) — mesma decisão do widget "Usuários do Escritório" de
|
|
// Perfis de Acesso (ver profiles.js).
|
|
const candidatos = users
|
|
.filter((u) => u.id !== excludeId && u.is_active)
|
|
.slice()
|
|
.sort((a, b) => a.nome.localeCompare(b.nome));
|
|
seletorLiderados.setDados(candidatos, selectedIds);
|
|
}
|
|
|
|
function selectedLideradosIds() {
|
|
return seletorLiderados.getVinculadosIds();
|
|
}
|
|
|
|
let departamentosCache = [];
|
|
|
|
async function fillDepartamentoChecklist(selectedIds) {
|
|
departamentosCache = (await pidFetchDepartamentos()).slice().sort((a, b) => a.nome.localeCompare(b.nome));
|
|
departamentoChecklist.innerHTML = departamentosCache
|
|
.map(
|
|
(d) => `
|
|
<div class="ua-departamento-item">
|
|
<label class="checklist-item">
|
|
<input type="checkbox" value="${d.id}" ${selectedIds.includes(d.id) ? "checked" : ""} />
|
|
<span>${d.nome}</span>
|
|
</label>
|
|
<div class="ua-departamento-item__actions">
|
|
<button type="button" class="ua-departamento-action" data-dep-edit="${d.id}" aria-label="Renomear departamento">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9" stroke-linecap="round"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
<button type="button" class="ua-departamento-action" data-dep-delete="${d.id}" aria-label="Excluir departamento">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>`
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function selectedDepartamentoIds() {
|
|
return Array.from(departamentoChecklist.querySelectorAll('input[type="checkbox"]:checked')).map((cb) =>
|
|
Number(cb.value)
|
|
);
|
|
}
|
|
|
|
async function openEdit(id) {
|
|
formError.textContent = "";
|
|
departamentoError.textContent = "";
|
|
departamentoNewInput.value = "";
|
|
senhaInput.value = "";
|
|
senhaInput.type = "password";
|
|
if (senhaToggleBtn) senhaToggleBtn.setAttribute("aria-label", "Mostrar senha");
|
|
|
|
if (id === null) {
|
|
editingId = null;
|
|
loginInput.value = "";
|
|
nomeInput.value = "";
|
|
senhaInput.placeholder = "Digite a senha";
|
|
emailInput.value = "";
|
|
codigoFolhaInput.value = "";
|
|
codigoQuestorInput.value = "";
|
|
codigoTareffaInput.value = "";
|
|
codigoContabitInput.value = "";
|
|
ramalInput.value = "";
|
|
dataAniversarioInput.value = "";
|
|
ativoInput.checked = true;
|
|
ativoInput.disabled = false;
|
|
liderancaInput.checked = false;
|
|
lideradosField.hidden = true;
|
|
await fillProfileChecklist([]);
|
|
await fillDepartamentoChecklist([]);
|
|
fillLideradosChecklist([], null);
|
|
} else {
|
|
const account = users.find((u) => u.id === id);
|
|
if (!account) return;
|
|
editingId = id;
|
|
loginInput.value = account.username;
|
|
nomeInput.value = account.nome;
|
|
senhaInput.placeholder = "Deixe em branco para manter a senha atual";
|
|
emailInput.value = account.email || "";
|
|
codigoFolhaInput.value = account.codigo_folha || "";
|
|
codigoQuestorInput.value = account.codigo_questor || "";
|
|
codigoTareffaInput.value = account.codigo_tareffa || "";
|
|
codigoContabitInput.value = account.codigo_contabit || "";
|
|
ramalInput.value = account.ramal || "";
|
|
dataAniversarioInput.value = account.data_aniversario || "";
|
|
ativoInput.checked = account.is_active;
|
|
ativoInput.disabled = account.id === me.id;
|
|
liderancaInput.checked = account.lideranca;
|
|
lideradosField.hidden = !account.lideranca;
|
|
await fillProfileChecklist(account.perfis.map((p) => p.codigo));
|
|
await fillDepartamentoChecklist(account.departamentos.map((d) => d.id));
|
|
fillLideradosChecklist(account.liderados.map((u) => u.id), account.id);
|
|
}
|
|
|
|
listView.hidden = true;
|
|
editView.hidden = false;
|
|
}
|
|
|
|
function closeEdit() {
|
|
editView.hidden = true;
|
|
listView.hidden = false;
|
|
renderList();
|
|
}
|
|
|
|
searchInput.addEventListener("input", renderList);
|
|
sortHeaders.forEach((th) => {
|
|
th.addEventListener("click", () => {
|
|
const key = th.getAttribute("data-sort");
|
|
if (sortKey === key) {
|
|
sortDir *= -1;
|
|
} else {
|
|
sortKey = key;
|
|
sortDir = 1;
|
|
}
|
|
renderList();
|
|
});
|
|
});
|
|
statusFiltros.addEventListener("click", (event) => {
|
|
const chip = event.target.closest("[data-ua-status]");
|
|
if (!chip) return;
|
|
statusFiltro = chip.dataset.uaStatus;
|
|
statusFiltros.querySelectorAll(".ua-status-chip").forEach((el) => el.classList.toggle("is-active", el === chip));
|
|
renderList();
|
|
});
|
|
newBtn.addEventListener("click", () => openEdit(null));
|
|
backBtn.addEventListener("click", closeEdit);
|
|
cancelBtn.addEventListener("click", closeEdit);
|
|
|
|
departamentoAddBtn.addEventListener("click", async () => {
|
|
const nome = departamentoNewInput.value.trim();
|
|
if (!nome) {
|
|
departamentoError.textContent = "Informe o nome do departamento.";
|
|
return;
|
|
}
|
|
departamentoError.textContent = "";
|
|
try {
|
|
const novo = await pidCreateDepartamento({ nome });
|
|
const selected = selectedDepartamentoIds();
|
|
selected.push(novo.id);
|
|
await fillDepartamentoChecklist(selected);
|
|
departamentoNewInput.value = "";
|
|
} catch (e) {
|
|
departamentoError.textContent = e.message;
|
|
}
|
|
});
|
|
|
|
departamentoChecklist.addEventListener("click", async (event) => {
|
|
const editBtn = event.target.closest("[data-dep-edit]");
|
|
if (editBtn) {
|
|
const id = Number(editBtn.getAttribute("data-dep-edit"));
|
|
const dep = departamentosCache.find((d) => d.id === id);
|
|
if (!dep) return;
|
|
const novoNome = window.prompt("Novo nome do departamento:", dep.nome);
|
|
if (novoNome === null) return;
|
|
const nome = novoNome.trim();
|
|
if (!nome) return;
|
|
departamentoError.textContent = "";
|
|
try {
|
|
await pidUpdateDepartamento(id, { nome });
|
|
await fillDepartamentoChecklist(selectedDepartamentoIds());
|
|
} catch (e) {
|
|
departamentoError.textContent = e.message;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const deleteBtn = event.target.closest("[data-dep-delete]");
|
|
if (deleteBtn) {
|
|
const id = Number(deleteBtn.getAttribute("data-dep-delete"));
|
|
const dep = departamentosCache.find((d) => d.id === id);
|
|
if (!dep) return;
|
|
if (!(await pidConfirm(`Excluir o departamento "${dep.nome}"?`, { perigoso: true }))) return;
|
|
try {
|
|
await pidDeleteDepartamento(id);
|
|
const selected = selectedDepartamentoIds().filter((depId) => depId !== id);
|
|
await fillDepartamentoChecklist(selected);
|
|
} catch (e) {
|
|
await pidAlert(e.message);
|
|
}
|
|
}
|
|
});
|
|
|
|
tableBody.addEventListener("click", async (event) => {
|
|
const editTarget = event.target.closest("[data-ua-edit]");
|
|
if (editTarget) {
|
|
event.preventDefault();
|
|
await openEdit(Number(editTarget.getAttribute("data-ua-edit")));
|
|
return;
|
|
}
|
|
|
|
const deleteTarget = event.target.closest("[data-ua-delete]");
|
|
if (deleteTarget) {
|
|
const id = Number(deleteTarget.getAttribute("data-ua-delete"));
|
|
if (id === me.id) {
|
|
await pidAlert("Você não pode excluir o usuário com o qual está logado.");
|
|
return;
|
|
}
|
|
const account = users.find((u) => u.id === id);
|
|
if (account && (await pidConfirm(`Excluir o usuário "${account.nome}"?`, { perigoso: true }))) {
|
|
await pidDeleteUsuario(id);
|
|
users = await pidFetchUsuarios();
|
|
renderList();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const toggleTarget = event.target.closest("[data-ua-toggle-active]");
|
|
if (toggleTarget) {
|
|
const id = Number(toggleTarget.getAttribute("data-ua-toggle-active"));
|
|
if (id === me.id) {
|
|
await pidAlert("Você não pode desativar o usuário com o qual está logado.");
|
|
return;
|
|
}
|
|
const account = users.find((u) => u.id === id);
|
|
if (!account) return;
|
|
const acao = account.is_active ? "Desativar" : "Ativar";
|
|
if (await pidConfirm(`${acao} o usuário "${account.nome}"?`)) {
|
|
await pidUpdateUsuario(id, { is_active: !account.is_active });
|
|
users = await pidFetchUsuarios();
|
|
renderList();
|
|
}
|
|
}
|
|
});
|
|
|
|
saveBtn.addEventListener("click", async () => {
|
|
const username = loginInput.value.trim();
|
|
const nome = nomeInput.value.trim();
|
|
const senha = senhaInput.value;
|
|
const perfis = Array.from(profileChecklist.querySelectorAll('input[type="checkbox"]:checked')).map((cb) =>
|
|
Number(cb.value)
|
|
);
|
|
const departamentos = selectedDepartamentoIds();
|
|
const email = emailInput.value.trim();
|
|
const codigo_folha = codigoFolhaInput.value.trim();
|
|
const codigo_questor = codigoQuestorInput.value.trim();
|
|
const codigo_tareffa = codigoTareffaInput.value.trim();
|
|
const codigo_contabit = codigoContabitInput.value.trim();
|
|
const ramal = ramalInput.value.trim();
|
|
const data_aniversario = dataAniversarioInput.value || null;
|
|
const lideranca = liderancaInput.checked;
|
|
const liderados = selectedLideradosIds();
|
|
|
|
if (!username || !nome) {
|
|
formError.textContent = "Informe login e nome do usuário.";
|
|
return;
|
|
}
|
|
if (editingId === null && !senha) {
|
|
formError.textContent = "Informe uma senha para o novo usuário.";
|
|
return;
|
|
}
|
|
|
|
formError.textContent = "";
|
|
|
|
const payload = {
|
|
username,
|
|
nome,
|
|
senha,
|
|
perfis,
|
|
departamentos,
|
|
email,
|
|
codigo_folha,
|
|
codigo_questor,
|
|
codigo_tareffa,
|
|
codigo_contabit,
|
|
ramal,
|
|
data_aniversario,
|
|
lideranca,
|
|
liderados,
|
|
is_active: ativoInput.checked,
|
|
};
|
|
|
|
try {
|
|
if (editingId === null) {
|
|
await pidCreateUsuario(payload);
|
|
} else {
|
|
await pidUpdateUsuario(editingId, payload);
|
|
}
|
|
users = await pidFetchUsuarios();
|
|
closeEdit();
|
|
} catch (e) {
|
|
formError.textContent = e.message;
|
|
}
|
|
});
|
|
|
|
renderList();
|
|
});
|