portal_publico/static/js/users-admin.js

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 (!window.confirm(`Excluir o departamento "${dep.nome}"?`)) return;
try {
await pidDeleteDepartamento(id);
const selected = selectedDepartamentoIds().filter((depId) => depId !== id);
await fillDepartamentoChecklist(selected);
} catch (e) {
window.alert(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) {
window.alert("Você não pode excluir o usuário com o qual está logado.");
return;
}
const account = users.find((u) => u.id === id);
if (account && window.confirm(`Excluir o usuário "${account.nome}"?`)) {
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) {
window.alert("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 (window.confirm(`${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();
});