const PID_LINK_DEFAULT_ICON = ''; const PID_LINK_ICON_MAX_BYTES = 2 * 1024 * 1024; async function pidFetchLinks() { return (await pidApiRequest("/links-ferramentas/")) || []; } async function pidCreateLink(formData) { return pidApiRequest("/links-ferramentas/", { method: "POST", body: formData }); } async function pidUpdateLinkOrdem(id, ordem) { return pidApiRequest(`/links-ferramentas/${id}/`, { method: "PATCH", body: { ordem } }); } async function pidUpdateLink(id, formData) { return pidApiRequest(`/links-ferramentas/${id}/`, { method: "PATCH", body: formData }); } async function pidDeleteLink(id) { return pidApiRequest(`/links-ferramentas/${id}/`, { method: "DELETE" }); } async function pidFetchLinkFavoritos() { const list = await pidApiRequest("/links-ferramentas-favoritos/"); return (list || []).map((f) => f.link); } async function pidAddLinkFavorito(linkId) { return pidApiRequest("/links-ferramentas-favoritos/", { method: "POST", body: { link: linkId } }); } async function pidRemoveLinkFavorito(linkId) { return pidApiRequest(`/links-ferramentas-favoritos/${linkId}/`, { method: "DELETE" }); } document.addEventListener("DOMContentLoaded", async () => { const grid = document.getElementById("lf-grid"); if (!grid) return; const me = await pidGetMe(); if (!me) return; const linkPerms = (me.permissoes_efetivas && me.permissoes_efetivas["links-ferramentas"]) || {}; const canView = !!(linkPerms.apps && linkPerms.apps["links-ferramentas-visualizar"]); const canManage = !!(linkPerms.apps && linkPerms.apps["links-ferramentas-editar"]); const noAccessEl = document.getElementById("lf-no-access"); const contentEl = document.getElementById("lf-content"); if (noAccessEl) noAccessEl.hidden = canView; if (contentEl) contentEl.hidden = !canView; if (!canView) return; const emptyEl = document.getElementById("lf-empty"); const addBtn = document.getElementById("lf-add-btn"); const modal = document.getElementById("lf-add-modal"); const formTitle = document.getElementById("lf-form-title"); const formNome = document.getElementById("lf-form-nome"); const formUrl = document.getElementById("lf-form-url"); const formIcone = document.getElementById("lf-form-icone"); const formIconeLabel = document.getElementById("lf-form-icone-label"); const formError = document.getElementById("lf-form-error"); const saveBtn = document.getElementById("lf-form-save-btn"); const cancelBtn = document.getElementById("lf-form-cancel-btn"); if (addBtn) addBtn.hidden = !canManage; let rawLinks = []; let links = []; let favoritos = new Set(); let editingId = null; function sortFavoritesFirst(list) { const fav = []; const rest = []; list.forEach((link) => (favoritos.has(link.id) ? fav : rest).push(link)); return fav.concat(rest); } function cardHtml(link, index) { const iconHtml = link.icone ? `` : PID_LINK_DEFAULT_ICON; const isFavorite = favoritos.has(link.id); const favoriteBtn = ` `; const adminHtml = canManage ? ` ` : ""; return `
${favoriteBtn} ${adminHtml}
${iconHtml} ${link.nome}
`; } function render() { grid.querySelectorAll(".lf-card").forEach((el) => el.remove()); const cardsHtml = links.map((link, index) => cardHtml(link, index)).join(""); if (emptyEl) { emptyEl.insertAdjacentHTML("afterend", cardsHtml); emptyEl.hidden = links.length !== 0; } else { grid.insertAdjacentHTML("beforeend", cardsHtml); } } async function refresh() { try { const [fetchedLinks, favoriteIds] = await Promise.all([pidFetchLinks(), pidFetchLinkFavoritos()]); rawLinks = fetchedLinks; favoritos = new Set(favoriteIds); } catch (e) { rawLinks = []; favoritos = new Set(); } links = sortFavoritesFirst(rawLinks); render(); } function openModal(link) { editingId = link ? link.id : null; formNome.value = link ? link.nome : ""; formUrl.value = link ? link.url : ""; formIcone.value = ""; formError.textContent = ""; formTitle.textContent = link ? "Editar Link" : "Adicionar Link"; formIconeLabel.textContent = link ? "Ícone (deixe em branco para manter o atual, até 2MB)" : "Ícone (opcional, até 2MB)"; saveBtn.textContent = link ? "Salvar" : "Adicionar"; modal.hidden = false; formNome.focus(); } function closeModal() { modal.hidden = true; editingId = null; } async function swapOrdem(a, b) { await Promise.all([pidUpdateLinkOrdem(a.id, b.ordem), pidUpdateLinkOrdem(b.id, a.ordem)]); await refresh(); } if (addBtn) { addBtn.addEventListener("click", () => openModal()); } if (formIcone) { formIcone.addEventListener("change", () => { const file = formIcone.files[0]; if (file && file.size > PID_LINK_ICON_MAX_BYTES) { formError.textContent = "O ícone deve ter no máximo 2MB."; formIcone.value = ""; } else { formError.textContent = ""; } }); } if (cancelBtn) cancelBtn.addEventListener("click", closeModal); if (modal) { modal.addEventListener("click", (event) => { if (event.target === modal) closeModal(); }); } if (saveBtn) { saveBtn.addEventListener("click", async () => { const nome = formNome.value.trim(); const url = formUrl.value.trim(); if (!nome || !url) { formError.textContent = "Informe o nome e o link."; return; } if (formIcone.files[0] && formIcone.files[0].size > PID_LINK_ICON_MAX_BYTES) { formError.textContent = "O ícone deve ter no máximo 2MB."; return; } formError.textContent = ""; const formData = new FormData(); formData.append("nome", nome); formData.append("url", url); if (formIcone.files[0]) formData.append("icone", formIcone.files[0]); try { if (editingId) { await pidUpdateLink(editingId, formData); } else { await pidCreateLink(formData); } closeModal(); await refresh(); } catch (e) { formError.textContent = e.message; } }); } grid.addEventListener("click", async (event) => { const favoriteBtn = event.target.closest("[data-link-favorite]"); if (favoriteBtn) { const id = Number(favoriteBtn.getAttribute("data-link-favorite")); try { if (favoritos.has(id)) { await pidRemoveLinkFavorito(id); favoritos.delete(id); } else { await pidAddLinkFavorito(id); favoritos.add(id); } links = sortFavoritesFirst(rawLinks); render(); } catch (e) { await pidAlert(e.message); } return; } const editBtn = event.target.closest("[data-link-edit]"); if (editBtn) { const id = Number(editBtn.getAttribute("data-link-edit")); const link = links.find((l) => l.id === id); if (link) openModal(link); return; } const removeBtn = event.target.closest("[data-link-remove]"); if (removeBtn) { const id = Number(removeBtn.getAttribute("data-link-remove")); const link = links.find((l) => l.id === id); if (link && (await pidConfirm(`Remover o link "${link.nome}"?`, { perigoso: true }))) { await pidDeleteLink(id); await refresh(); } return; } const upBtn = event.target.closest("[data-link-move-up]"); if (upBtn) { const id = Number(upBtn.getAttribute("data-link-move-up")); const index = links.findIndex((l) => l.id === id); if (index > 0) await swapOrdem(links[index], links[index - 1]); return; } const downBtn = event.target.closest("[data-link-move-down]"); if (downBtn) { const id = Number(downBtn.getAttribute("data-link-move-down")); const index = links.findIndex((l) => l.id === id); if (index !== -1 && index < links.length - 1) await swapOrdem(links[index], links[index + 1]); } }); let draggedId = null; let dropTargetId = null; let dropBefore = true; function clearDropHighlight() { grid.querySelectorAll(".lf-card--drop-before, .lf-card--drop-after").forEach((el) => { el.classList.remove("lf-card--drop-before", "lf-card--drop-after"); }); } grid.addEventListener("dragstart", (event) => { const card = event.target.closest(".lf-card--draggable"); if (!card) return; draggedId = Number(card.dataset.linkId); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", String(draggedId)); requestAnimationFrame(() => card.classList.add("lf-card--dragging")); }); grid.addEventListener("dragover", (event) => { if (draggedId === null) return; const card = event.target.closest(".lf-card--draggable"); if (!card || Number(card.dataset.linkId) === draggedId) return; event.preventDefault(); const rect = card.getBoundingClientRect(); dropBefore = event.clientX < rect.left + rect.width / 2; dropTargetId = Number(card.dataset.linkId); clearDropHighlight(); card.classList.add(dropBefore ? "lf-card--drop-before" : "lf-card--drop-after"); }); grid.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 fromIndex = links.findIndex((l) => l.id === fromId); if (fromIndex === -1) return; const reordered = links.slice(); const [moved] = reordered.splice(fromIndex, 1); let insertIndex = reordered.findIndex((l) => l.id === toId); if (insertIndex === -1) return; if (!dropBefore) insertIndex += 1; reordered.splice(insertIndex, 0, moved); const updates = reordered .map((link, index) => ({ link, index })) .filter(({ link, index }) => link.ordem !== index); if (!updates.length) return; links = reordered; render(); await Promise.all(updates.map(({ link, index }) => pidUpdateLinkOrdem(link.id, index))); await refresh(); }); grid.addEventListener("dragend", () => { draggedId = null; dropTargetId = null; clearDropHighlight(); grid.querySelectorAll(".lf-card--dragging").forEach((el) => el.classList.remove("lf-card--dragging")); }); await refresh(); });