portal_publico/static/js/acessos-gerais.js

612 lines
22 KiB
JavaScript

const PID_AG_ROW_ICON =
'<svg class="ag-row__icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
const PID_AG_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
async function pidFetchAcessoGeralSecoes() {
return (await pidApiRequest("/acessos-gerais-secoes/")) || [];
}
async function pidCreateAcessoGeralSecao(payload) {
return pidApiRequest("/acessos-gerais-secoes/", { method: "POST", body: payload });
}
async function pidUpdateAcessoGeralSecao(id, payload) {
return pidApiRequest(`/acessos-gerais-secoes/${id}/`, { method: "PATCH", body: payload });
}
async function pidDeleteAcessoGeralSecao(id) {
return pidApiRequest(`/acessos-gerais-secoes/${id}/`, { method: "DELETE" });
}
async function pidFetchAcessosGerais() {
return (await pidApiRequest("/acessos-gerais/")) || [];
}
async function pidCreateAcessoGeral(payload) {
return pidApiRequest("/acessos-gerais/", { method: "POST", body: payload });
}
async function pidUpdateAcessoGeral(id, payload) {
return pidApiRequest(`/acessos-gerais/${id}/`, { method: "PATCH", body: payload });
}
async function pidDeleteAcessoGeral(id) {
return pidApiRequest(`/acessos-gerais/${id}/`, { method: "DELETE" });
}
document.addEventListener("DOMContentLoaded", async () => {
const sectionsEl = document.getElementById("ag-sections");
if (!sectionsEl) return;
const me = await pidGetMe();
if (!me) return;
const agPerms = (me.permissoes_efetivas && me.permissoes_efetivas["links-ferramentas"]) || {};
const canView = !!(agPerms.apps && agPerms.apps["acessos-gerais-visualizar"]);
const canManage = !!(agPerms.apps && agPerms.apps["acessos-gerais-editar"]);
const noAccessEl = document.getElementById("ag-no-access");
const contentEl = document.getElementById("ag-content");
if (noAccessEl) noAccessEl.hidden = canView;
if (contentEl) contentEl.hidden = !canView;
if (!canView) return;
const emptyEl = document.getElementById("ag-empty");
const addSecaoBtn = document.getElementById("ag-add-secao-btn");
if (addSecaoBtn) addSecaoBtn.hidden = !canManage;
// Modal de seção (adicionar/renomear)
const secaoModal = document.getElementById("ag-secao-modal");
const secaoFormTitle = document.getElementById("ag-secao-form-title");
const secaoFormNome = document.getElementById("ag-secao-form-nome");
const secaoFormPerfis = document.getElementById("ag-secao-form-perfis");
const secaoFormError = document.getElementById("ag-secao-form-error");
const secaoSaveBtn = document.getElementById("ag-secao-form-save-btn");
const secaoCancelBtn = document.getElementById("ag-secao-form-cancel-btn");
// Modal de acesso (adicionar/editar)
const formModal = document.getElementById("ag-form-modal");
const formTitle = document.getElementById("ag-form-title");
const formNome = document.getElementById("ag-form-nome");
const formUrl = document.getElementById("ag-form-url");
const formUsuario = document.getElementById("ag-form-usuario");
const formSenha = document.getElementById("ag-form-senha");
const formSenhaToggle = document.getElementById("ag-form-senha-toggle");
const formObservacoes = document.getElementById("ag-form-observacoes");
const formError = document.getElementById("ag-form-error");
const formSaveBtn = document.getElementById("ag-form-save-btn");
const formCancelBtn = document.getElementById("ag-form-cancel-btn");
// Modal de visualização (popup ao clicar numa linha)
const viewModal = document.getElementById("ag-view-modal");
const viewNome = document.getElementById("ag-view-nome");
const viewUrlField = document.getElementById("ag-view-url-field");
const viewUrl = document.getElementById("ag-view-url");
const viewUsuarioField = document.getElementById("ag-view-usuario-field");
const viewUsuario = document.getElementById("ag-view-usuario");
const viewSenhaField = document.getElementById("ag-view-senha-field");
const viewSenha = document.getElementById("ag-view-senha");
const viewSenhaToggle = document.getElementById("ag-view-senha-toggle");
const viewObservacoesField = document.getElementById("ag-view-observacoes-field");
const viewObservacoes = document.getElementById("ag-view-observacoes");
const viewActions = document.getElementById("ag-view-actions");
const viewEditBtn = document.getElementById("ag-view-edit-btn");
const viewDeleteBtn = document.getElementById("ag-view-delete-btn");
const viewCloseBtn = document.getElementById("ag-view-close-btn");
pidBindPasswordToggle(formSenhaToggle, formSenha);
let secoes = [];
let acessos = [];
let editingSecaoId = null;
let editingAcessoId = null;
let viewingAcessoId = null;
function acessosDaSecao(secaoId) {
return acessos.filter((a) => a.secao === secaoId);
}
function rowHtml(acesso, index, total) {
const adminHtml = canManage
? `
<button type="button" class="ag-row__move" data-acesso-move-up="${acesso.id}" ${index === 0 ? "disabled" : ""} aria-label="Mover para cima">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button type="button" class="ag-row__move" data-acesso-move-down="${acesso.id}" ${index === total - 1 ? "disabled" : ""} aria-label="Mover para baixo">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>`
: "";
return `
<li class="ag-row${canManage ? " ag-row--draggable" : ""}" data-acesso-id="${acesso.id}" ${canManage ? 'draggable="true"' : ""}>
<button type="button" class="ag-row__open" data-acesso-open="${acesso.id}">
${PID_AG_ROW_ICON}
<span class="ag-row__label">${acesso.nome}</span>
</button>
<div class="ag-row__admin">${adminHtml}</div>
</li>`;
}
function sectionHtml(secao) {
const rows = acessosDaSecao(secao.id);
const rowsHtml = rows.length
? rows.map((a, index) => rowHtml(a, index, rows.length)).join("")
: `<li class="ag-row-empty">Nenhum acesso cadastrado nesta seção.</li>`;
const actionsHtml = canManage
? `
<div class="ag-section__actions">
<button type="button" class="icon-btn" data-secao-edit="${secao.id}" aria-label="Renomear seção">
<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="icon-btn" data-secao-delete="${secao.id}" aria-label="Excluir seção">
<svg width="14" height="14" 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>`
: "";
const addRowBtnHtml = canManage
? `<button type="button" class="ag-add-row-btn" data-secao-add="${secao.id}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14" stroke-linecap="round"/></svg>
Adicionar Acesso
</button>`
: "";
return `
<section class="ag-section" data-secao-id="${secao.id}">
<div class="ag-section__header">
<h3 class="ag-section__title">${secao.nome}</h3>
${actionsHtml}
</div>
<ul class="ag-row-list" data-secao-list="${secao.id}">${rowsHtml}</ul>
${addRowBtnHtml}
</section>`;
}
function render() {
sectionsEl.innerHTML = secoes.map(sectionHtml).join("");
if (emptyEl) emptyEl.hidden = secoes.length !== 0;
}
async function refresh() {
try {
const [fetchedSecoes, fetchedAcessos] = await Promise.all([
pidFetchAcessoGeralSecoes(),
pidFetchAcessosGerais(),
]);
secoes = fetchedSecoes;
acessos = fetchedAcessos;
} catch (e) {
secoes = [];
acessos = [];
}
render();
}
// --- Modal de seção ---
let perfisCache = [];
async function ensurePerfisCache() {
if (!perfisCache.length) {
perfisCache = (await pidFetchPerfis()).slice().sort((a, b) => a.nome.localeCompare(b.nome));
}
}
function renderSecaoPerfisChecklist(selectedCodigos) {
secaoFormPerfis.innerHTML = perfisCache
.map(
(p) => `
<label class="checklist-item">
<input type="checkbox" value="${p.codigo}" ${selectedCodigos.includes(p.codigo) ? "checked" : ""} />
<span>${p.nome}</span>
</label>`
)
.join("");
}
function selectedSecaoPerfis() {
return Array.from(secaoFormPerfis.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => Number(cb.value));
}
async function openSecaoModal(secao) {
editingSecaoId = secao ? secao.id : null;
secaoFormTitle.textContent = secao ? "Renomear Seção" : "Adicionar Seção";
secaoFormNome.value = secao ? secao.nome : "";
secaoFormError.textContent = "";
secaoSaveBtn.textContent = secao ? "Salvar" : "Adicionar";
await ensurePerfisCache();
renderSecaoPerfisChecklist(secao ? secao.perfis_restritos : []);
secaoModal.hidden = false;
secaoFormNome.focus();
}
function closeSecaoModal() {
secaoModal.hidden = true;
editingSecaoId = null;
}
if (addSecaoBtn) addSecaoBtn.addEventListener("click", () => openSecaoModal(null));
if (secaoCancelBtn) secaoCancelBtn.addEventListener("click", closeSecaoModal);
if (secaoModal) {
secaoModal.addEventListener("click", (event) => {
if (event.target === secaoModal) closeSecaoModal();
});
}
if (secaoSaveBtn) {
secaoSaveBtn.addEventListener("click", async () => {
const nome = secaoFormNome.value.trim();
if (!nome) {
secaoFormError.textContent = "Informe o nome da seção.";
return;
}
secaoFormError.textContent = "";
const payload = { nome, perfis_restritos: selectedSecaoPerfis() };
try {
if (editingSecaoId) {
await pidUpdateAcessoGeralSecao(editingSecaoId, payload);
} else {
await pidCreateAcessoGeralSecao(payload);
}
closeSecaoModal();
await refresh();
} catch (e) {
secaoFormError.textContent = e.message;
}
});
}
// --- Modal de acesso (adicionar/editar) ---
let formSecaoId = null;
function openFormModal(secaoId, acesso) {
formSecaoId = secaoId;
editingAcessoId = acesso ? acesso.id : null;
formTitle.textContent = acesso ? "Editar Acesso" : "Adicionar Acesso";
formNome.value = acesso ? acesso.nome : "";
formUrl.value = acesso ? acesso.url : "";
formUsuario.value = acesso ? acesso.usuario : "";
formSenha.value = acesso ? acesso.senha : "";
formSenha.type = "password";
formObservacoes.innerHTML = acesso ? acesso.observacoes : "";
formError.textContent = "";
formSaveBtn.textContent = acesso ? "Salvar" : "Adicionar";
formModal.hidden = false;
formNome.focus();
}
// --- Observações ricas (contenteditable + imagens embutidas via colar/arrastar) ---
function observacoesValue() {
const hasImage = formObservacoes.querySelector("img") !== null;
const hasText = formObservacoes.textContent.trim() !== "";
return hasImage || hasText ? formObservacoes.innerHTML : "";
}
function placeCaretAtPoint(x, y) {
const selection = window.getSelection();
if (document.caretRangeFromPoint) {
const range = document.caretRangeFromPoint(x, y);
if (range) {
selection.removeAllRanges();
selection.addRange(range);
}
} else if (document.caretPositionFromPoint) {
const pos = document.caretPositionFromPoint(x, y);
if (pos) {
const range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
function insertImageFile(file, atPoint) {
if (!file || !file.type.startsWith("image/")) return;
if (file.size > PID_AG_IMAGE_MAX_BYTES) {
formError.textContent = "A imagem deve ter no máximo 2MB.";
return;
}
formError.textContent = "";
const reader = new FileReader();
reader.onload = () => {
formObservacoes.focus();
if (atPoint) placeCaretAtPoint(atPoint.x, atPoint.y);
document.execCommand("insertImage", false, reader.result);
};
reader.readAsDataURL(file);
}
if (formObservacoes) {
formObservacoes.addEventListener("paste", (event) => {
const items = Array.from((event.clipboardData && event.clipboardData.items) || []);
const imageItem = items.find((item) => item.type.startsWith("image/"));
if (!imageItem) return;
event.preventDefault();
insertImageFile(imageItem.getAsFile());
});
formObservacoes.addEventListener("dragover", (event) => {
event.preventDefault();
});
formObservacoes.addEventListener("drop", (event) => {
const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
if (!file || !file.type.startsWith("image/")) return;
event.preventDefault();
insertImageFile(file, { x: event.clientX, y: event.clientY });
});
}
function closeFormModal() {
formModal.hidden = true;
editingAcessoId = null;
formSecaoId = null;
}
if (formCancelBtn) formCancelBtn.addEventListener("click", closeFormModal);
if (formModal) {
formModal.addEventListener("click", (event) => {
if (event.target === formModal) closeFormModal();
});
}
if (formSaveBtn) {
formSaveBtn.addEventListener("click", async () => {
const nome = formNome.value.trim();
if (!nome) {
formError.textContent = "Informe o nome do acesso.";
return;
}
formError.textContent = "";
const payload = {
nome,
url: formUrl.value.trim(),
usuario: formUsuario.value.trim(),
senha: formSenha.value,
observacoes: observacoesValue(),
};
if (!editingAcessoId) payload.secao = formSecaoId;
try {
if (editingAcessoId) {
await pidUpdateAcessoGeral(editingAcessoId, payload);
} else {
await pidCreateAcessoGeral(payload);
}
closeFormModal();
await refresh();
} catch (e) {
formError.textContent = e.message;
}
});
}
// --- Modal de visualização (popup) ---
function setViewField(fieldEl, valueEl, value) {
if (!fieldEl) return;
fieldEl.hidden = !value;
if (valueEl) valueEl.textContent = value || "";
}
function openViewModal(acesso) {
viewingAcessoId = acesso.id;
viewNome.textContent = acesso.nome;
if (viewUrlField) {
viewUrlField.hidden = !acesso.url;
if (viewUrl) {
viewUrl.textContent = acesso.url;
viewUrl.href = acesso.url;
}
}
setViewField(viewUsuarioField, viewUsuario, acesso.usuario);
if (viewSenhaField) {
viewSenhaField.hidden = !acesso.senha;
if (viewSenha) {
viewSenha.dataset.value = acesso.senha || "";
viewSenha.textContent = "••••••••";
}
if (viewSenhaToggle) viewSenhaToggle.setAttribute("aria-label", "Mostrar senha");
}
if (viewObservacoesField) {
viewObservacoesField.hidden = !acesso.observacoes;
if (viewObservacoes) viewObservacoes.innerHTML = acesso.observacoes || "";
}
if (viewActions) viewActions.hidden = !canManage;
viewModal.hidden = false;
}
function closeViewModal() {
viewModal.hidden = true;
viewingAcessoId = null;
}
if (viewCloseBtn) viewCloseBtn.addEventListener("click", closeViewModal);
if (viewModal) {
viewModal.addEventListener("click", (event) => {
if (event.target === viewModal) closeViewModal();
});
}
if (viewSenhaToggle) {
viewSenhaToggle.addEventListener("click", () => {
const revealed = viewSenha.textContent === viewSenha.dataset.value;
viewSenha.textContent = revealed ? "••••••••" : viewSenha.dataset.value;
viewSenhaToggle.setAttribute("aria-label", revealed ? "Mostrar senha" : "Ocultar senha");
});
}
if (viewEditBtn) {
viewEditBtn.addEventListener("click", () => {
const acesso = acessos.find((a) => a.id === viewingAcessoId);
if (!acesso) return;
closeViewModal();
openFormModal(acesso.secao, acesso);
});
}
if (viewDeleteBtn) {
viewDeleteBtn.addEventListener("click", async () => {
const acesso = acessos.find((a) => a.id === viewingAcessoId);
if (!acesso) return;
if (!window.confirm(`Excluir o acesso "${acesso.nome}"?`)) return;
await pidDeleteAcessoGeral(acesso.id);
closeViewModal();
await refresh();
});
}
// --- Ações na grade (seções e linhas) ---
async function swapOrdem(a, b) {
await Promise.all([pidUpdateAcessoGeral(a.id, { ordem: b.ordem }), pidUpdateAcessoGeral(b.id, { ordem: a.ordem })]);
await refresh();
}
sectionsEl.addEventListener("click", async (event) => {
const secaoEditBtn = event.target.closest("[data-secao-edit]");
if (secaoEditBtn) {
const id = Number(secaoEditBtn.getAttribute("data-secao-edit"));
const secao = secoes.find((s) => s.id === id);
if (secao) openSecaoModal(secao);
return;
}
const secaoDeleteBtn = event.target.closest("[data-secao-delete]");
if (secaoDeleteBtn) {
const id = Number(secaoDeleteBtn.getAttribute("data-secao-delete"));
const secao = secoes.find((s) => s.id === id);
if (secao && window.confirm(`Excluir a seção "${secao.nome}" e todos os seus acessos?`)) {
await pidDeleteAcessoGeralSecao(id);
await refresh();
}
return;
}
const addRowBtn = event.target.closest("[data-secao-add]");
if (addRowBtn) {
const secaoId = Number(addRowBtn.getAttribute("data-secao-add"));
openFormModal(secaoId, null);
return;
}
const openBtn = event.target.closest("[data-acesso-open]");
if (openBtn) {
const id = Number(openBtn.getAttribute("data-acesso-open"));
const acesso = acessos.find((a) => a.id === id);
if (acesso) openViewModal(acesso);
return;
}
const upBtn = event.target.closest("[data-acesso-move-up]");
if (upBtn) {
const id = Number(upBtn.getAttribute("data-acesso-move-up"));
const acesso = acessos.find((a) => a.id === id);
if (!acesso) return;
const irmaos = acessosDaSecao(acesso.secao);
const index = irmaos.findIndex((a) => a.id === id);
if (index > 0) await swapOrdem(irmaos[index], irmaos[index - 1]);
return;
}
const downBtn = event.target.closest("[data-acesso-move-down]");
if (downBtn) {
const id = Number(downBtn.getAttribute("data-acesso-move-down"));
const acesso = acessos.find((a) => a.id === id);
if (!acesso) return;
const irmaos = acessosDaSecao(acesso.secao);
const index = irmaos.findIndex((a) => a.id === id);
if (index !== -1 && index < irmaos.length - 1) await swapOrdem(irmaos[index], irmaos[index + 1]);
}
});
// --- Drag-and-drop (reordenar linhas dentro da mesma seção) ---
let draggedId = null;
let dropTargetId = null;
let dropBefore = true;
function clearDropHighlight() {
sectionsEl.querySelectorAll(".ag-row--drop-before, .ag-row--drop-after").forEach((el) => {
el.classList.remove("ag-row--drop-before", "ag-row--drop-after");
});
}
sectionsEl.addEventListener("dragstart", (event) => {
const row = event.target.closest(".ag-row--draggable");
if (!row) return;
draggedId = Number(row.dataset.acessoId);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", String(draggedId));
requestAnimationFrame(() => row.classList.add("ag-row--dragging"));
});
sectionsEl.addEventListener("dragover", (event) => {
if (draggedId === null) return;
const row = event.target.closest(".ag-row--draggable");
if (!row || Number(row.dataset.acessoId) === draggedId) return;
const draggedAcesso = acessos.find((a) => a.id === draggedId);
const targetAcesso = acessos.find((a) => a.id === Number(row.dataset.acessoId));
if (!draggedAcesso || !targetAcesso || draggedAcesso.secao !== targetAcesso.secao) return;
event.preventDefault();
const rect = row.getBoundingClientRect();
dropBefore = event.clientY < rect.top + rect.height / 2;
dropTargetId = Number(row.dataset.acessoId);
clearDropHighlight();
row.classList.add(dropBefore ? "ag-row--drop-before" : "ag-row--drop-after");
});
sectionsEl.addEventListener("drop", async (event) => {
if (draggedId === null) return;
event.preventDefault();
clearDropHighlight();
const fromId = draggedId;
const toId = dropTargetId;
draggedId = null;
dropTargetId = null;
if (toId === null || fromId === toId) return;
const fromAcesso = acessos.find((a) => a.id === fromId);
if (!fromAcesso) return;
const irmaos = acessosDaSecao(fromAcesso.secao);
const fromIndex = irmaos.findIndex((a) => a.id === fromId);
if (fromIndex === -1) return;
const reordered = irmaos.slice();
const [moved] = reordered.splice(fromIndex, 1);
let insertIndex = reordered.findIndex((a) => a.id === toId);
if (insertIndex === -1) return;
if (!dropBefore) insertIndex += 1;
reordered.splice(insertIndex, 0, moved);
const updates = reordered.map((acesso, index) => ({ acesso, index })).filter(({ acesso, index }) => acesso.ordem !== index);
if (!updates.length) return;
acessos = acessos.map((a) => {
const update = updates.find((u) => u.acesso.id === a.id);
return update ? { ...a, ordem: update.index } : a;
});
render();
await Promise.all(updates.map(({ acesso, index }) => pidUpdateAcessoGeral(acesso.id, { ordem: index })));
await refresh();
});
sectionsEl.addEventListener("dragend", () => {
draggedId = null;
dropTargetId = null;
clearDropHighlight();
sectionsEl.querySelectorAll(".ag-row--dragging").forEach((el) => el.classList.remove("ag-row--dragging"));
});
await refresh();
});