612 lines
22 KiB
JavaScript
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">${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) {
|
|
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>${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";
|
|
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 (!(await pidConfirm(`Excluir o acesso "${acesso.nome}"?`, { perigoso: true }))) 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 && (await pidConfirm(`Excluir a seção "${secao.nome}" e todos os seus acessos?`, { perigoso: true }))) {
|
|
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();
|
|
});
|