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 usersAddSelect = document.getElementById("pa-user-add-select");
const usersAddBtn = document.getElementById("pa-user-add-btn");
const usersListEl = document.getElementById("pa-user-list");
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
? `
${entries.map((entry) => renderEntry(perm, entry, term)).join("")}
`
: ""
}
`;
}
async function renderUsers() {
const usuarios = await pidFetchUsuarios();
const isLinked = (u) => u.perfis.some((p) => p.codigo === draftProfile.codigo);
const linked = usuarios.filter(isLinked);
const unlinked = usuarios.filter((u) => !isLinked(u));
usersListEl.innerHTML = linked.length
? linked
.map(
(u) => `
${u.nome} (${u.username})
`
)
.join("")
: `Nenhum usuário vinculado a este perfil.
`;
usersAddSelect.innerHTML = unlinked.length
? `` +
unlinked
.map((u) => {
const otherNames = u.perfis.map((p) => p.nome);
const suffix = otherNames.length ? ` — também em: ${otherNames.join(", ")}` : "";
return ``;
})
.join("")
: ``;
}
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;
}
});
async function linkUser() {
const id = Number(usersAddSelect.value);
if (!id) return;
const usuarios = await pidFetchUsuarios();
const account = usuarios.find((u) => u.id === id);
if (!account) return;
const codes = account.perfis.map((p) => p.codigo);
if (!codes.includes(draftProfile.codigo)) codes.push(draftProfile.codigo);
await pidSetUsuarioPerfis(id, codes);
await renderUsers();
}
usersAddBtn.addEventListener("click", linkUser);
usersListEl.addEventListener("click", async (event) => {
const removeBtn = event.target.closest("[data-pa-user-unlink]");
if (!removeBtn) return;
const id = Number(removeBtn.getAttribute("data-pa-user-unlink"));
const usuarios = await pidFetchUsuarios();
const account = usuarios.find((u) => u.id === id);
if (!account) return;
const codes = account.perfis.map((p) => p.codigo).filter((c) => c !== draftProfile.codigo);
await pidSetUsuarioPerfis(id, codes);
await renderUsers();
});
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();
});