let PID_MODULES = []; let PID_MODULE_APPS = {}; async function pidFetchCatalogo() { return pidApiRequest("/catalogo/"); } async function pidFetchPerfis() { return pidApiRequest("/perfis/"); } async function pidCreatePerfil(payload) { return pidApiRequest("/perfis/", { method: "POST", body: payload }); } async function pidUpdatePerfil(codigo, payload) { return pidApiRequest(`/perfis/${codigo}/`, { method: "PUT", body: payload }); } async function pidUpdatePerfilPermissoes(codigo, permissoes) { return pidApiRequest(`/perfis/${codigo}/`, { method: "PATCH", body: { permissoes } }); } async function pidDeletePerfil(codigo) { return pidApiRequest(`/perfis/${codigo}/`, { method: "DELETE" }); } /** Usada também por users-admin.js (lista de usuários + perfis vinculados). */ async function pidFetchUsuarios() { return pidApiRequest("/usuarios/"); } /** Usadas também por users-admin.js (checklist de departamentos + cadastro inline de um novo). */ async function pidFetchDepartamentos() { return pidApiRequest("/departamentos/"); } async function pidCreateDepartamento(payload) { return pidApiRequest("/departamentos/", { method: "POST", body: payload }); } async function pidUpdateDepartamento(id, payload) { return pidApiRequest(`/departamentos/${id}/`, { method: "PATCH", body: payload }); } async function pidDeleteDepartamento(id) { return pidApiRequest(`/departamentos/${id}/`, { method: "DELETE" }); } async function pidSetUsuarioPerfis(id, perfisCodigos) { return pidApiRequest(`/usuarios/${id}/`, { method: "PATCH", body: { perfis: perfisCodigos } }); } function pidPerfilFromApi(p) { return { codigo: p.codigo, nome: p.nome, ativo: p.ativo, gerenciaPermissoes: p.gerencia_permissoes, permissions: p.permissoes, }; } function pidPerfilToApi(p) { return { nome: p.nome, ativo: p.ativo, gerencia_permissoes: p.gerenciaPermissoes, permissoes: p.permissions, }; } function pidAppsFor(moduleKey, value) { const apps = {}; (PID_MODULE_APPS[moduleKey] || []).forEach((entry) => { if (entry.tools) { entry.tools.forEach((tool) => { apps[tool.key] = value; }); } else { apps[entry.key] = value; } }); return apps; } function pidEmptyPermissions(allEnabled) { const perms = {}; PID_MODULES.forEach((m) => { perms[m.key] = { enabled: !!allEnabled, apps: pidAppsFor(m.key, !!allEnabled) }; }); return perms; } /** Achata PID_MODULES x PID_MODULE_APPS numa lista plana de "aplicações" * pesquisáveis (uma por entrada de MODULE_APPS — app simples ou subgrupo), * usada pelo popup "Nova Aplicação". Cada item mantém `tools` (ou `null` * pra app simples) pra o painel de gerenciamento saber quantas colunas de * acesso desenhar por perfil. */ function pidFlattenAplicacoes() { const result = []; PID_MODULES.forEach((modulo) => { (PID_MODULE_APPS[modulo.key] || []).forEach((entry) => { result.push({ moduleKey: modulo.key, moduleLabel: modulo.label, key: entry.key, label: entry.label, tools: entry.tools || null, }); }); }); return result; } document.addEventListener("DOMContentLoaded", async () => { const listView = document.getElementById("pa-list-view"); const editView = document.getElementById("pa-edit-view"); if (!listView || !editView) return; const me = await pidGetMe(); if (!me) return; if (!me.gerencia_permissoes) { window.location.href = "portal.html"; return; } const catalogo = await pidFetchCatalogo(); PID_MODULES = catalogo.modulos; PID_MODULE_APPS = catalogo.aplicacoes_por_modulo; let profiles = (await pidFetchPerfis()).map(pidPerfilFromApi); let draftProfile = null; let activeModuleKey = PID_MODULES[0].key; let activeTab = "permissoes"; const expandedModules = new Set(); const expandedSubgroups = new Set(); let appSearchSelected = null; const tableBody = document.getElementById("pa-table-body"); const footerCount = document.getElementById("pa-footer-count"); const listSearchInput = document.getElementById("pa-search-input"); const newBtn = document.getElementById("pa-new-btn"); const backBtn = document.getElementById("pa-back-btn"); const cancelBtn = document.getElementById("pa-cancel-btn"); const saveBtn = document.getElementById("pa-save-btn"); const formCodigo = document.getElementById("pa-codigo"); const formNome = document.getElementById("pa-nome"); const formError = document.getElementById("pa-nome-error"); const moduleListEl = document.getElementById("pa-module-list"); const treePanel = document.getElementById("pa-tree-panel"); const treeSearchInput = document.getElementById("pa-tree-search-input"); const usersPanelsEl = { available: { refreshBtn: document.getElementById("pa-users-available-refresh-btn"), linkBtn: document.getElementById("pa-users-link-btn"), selectAll: document.getElementById("pa-users-available-select-all"), sortHeader: document.getElementById("pa-users-available-sort-nome"), searchNome: document.getElementById("pa-users-available-search-nome"), searchEmail: document.getElementById("pa-users-available-search-email"), body: document.getElementById("pa-users-available-body"), footer: document.getElementById("pa-users-available-footer"), }, linked: { refreshBtn: document.getElementById("pa-users-linked-refresh-btn"), linkBtn: document.getElementById("pa-users-unlink-btn"), selectAll: document.getElementById("pa-users-linked-select-all"), sortHeader: document.getElementById("pa-users-linked-sort-nome"), searchNome: document.getElementById("pa-users-linked-search-nome"), searchEmail: document.getElementById("pa-users-linked-search-email"), body: document.getElementById("pa-users-linked-body"), footer: document.getElementById("pa-users-linked-footer"), }, }; const usersDualState = { available: { sortDir: 1, selected: new Set() }, linked: { sortDir: 1, selected: new Set() }, }; let usuariosCacheAtual = []; const appSearchBtn = document.getElementById("pa-app-search-btn"); const appSearchModal = document.getElementById("pa-app-search-modal"); const appSearchCloseBtn = document.getElementById("pa-app-search-close-btn"); const appSearchField = document.getElementById("pa-app-search-field"); const appSearchInput = document.getElementById("pa-app-search-input"); const appSearchResults = document.getElementById("pa-app-search-results"); const appManagePanel = document.getElementById("pa-app-manage-panel"); const appManageBackBtn = document.getElementById("pa-app-manage-back-btn"); const appManageTitle = document.getElementById("pa-app-manage-title"); const appManageSearchInput = document.getElementById("pa-app-manage-search"); const appManageThead = document.getElementById("pa-app-manage-thead"); const appManageTbody = document.getElementById("pa-app-manage-tbody"); function renderList() { const term = listSearchInput.value.trim().toLowerCase(); const rows = profiles .filter((p) => !term || p.nome.toLowerCase().includes(term)) .slice() .sort((a, b) => a.codigo - b.codigo); tableBody.innerHTML = rows.length ? rows .map( (p) => ` ${p.codigo} ${p.nome} ${p.gerenciaPermissoes ? 'Acesso total' : ""}
` ) .join("") : `Nenhum perfil encontrado.`; footerCount.textContent = `${rows.length} ${rows.length === 1 ? "item" : "itens"}`; } function renderModuleList() { moduleListEl.innerHTML = PID_MODULES.map((m, idx) => { const enabled = draftProfile.permissions[m.key].enabled; return ` `; }).join(""); } function renderLeaf(perm, tool, term) { const match = !term || tool.label.toLowerCase().includes(term); return ` `; } function renderEntry(perm, entry, term) { if (!entry.tools) { return renderLeaf(perm, entry, term); } const subgroupKey = `${activeModuleKey}__${entry.key}`; const subgroupExpanded = expandedSubgroups.has(subgroupKey); const groupChecked = entry.tools.every((tool) => perm.apps[tool.key]); const anyMatch = !term || entry.label.toLowerCase().includes(term) || entry.tools.some((tool) => tool.label.toLowerCase().includes(term)); return `
${entry.tools.map((tool) => renderLeaf(perm, tool, term)).join("")}
`; } function renderTree() { const perm = draftProfile.permissions[activeModuleKey]; const moduleLabel = PID_MODULES.find((m) => m.key === activeModuleKey).label; const term = treeSearchInput.value.trim().toLowerCase(); const expanded = expandedModules.has(activeModuleKey); const entries = PID_MODULE_APPS[activeModuleKey] || []; const hasEntries = entries.length > 0; treePanel.innerHTML = `
${ hasEntries ? `` : "" }
${ hasEntries ? `
${entries.map((entry) => renderEntry(perm, entry, term)).join("")}
` : "" } `; } function usersDualFiltrados(kind, lista) { const el = usersPanelsEl[kind]; const state = usersDualState[kind]; const termNome = el.searchNome.value.trim().toLowerCase(); const termEmail = el.searchEmail.value.trim().toLowerCase(); return lista .filter((u) => !termNome || u.nome.toLowerCase().includes(termNome)) .filter((u) => !termEmail || (u.email || "").toLowerCase().includes(termEmail)) .slice() .sort((a, b) => a.nome.localeCompare(b.nome, "pt-BR", { sensitivity: "base" }) * state.sortDir); } function renderUsersPanel(kind, listaCompleta) { const el = usersPanelsEl[kind]; const state = usersDualState[kind]; // Poda a seleção de ids que saíram da lista (ex.: acabou de ser // vinculado/desvinculado e migrou pro outro painel). const idsAtuais = new Set(listaCompleta.map((u) => u.id)); Array.from(state.selected).forEach((id) => { if (!idsAtuais.has(id)) state.selected.delete(id); }); const filtrados = usersDualFiltrados(kind, listaCompleta); el.body.innerHTML = filtrados.length ? filtrados .map( (u) => ` ${u.nome} ${u.email || "—"} ` ) .join("") : `Nenhum usuário encontrado.`; const selecionadosVisiveis = filtrados.filter((u) => state.selected.has(u.id)).length; el.selectAll.disabled = filtrados.length === 0; el.selectAll.checked = filtrados.length > 0 && selecionadosVisiveis === filtrados.length; el.selectAll.indeterminate = selecionadosVisiveis > 0 && selecionadosVisiveis < filtrados.length; el.sortHeader.classList.toggle("is-sort-asc", state.sortDir === 1); el.sortHeader.classList.toggle("is-sort-desc", state.sortDir === -1); el.footer.textContent = `${filtrados.length} ${filtrados.length === 1 ? "usuário" : "usuários"}`; el.linkBtn.disabled = state.selected.size === 0; } function listaParaPainelUsuarios(kind) { // Usuário inativo não aparece em nenhum dos dois painéis — decisão // explícita do usuário; inativar já limpa os perfis dele no backend // (ver _revogar_acesso_se_inativo em serializers.py), então isso é // sobretudo defensivo pra dados legados/transição. const ativos = usuariosCacheAtual.filter((u) => u.is_active); const isLinked = (u) => u.perfis.some((p) => p.codigo === draftProfile.codigo); return kind === "linked" ? ativos.filter(isLinked) : ativos.filter((u) => !isLinked(u)); } function renderUsersFromCache() { renderUsersPanel("available", listaParaPainelUsuarios("available")); renderUsersPanel("linked", listaParaPainelUsuarios("linked")); } async function refreshUsuarios() { usuariosCacheAtual = await pidFetchUsuarios(); renderUsersFromCache(); } async function renderUsers() { ["available", "linked"].forEach((kind) => { usersDualState[kind].selected.clear(); usersDualState[kind].sortDir = 1; usersPanelsEl[kind].searchNome.value = ""; usersPanelsEl[kind].searchEmail.value = ""; }); await refreshUsuarios(); } function renderTabs() { document.querySelectorAll("[data-pa-tab]").forEach((tab) => { tab.classList.toggle("is-active", tab.getAttribute("data-pa-tab") === activeTab); }); document.querySelectorAll("[data-pa-tab-panel]").forEach((panel) => { panel.hidden = panel.getAttribute("data-pa-tab-panel") !== activeTab; }); } function renderAppSearchResults() { const term = appSearchInput.value.trim().toLowerCase(); const items = pidFlattenAplicacoes().filter((a) => { if (!term) return true; if (a.label.toLowerCase().includes(term)) return true; if (a.moduleLabel.toLowerCase().includes(term)) return true; return !!(a.tools && a.tools.some((tool) => tool.label.toLowerCase().includes(term))); }); appSearchResults.innerHTML = items.length ? items .map( (a) => ` ` ) .join("") : `

Nenhuma aplicação encontrada.

`; } function renderAppManageTable() { const entry = appSearchSelected; if (!entry) return; const term = appManageSearchInput.value.trim().toLowerCase(); const tools = entry.tools || [{ key: entry.key, label: "Acesso" }]; appManageThead.innerHTML = ` Perfil ${tools.map((tool) => `${tool.label.split(" (")[0]}`).join("")} `; const rows = profiles .filter((p) => !term || p.nome.toLowerCase().includes(term)) .slice() .sort((a, b) => a.nome.localeCompare(b.nome)); appManageTbody.innerHTML = rows.length ? rows .map((p) => { const perm = p.permissions[entry.moduleKey]; return ` ${p.nome} ${p.ativo ? "" : 'Inativo'} ${tools .map( (tool) => ` ` ) .join("")} `; }) .join("") : `Nenhum perfil encontrado.`; } function openAppManage(entry) { appSearchSelected = entry; appManageTitle.textContent = `${entry.label} — ${entry.moduleLabel}`; appManageSearchInput.value = ""; appSearchField.hidden = true; appManagePanel.hidden = false; renderAppManageTable(); } function closeAppManage() { appSearchSelected = null; appManagePanel.hidden = true; appSearchField.hidden = false; } function openAppSearchModal() { closeAppManage(); appSearchInput.value = ""; renderAppSearchResults(); appSearchModal.hidden = false; } function closeAppSearchModal() { appSearchModal.hidden = true; } async function openEdit(codigo) { if (codigo === null) { draftProfile = { codigo: null, nome: "", ativo: true, gerenciaPermissoes: false, permissions: pidEmptyPermissions(false), }; } else { const found = profiles.find((p) => p.codigo === codigo); if (!found) return; draftProfile = JSON.parse(JSON.stringify(found)); } activeModuleKey = PID_MODULES[0].key; activeTab = "permissoes"; expandedModules.clear(); expandedSubgroups.clear(); formError.textContent = ""; formCodigo.value = draftProfile.codigo === null ? "Gerado ao salvar" : draftProfile.codigo; formNome.value = draftProfile.nome; renderTabs(); renderModuleList(); renderTree(); listView.hidden = true; editView.hidden = false; await renderUsers(); } function closeEdit() { editView.hidden = true; listView.hidden = false; renderList(); } listSearchInput.addEventListener("input", renderList); newBtn.addEventListener("click", () => openEdit(null)); backBtn.addEventListener("click", closeEdit); cancelBtn.addEventListener("click", closeEdit); appSearchBtn.addEventListener("click", openAppSearchModal); appSearchCloseBtn.addEventListener("click", closeAppSearchModal); appSearchModal.addEventListener("click", (event) => { if (event.target === appSearchModal) closeAppSearchModal(); }); appSearchInput.addEventListener("input", renderAppSearchResults); appManageBackBtn.addEventListener("click", closeAppManage); appManageSearchInput.addEventListener("input", renderAppManageTable); appSearchResults.addEventListener("click", (event) => { const btn = event.target.closest("[data-pa-app-key]"); if (!btn) return; const moduleKey = btn.getAttribute("data-pa-app-module"); const key = btn.getAttribute("data-pa-app-key"); const entry = pidFlattenAplicacoes().find((a) => a.moduleKey === moduleKey && a.key === key); if (entry) openAppManage(entry); }); appManageTbody.addEventListener("change", async (event) => { const checkbox = event.target.closest("[data-pa-app-key]"); if (!checkbox || !appSearchSelected) return; const codigo = Number(checkbox.getAttribute("data-pa-app-perfil")); const key = checkbox.getAttribute("data-pa-app-key"); const profile = profiles.find((p) => p.codigo === codigo); if (!profile) return; const perm = profile.permissions[appSearchSelected.moduleKey]; const checked = checkbox.checked; const wasEnabled = perm.enabled; perm.apps[key] = checked; if (checked && !perm.enabled) perm.enabled = true; checkbox.disabled = true; try { await pidUpdatePerfilPermissoes(profile.codigo, profile.permissions); } catch (e) { perm.apps[key] = !checked; perm.enabled = wasEnabled; checkbox.checked = !checked; window.alert(`Não foi possível salvar: ${e.message}`); } finally { checkbox.disabled = false; } }); tableBody.addEventListener("click", async (event) => { const editTarget = event.target.closest("[data-pa-edit]"); if (editTarget) { event.preventDefault(); await openEdit(Number(editTarget.getAttribute("data-pa-edit"))); return; } const deleteTarget = event.target.closest("[data-pa-delete]"); if (deleteTarget) { const codigo = Number(deleteTarget.getAttribute("data-pa-delete")); const profile = profiles.find((p) => p.codigo === codigo); if (profile && window.confirm(`Excluir o perfil "${profile.nome}"?`)) { await pidDeletePerfil(codigo); profiles = (await pidFetchPerfis()).map(pidPerfilFromApi); renderList(); } } }); document.querySelectorAll("[data-pa-tab]").forEach((tab) => { tab.addEventListener("click", () => { activeTab = tab.getAttribute("data-pa-tab"); renderTabs(); }); }); moduleListEl.addEventListener("click", (event) => { const btn = event.target.closest("[data-pa-module]"); if (!btn) return; activeModuleKey = btn.getAttribute("data-pa-module"); renderModuleList(); renderTree(); }); treeSearchInput.addEventListener("input", renderTree); treePanel.addEventListener("click", (event) => { const subgroupExpandBtn = event.target.closest("[data-perm-subgroup-expand]"); if (subgroupExpandBtn) { const key = subgroupExpandBtn.getAttribute("data-perm-subgroup-expand"); if (expandedSubgroups.has(key)) { expandedSubgroups.delete(key); } else { expandedSubgroups.add(key); } renderTree(); return; } const expandBtn = event.target.closest("[data-perm-expand]"); if (!expandBtn) return; if (expandedModules.has(activeModuleKey)) { expandedModules.delete(activeModuleKey); } else { expandedModules.add(activeModuleKey); } renderTree(); }); treePanel.addEventListener("change", (event) => { const perm = draftProfile.permissions[activeModuleKey]; const roleInput = event.target.closest("[data-perm-role]"); if (roleInput) { const checked = roleInput.checked; perm.enabled = checked; Object.keys(perm.apps).forEach((key) => { perm.apps[key] = checked; }); renderModuleList(); renderTree(); return; } const groupInput = event.target.closest("[data-perm-group]"); if (groupInput) { const entryKey = groupInput.getAttribute("data-perm-group"); const entries = PID_MODULE_APPS[activeModuleKey] || []; const entry = entries.find((e) => e.key === entryKey); if (entry && entry.tools) { entry.tools.forEach((tool) => { perm.apps[tool.key] = groupInput.checked; }); } renderTree(); return; } const appInput = event.target.closest("[data-perm-app]"); if (appInput) { perm.apps[appInput.getAttribute("data-perm-app")] = appInput.checked; } }); // Vincula (available → linked) ou desvincula (linked → available) todos // os usuários selecionados no painel de uma vez, em paralelo. async function bulkAlterarVinculo(kind, vincular) { const state = usersDualState[kind]; const ids = Array.from(state.selected); if (!ids.length) return; const el = usersPanelsEl[kind]; el.linkBtn.disabled = true; try { await Promise.all( ids.map((id) => { const account = usuariosCacheAtual.find((u) => u.id === id); if (!account) return Promise.resolve(); const codes = account.perfis.map((p) => p.codigo).filter((c) => c !== draftProfile.codigo); if (vincular) codes.push(draftProfile.codigo); return pidSetUsuarioPerfis(id, codes); }) ); } finally { state.selected.clear(); await refreshUsuarios(); } } ["available", "linked"].forEach((kind) => { const el = usersPanelsEl[kind]; el.refreshBtn.addEventListener("click", refreshUsuarios); el.searchNome.addEventListener("input", renderUsersFromCache); el.searchEmail.addEventListener("input", renderUsersFromCache); el.sortHeader.addEventListener("click", () => { usersDualState[kind].sortDir *= -1; renderUsersFromCache(); }); el.selectAll.addEventListener("change", () => { const filtrados = usersDualFiltrados(kind, listaParaPainelUsuarios(kind)); filtrados.forEach((u) => { if (el.selectAll.checked) usersDualState[kind].selected.add(u.id); else usersDualState[kind].selected.delete(u.id); }); renderUsersFromCache(); }); el.body.addEventListener("change", (event) => { const checkbox = event.target.closest("[data-pa-users-select]"); if (!checkbox) return; const id = Number(checkbox.getAttribute("data-pa-users-select")); if (checkbox.checked) usersDualState[kind].selected.add(id); else usersDualState[kind].selected.delete(id); renderUsersFromCache(); }); el.linkBtn.addEventListener("click", () => bulkAlterarVinculo(kind, kind === "available")); }); saveBtn.addEventListener("click", async () => { const nome = formNome.value.trim(); if (!nome) { formError.textContent = "Informe o nome do perfil."; return; } formError.textContent = ""; draftProfile.nome = nome; try { if (draftProfile.codigo === null) { await pidCreatePerfil(pidPerfilToApi(draftProfile)); } else { await pidUpdatePerfil(draftProfile.codigo, pidPerfilToApi(draftProfile)); } profiles = (await pidFetchPerfis()).map(pidPerfilFromApi); closeEdit(); } catch (e) { formError.textContent = e.message; } }); renderList(); });