portal_publico/static/js/profiles.js

743 lines
27 KiB
JavaScript

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) => `
<tr>
<td>${p.codigo}</td>
<td>
<a href="#" class="pa-name-link" data-pa-edit="${p.codigo}">${p.nome}</a>
${p.gerenciaPermissoes ? '<span class="badge-total-access">Acesso total</span>' : ""}
</td>
<td>
<div class="pa-row-actions">
<button type="button" class="icon-btn" data-pa-edit="${p.codigo}" aria-label="Editar perfil">
<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-pa-delete="${p.codigo}" aria-label="Excluir perfil">
<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="3">Nenhum perfil encontrado.</td></tr>`;
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 `
<button type="button" class="pa-module-item ${m.key === activeModuleKey ? "is-active" : ""}" data-pa-module="${m.key}">
<span class="pa-module-item__index">${idx + 1}</span>
<span class="pa-module-item__label">${m.label}</span>
<svg class="pa-module-item__check" ${enabled ? "" : "hidden"} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>`;
}).join("");
}
function renderLeaf(perm, tool, term) {
const match = !term || tool.label.toLowerCase().includes(term);
return `
<label class="perm-node perm-node--leaf ${match ? "" : "is-hidden"}">
<input type="checkbox" data-perm-app="${tool.key}" ${perm.apps[tool.key] ? "checked" : ""} />
<span>${tool.label}</span>
</label>`;
}
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 `
<div class="perm-node perm-node--group ${anyMatch ? "" : "is-hidden"}">
<button type="button" class="perm-expand ${subgroupExpanded ? "is-open" : ""}" data-perm-subgroup-expand="${subgroupKey}" aria-label="Expandir">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<label>
<input type="checkbox" data-perm-group="${entry.key}" ${groupChecked ? "checked" : ""} />
<span>${entry.label}</span>
</label>
</div>
<div class="perm-children perm-children--nested" data-perm-children ${subgroupExpanded ? "" : "hidden"}>
${entry.tools.map((tool) => renderLeaf(perm, tool, term)).join("")}
</div>`;
}
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 = `
<label class="perm-node perm-node--root">
<input type="checkbox" data-perm-role="root" ${perm.enabled ? "checked" : ""} />
<span>Todos</span>
</label>
<div class="perm-node perm-node--module">
${
hasEntries
? `<button type="button" class="perm-expand ${expanded ? "is-open" : ""}" data-perm-expand aria-label="Expandir">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>`
: ""
}
<label>
<input type="checkbox" data-perm-role="module" ${perm.enabled ? "checked" : ""} />
<span>${moduleLabel}</span>
</label>
</div>
${
hasEntries
? `<div class="perm-children" data-perm-children ${expanded ? "" : "hidden"}>
${entries.map((entry) => renderEntry(perm, entry, term)).join("")}
</div>`
: ""
}
`;
}
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) => `
<tr>
<td><input type="checkbox" data-pa-users-select="${u.id}" ${state.selected.has(u.id) ? "checked" : ""} /></td>
<td>${u.nome}</td>
<td>${u.email || "—"}</td>
</tr>`
)
.join("")
: `<tr class="pa-empty-row"><td colspan="3">Nenhum usuário encontrado.</td></tr>`;
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) => `
<button type="button" class="pa-app-result-item" data-pa-app-module="${a.moduleKey}" data-pa-app-key="${a.key}">
<span class="pa-app-result-item__label">${a.label}</span>
<span class="pa-app-result-item__module">${a.moduleLabel}</span>
</button>`
)
.join("")
: `<p class="checklist-empty">Nenhuma aplicação encontrada.</p>`;
}
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 = `
<tr>
<th>Perfil</th>
${tools.map((tool) => `<th>${tool.label.split(" (")[0]}</th>`).join("")}
</tr>`;
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 `
<tr>
<td>${p.nome} ${p.ativo ? "" : '<span class="status-pill status-pill--inativo">Inativo</span>'}</td>
${tools
.map(
(tool) => `
<td><input type="checkbox" data-pa-app-perfil="${p.codigo}" data-pa-app-key="${tool.key}" ${
perm.apps[tool.key] ? "checked" : ""
} /></td>`
)
.join("")}
</tr>`;
})
.join("")
: `<tr class="pa-empty-row"><td colspan="${1 + tools.length}">Nenhum perfil encontrado.</td></tr>`;
}
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;
await pidAlert(`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 && (await pidConfirm(`Excluir o perfil "${profile.nome}"?`, { perigoso: true }))) {
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();
});