709 lines
26 KiB
JavaScript
709 lines
26 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;
|
||
// Retratos dos formulários na abertura, pra só perguntar "Sair sem salvar"
|
||
// quando algo mudou de fato (inclusive imagem colada nas observações).
|
||
let retratoSecao = null;
|
||
let retratoAcesso = null;
|
||
const textoVazio = emptyEl ? emptyEl.textContent : "";
|
||
const msgErroCarga = "Não foi possível carregar. Recarregue a página; se persistir, contate a Inovação.";
|
||
|
||
function confirmarSairSemSalvar() {
|
||
return pidConfirm("Sair sem salvar as alterações?", {
|
||
titulo: "Sair sem salvar",
|
||
textoConfirmar: "Sair sem salvar",
|
||
perigoso: true,
|
||
});
|
||
}
|
||
|
||
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">${pidEscapeHtml(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">${pidEscapeHtml(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) {
|
||
// Falha de carga não pode parecer "Nenhuma seção cadastrada ainda.".
|
||
secoes = [];
|
||
acessos = [];
|
||
render();
|
||
if (emptyEl) {
|
||
emptyEl.textContent = msgErroCarga;
|
||
emptyEl.hidden = false;
|
||
}
|
||
return;
|
||
}
|
||
if (emptyEl) emptyEl.textContent = textoVazio;
|
||
render();
|
||
}
|
||
|
||
// --- Modal de seção ---
|
||
|
||
let perfisCache = [];
|
||
|
||
// Lista enxuta da própria aplicação: `/api/perfis/` exige gerenciar
|
||
// permissões, e quem só edita Acessos Gerais não abria o modal.
|
||
async function ensurePerfisCache() {
|
||
if (!perfisCache.length) {
|
||
perfisCache = (await pidApiRequest("/acessos-gerais-secoes/perfis/"))
|
||
.slice()
|
||
.sort((a, b) => a.nome.localeCompare(b.nome, "pt-BR"));
|
||
}
|
||
}
|
||
|
||
function renderSecaoPerfisChecklist(selectedCodigos) {
|
||
secaoFormPerfis.innerHTML = perfisCache
|
||
.map(
|
||
(p) => `
|
||
<label class="checklist-item">
|
||
<input type="checkbox" value="${p.codigo}" ${selectedCodigos.includes(p.codigo) ? "checked" : ""} />
|
||
<span>${pidEscapeHtml(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";
|
||
try {
|
||
await ensurePerfisCache();
|
||
} catch (e) {
|
||
pidAlert(`${e.message} Tente novamente; se persistir, contate a Inovação.`, { titulo: "Não foi possível abrir a seção" });
|
||
return;
|
||
}
|
||
renderSecaoPerfisChecklist(secao ? secao.perfis_restritos : []);
|
||
retratoSecao = retratoSecaoAtual();
|
||
secaoModal.hidden = false;
|
||
secaoFormNome.focus();
|
||
}
|
||
|
||
function retratoSecaoAtual() {
|
||
return JSON.stringify([secaoFormNome.value, selectedSecaoPerfis().sort((a, b) => a - b)]);
|
||
}
|
||
|
||
function closeSecaoModal() {
|
||
secaoModal.hidden = true;
|
||
editingSecaoId = null;
|
||
retratoSecao = null;
|
||
}
|
||
|
||
async function tentarFecharSecaoModal() {
|
||
if (retratoSecao !== null && retratoSecaoAtual() !== retratoSecao && !(await confirmarSairSemSalvar())) return;
|
||
closeSecaoModal();
|
||
}
|
||
|
||
if (addSecaoBtn) addSecaoBtn.addEventListener("click", () => openSecaoModal(null));
|
||
if (secaoCancelBtn) secaoCancelBtn.addEventListener("click", tentarFecharSecaoModal);
|
||
if (secaoModal) {
|
||
secaoModal.addEventListener("click", (event) => {
|
||
if (event.target === secaoModal) tentarFecharSecaoModal();
|
||
});
|
||
}
|
||
|
||
if (secaoSaveBtn) {
|
||
secaoSaveBtn.addEventListener("click", async () => {
|
||
const nome = secaoFormNome.value.trim();
|
||
if (!nome) {
|
||
secaoFormError.textContent = "Informe o nome da seção.";
|
||
secaoFormNome.focus();
|
||
return;
|
||
}
|
||
secaoFormError.textContent = "";
|
||
const payload = { nome, perfis_restritos: selectedSecaoPerfis() };
|
||
// Trava contra clique duplo (criaria a seção duas vezes).
|
||
const textoOriginal = secaoSaveBtn.textContent;
|
||
secaoSaveBtn.disabled = true;
|
||
secaoSaveBtn.textContent = editingSecaoId ? "Salvando…" : "Adicionando…";
|
||
try {
|
||
if (editingSecaoId) {
|
||
await pidUpdateAcessoGeralSecao(editingSecaoId, payload);
|
||
} else {
|
||
await pidCreateAcessoGeralSecao(payload);
|
||
}
|
||
closeSecaoModal();
|
||
await refresh();
|
||
} catch (e) {
|
||
secaoFormError.textContent = e.message;
|
||
} finally {
|
||
secaoSaveBtn.disabled = false;
|
||
secaoSaveBtn.textContent = textoOriginal;
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- 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";
|
||
retratoAcesso = retratoAcessoAtual();
|
||
formModal.hidden = false;
|
||
formNome.focus();
|
||
}
|
||
|
||
function retratoAcessoAtual() {
|
||
return JSON.stringify([formNome.value, formUrl.value, formUsuario.value, formSenha.value, formObservacoes.innerHTML]);
|
||
}
|
||
|
||
// --- 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 2 MB.";
|
||
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;
|
||
retratoAcesso = null;
|
||
}
|
||
|
||
async function tentarFecharFormModal() {
|
||
if (retratoAcesso !== null && retratoAcessoAtual() !== retratoAcesso && !(await confirmarSairSemSalvar())) return;
|
||
closeFormModal();
|
||
}
|
||
|
||
if (formCancelBtn) formCancelBtn.addEventListener("click", tentarFecharFormModal);
|
||
if (formModal) {
|
||
formModal.addEventListener("click", (event) => {
|
||
if (event.target === formModal) tentarFecharFormModal();
|
||
});
|
||
}
|
||
|
||
if (formSaveBtn) {
|
||
formSaveBtn.addEventListener("click", async () => {
|
||
const nome = formNome.value.trim();
|
||
if (!nome) {
|
||
formError.textContent = "Informe o nome do acesso.";
|
||
formNome.focus();
|
||
return;
|
||
}
|
||
formError.textContent = "";
|
||
|
||
const payload = {
|
||
nome,
|
||
url: formUrl.value.trim(),
|
||
usuario: formUsuario.value.trim(),
|
||
senha: formSenha.value,
|
||
observacoes: observacoesValue(),
|
||
};
|
||
if (!editingAcessoId) payload.secao = formSecaoId;
|
||
|
||
// Trava contra clique duplo (observações com imagem deixam o envio lento).
|
||
const textoOriginal = formSaveBtn.textContent;
|
||
formSaveBtn.disabled = true;
|
||
formSaveBtn.textContent = editingAcessoId ? "Salvando…" : "Adicionando…";
|
||
try {
|
||
if (editingAcessoId) {
|
||
await pidUpdateAcessoGeral(editingAcessoId, payload);
|
||
} else {
|
||
await pidCreateAcessoGeral(payload);
|
||
}
|
||
closeFormModal();
|
||
await refresh();
|
||
} catch (e) {
|
||
formError.textContent = e.message;
|
||
} finally {
|
||
formSaveBtn.disabled = false;
|
||
formSaveBtn.textContent = textoOriginal;
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- 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 (!(await pidConfirm(`Excluir o acesso "${acesso.nome}"?`, { perigoso: true }))) return;
|
||
viewDeleteBtn.disabled = true;
|
||
try {
|
||
await pidDeleteAcessoGeral(acesso.id);
|
||
} catch (e) {
|
||
await pidAlert(e.message, { titulo: "Não foi possível excluir o acesso" });
|
||
return;
|
||
} finally {
|
||
viewDeleteBtn.disabled = false;
|
||
}
|
||
closeViewModal();
|
||
await refresh();
|
||
});
|
||
}
|
||
|
||
// --- Ações na grade (seções e linhas) ---
|
||
|
||
// Na falha, uma das duas gravações pode ter passado: recarrega pra tela
|
||
// mostrar a ordem real do servidor.
|
||
async function swapOrdem(a, b, botao) {
|
||
botao.disabled = true;
|
||
try {
|
||
await Promise.all([pidUpdateAcessoGeral(a.id, { ordem: b.ordem }), pidUpdateAcessoGeral(b.id, { ordem: a.ordem })]);
|
||
} catch (e) {
|
||
await pidAlert(e.message, { titulo: "Não foi possível mover o acesso" });
|
||
}
|
||
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 && (await pidConfirm(`Excluir a seção "${secao.nome}" e todos os seus acessos?`, { perigoso: true }))) {
|
||
secaoDeleteBtn.disabled = true;
|
||
try {
|
||
await pidDeleteAcessoGeralSecao(id);
|
||
} catch (e) {
|
||
secaoDeleteBtn.disabled = false;
|
||
await pidAlert(e.message, { titulo: "Não foi possível excluir a seção" });
|
||
return;
|
||
}
|
||
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], upBtn);
|
||
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], downBtn);
|
||
}
|
||
});
|
||
|
||
// --- 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;
|
||
|
||
// Reordena localmente as posições da seção no array (render() segue a
|
||
// ordem do array, não o campo `ordem`), pra linha mudar na hora.
|
||
const fila = reordered.map((a, index) => ({ ...a, ordem: index }));
|
||
let k = 0;
|
||
acessos = acessos.map((a) => (a.secao === fromAcesso.secao ? fila[k++] : a));
|
||
render();
|
||
try {
|
||
await Promise.all(updates.map(({ acesso, index }) => pidUpdateAcessoGeral(acesso.id, { ordem: index })));
|
||
} catch (e) {
|
||
await pidAlert(e.message, { titulo: "Não foi possível reordenar os acessos" });
|
||
}
|
||
// Sempre recarrega: na falha, desfaz a ordem que a tela mostrou antes da hora.
|
||
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();
|
||
});
|