portal_publico/static/js/acessos-gerais.js

709 lines
26 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
});