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;
}
const msgErroCarga = "Não foi possível carregar. Recarregue a página; se persistir, contate a Inovação.";
const tableBody = document.getElementById("ua-table-body");
// Falha de carga não pode virar "Nenhum usuário encontrado.".
let users;
try {
users = await pidFetchUsuarios();
} catch (e) {
tableBody.innerHTML = `
| ${msgErroCarga} |
`;
return;
}
let editingId = null;
// Retrato dos campos na abertura da edição, pra só perguntar "Sair sem
// salvar" quando algo mudou de fato.
let retratoEdicao = null;
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 ? pidEscapeHtml(valor) : '—';
}
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) => `
| ${pidEscapeHtml(u.username)} |
${pidEscapeHtml(u.nome)} |
${celulaCodigo(u.codigo_folha)} |
${celulaCodigo(u.codigo_questor)} |
${celulaCodigo(u.codigo_tareffa)} |
${celulaCodigo(u.codigo_contabit)} |
${celulaCodigo(u.ramal)} |
${pidEscapeHtml(profileNomes(u.perfis))} |
${u.is_active ? "Ativo" : "Inativo"} |
|
`
)
.join("")
: `| Nenhum usuário encontrado. |
`;
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, "pt-BR"));
profileChecklist.innerHTML = perfis
.map(
(p) => `
`
)
.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, "pt-BR"));
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, "pt-BR"));
departamentoChecklist.innerHTML = departamentosCache
.map(
(d) => `
`
)
.join("");
}
function selectedDepartamentoIds() {
return Array.from(departamentoChecklist.querySelectorAll('input[type="checkbox"]:checked')).map((cb) =>
Number(cb.value)
);
}
// Valores do formulário no formato do payload; usado no salvar e no
// retrato de "houve alteração?".
function coletarFormulario() {
return {
username: loginInput.value.trim(),
nome: nomeInput.value.trim(),
senha: senhaInput.value,
perfis: Array.from(profileChecklist.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => Number(cb.value)),
departamentos: selectedDepartamentoIds(),
email: emailInput.value.trim(),
codigo_folha: codigoFolhaInput.value.trim(),
codigo_questor: codigoQuestorInput.value.trim(),
codigo_tareffa: codigoTareffaInput.value.trim(),
codigo_contabit: codigoContabitInput.value.trim(),
ramal: ramalInput.value.trim(),
data_aniversario: dataAniversarioInput.value || null,
lideranca: liderancaInput.checked,
liderados: selectedLideradosIds(),
is_active: ativoInput.checked,
};
}
function retratoAtual() {
const dados = coletarFormulario();
// Ordem de marcação não importa, só o conjunto.
["perfis", "departamentos", "liderados"].forEach((k) => (dados[k] = dados[k].slice().sort((a, b) => a - b)));
return JSON.stringify(dados);
}
async function podeSairDaEdicao() {
if (retratoEdicao === null || retratoAtual() === retratoEdicao) return true;
return pidConfirm("Sair sem salvar as alterações?", {
titulo: "Sair sem salvar",
textoConfirmar: "Sair sem salvar",
perigoso: true,
});
}
async function openEdit(id) {
try {
await preencherEdicao(id);
} catch (e) {
// Perfis/departamentos não vieram: fica na lista em vez de abrir um
// formulário pela metade.
await pidAlert(`${e.message} Tente novamente; se persistir, contate a Inovação.`, {
titulo: "Não foi possível abrir o usuário",
});
}
}
async function preencherEdicao(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);
}
retratoEdicao = retratoAtual();
listView.hidden = true;
editView.hidden = false;
}
function closeEdit() {
editView.hidden = true;
listView.hidden = false;
retratoEdicao = null;
renderList();
}
async function tentarFecharEdicao() {
if (await podeSairDaEdicao()) closeEdit();
}
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", tentarFecharEdicao);
cancelBtn.addEventListener("click", tentarFecharEdicao);
departamentoAddBtn.addEventListener("click", async () => {
const nome = departamentoNewInput.value.trim();
if (!nome) {
departamentoError.textContent = "Informe o nome do departamento.";
departamentoNewInput.focus();
return;
}
departamentoError.textContent = "";
// Trava contra clique duplo (criaria o departamento duas vezes).
const textoOriginal = departamentoAddBtn.textContent;
departamentoAddBtn.disabled = true;
departamentoAddBtn.textContent = "Adicionando…";
try {
const novo = await pidCreateDepartamento({ nome });
const selected = selectedDepartamentoIds();
selected.push(novo.id);
await fillDepartamentoChecklist(selected);
departamentoNewInput.value = "";
} catch (e) {
departamentoError.textContent = e.message;
departamentoNewInput.focus();
} finally {
departamentoAddBtn.disabled = false;
departamentoAddBtn.textContent = textoOriginal;
}
});
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;
deleteBtn.disabled = true;
try {
await pidDeleteDepartamento(id);
const selected = selectedDepartamentoIds().filter((depId) => depId !== id);
await fillDepartamentoChecklist(selected);
} catch (e) {
deleteBtn.disabled = false;
await pidAlert(e.message, { titulo: "Não foi possível excluir o departamento" });
}
}
});
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 }))) {
deleteTarget.disabled = true;
try {
await pidDeleteUsuario(id);
users = await pidFetchUsuarios();
renderList();
} catch (e) {
deleteTarget.disabled = false;
await pidAlert(e.message, { titulo: "Não foi possível excluir o usuário" });
}
}
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;
// Desativar limpa perfis e liderança no servidor (_revogar_acesso_se_inativo):
// quem confirma precisa saber que reativar não devolve isso.
const confirmado = account.is_active
? await pidConfirm(
`Desativar o usuário "${account.nome}"? Isso remove todos os perfis de acesso dele e o tira da liderança de quem o lidera. Ao reativar, será preciso configurar tudo de novo.`,
{ titulo: "Desativar usuário", textoConfirmar: "Desativar", perigoso: true }
)
: await pidConfirm(`Ativar o usuário "${account.nome}"?`, { titulo: "Ativar usuário", textoConfirmar: "Ativar" });
if (confirmado) {
toggleTarget.disabled = true;
try {
await pidUpdateUsuario(id, { is_active: !account.is_active });
users = await pidFetchUsuarios();
renderList();
} catch (e) {
toggleTarget.disabled = false;
await pidAlert(e.message, {
titulo: account.is_active ? "Não foi possível desativar o usuário" : "Não foi possível ativar o usuário",
});
}
}
}
});
saveBtn.addEventListener("click", async () => {
const payload = coletarFormulario();
// A mensagem fica no fim do formulário; o cursor vai pro campo que falta.
if (!payload.username) {
formError.textContent = "Informe o login do usuário.";
loginInput.focus();
return;
}
if (!payload.nome) {
formError.textContent = "Informe o nome do usuário.";
nomeInput.focus();
return;
}
if (editingId === null && !payload.senha) {
formError.textContent = "Informe uma senha para o novo usuário.";
senhaInput.focus();
return;
}
formError.textContent = "";
// Trava contra clique duplo (criaria a conta duas vezes).
const textoOriginal = saveBtn.textContent;
saveBtn.disabled = true;
saveBtn.textContent = "Salvando…";
try {
if (editingId === null) {
await pidCreateUsuario(payload);
} else {
await pidUpdateUsuario(editingId, payload);
}
users = await pidFetchUsuarios();
closeEdit();
} catch (e) {
// Erro do servidor não diz o campo: ao menos rola até a mensagem.
formError.textContent = e.message;
formError.scrollIntoView({ block: "center", behavior: "smooth" });
} finally {
saveBtn.disabled = false;
saveBtn.textContent = textoOriginal;
}
});
renderList();
});