const PID_STAR_ICON = ''; const PID_DEFAULT_APP_ICON = ''; async function pidFetchFavorites() { return (await pidApiRequest("/favoritos/")) || []; } async function pidToggleFavorite(appId, isFavorite) { if (isFavorite) { await pidApiRequest(`/favoritos/${encodeURIComponent(appId)}/`, { method: "DELETE" }); } else { await pidApiRequest("/favoritos/", { method: "POST", body: { app_id: appId } }); } } async function pidUpdateFavoriteOrdem(appId, ordem) { return pidApiRequest(`/favoritos/${encodeURIComponent(appId)}/`, { method: "PATCH", body: { ordem } }); } function pidSlug(text) { return text .toLowerCase() .normalize("NFD") .replace(/[̀-ͯ]/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); } function pidCollectFavoritableApps() { const apps = []; document.querySelectorAll(".sidebar a.nav-item, .sidebar a.nav-subitem").forEach((link) => { const li = link.closest("li"); if (!li || li.getAttribute("data-section") === "principal") return; const sectionLi = li.hasAttribute("data-section") ? li : li.closest("[data-section]"); if (!sectionLi) return; const labelEl = link.querySelector(".nav-item__label"); const label = labelEl ? labelEl.textContent.trim() : link.textContent.trim(); const id = li.hasAttribute("data-section") ? li.getAttribute("data-section") : `${sectionLi.getAttribute("data-section")}__${pidSlug(label)}`; // Sub-itens (a.nav-subitem) não têm ícone próprio no menu — usa o ícone // do módulo pai (o svg do próprio botão do nav-group, ex.: o lápis de // "Utilitários") em vez de cair no ícone genérico de grade. const iconNode = link.querySelector("svg") || sectionLi.querySelector(".nav-group__toggle svg") || null; apps.push({ id, label, link, iconNode, }); }); return apps; } function pidIconHtmlFor(app) { if (!app.iconNode) return PID_DEFAULT_APP_ICON; const clone = app.iconNode.cloneNode(true); clone.removeAttribute("width"); clone.removeAttribute("height"); clone.setAttribute("class", "app-card__icon"); return clone.outerHTML; } document.addEventListener("DOMContentLoaded", async () => { const me = await pidGetMe(); if (!me) return; const apps = pidCollectFavoritableApps(); const starButtons = []; apps.forEach((app) => { const row = document.createElement("div"); row.className = "nav-item-row"; app.link.replaceWith(row); row.appendChild(app.link); const starBtn = document.createElement("button"); starBtn.type = "button"; starBtn.className = "fav-toggle"; starBtn.innerHTML = PID_STAR_ICON; row.appendChild(starBtn); starButtons.push({ id: app.id, btn: starBtn }); starBtn.addEventListener("click", async () => { // Trava a própria estrela: clique duplo mandaria dois POST/DELETE. if (starBtn.disabled) return; const isFavorite = starBtn.classList.contains("is-favorite"); starBtn.disabled = true; try { await pidToggleFavorite(app.id, isFavorite); await refreshAll(); } catch (e) { await pidAlert(e.message, { titulo: "Não foi possível atualizar os favoritos" }); } finally { starBtn.disabled = false; } }); }); const grid = document.getElementById("app-card-grid"); const favoritesEmpty = document.getElementById("favorites-empty"); let favorites = []; let favIds = []; function syncStarButtons() { starButtons.forEach(({ id, btn }) => { const active = favIds.includes(id); btn.classList.toggle("is-favorite", active); btn.setAttribute("aria-pressed", String(active)); btn.setAttribute("aria-label", active ? "Remover dos favoritos" : "Adicionar aos favoritos"); }); } function renderDashboard() { if (!grid) return; const favApps = favIds.map((id) => apps.find((a) => a.id === id)).filter(Boolean); grid.querySelectorAll(".app-card").forEach((el) => el.remove()); const cardsHtml = favApps .map( (app) => `
${pidIconHtmlFor(app)} ${pidEscapeHtml(app.label)}
` ) .join(""); if (favoritesEmpty) { favoritesEmpty.insertAdjacentHTML("beforebegin", cardsHtml); favoritesEmpty.hidden = favApps.length !== 0; } else { grid.insertAdjacentHTML("beforeend", cardsHtml); } } const textoFavoritosVazio = favoritesEmpty ? favoritesEmpty.textContent : ""; async function refreshAll() { favorites = await pidFetchFavorites(); // Se a carga inicial falhou, o aviso de erro ocupou este parágrafo: devolve o texto normal. if (favoritesEmpty) favoritesEmpty.textContent = textoFavoritosVazio; favIds = favorites.map((f) => f.app_id); syncStarButtons(); renderDashboard(); } // Dispara o clique no próprio original do sidebar (ainda no DOM, só // movido pra dentro do .nav-item-row) em vez de reimplementar a navegação // a partir de href/target — assim um app favoritado herda automaticamente // qualquer comportamento especial do item (ex.: "Calendário De Paula", que // não navega mais, abre um modal com iframe via listener em sidebar.js). function openApp(app) { app.link.click(); } if (grid) { grid.addEventListener("click", async (event) => { const removeBtn = event.target.closest("[data-fav-remove]"); if (removeBtn) { if (removeBtn.disabled) return; const appId = removeBtn.getAttribute("data-fav-remove"); const app = apps.find((a) => a.id === appId); const confirmado = await pidConfirm( `Remover "${app ? app.label : "esta aplicação"}" dos favoritos?`, { titulo: "Remover favorito", textoConfirmar: "Remover" } ); if (!confirmado) return; removeBtn.disabled = true; try { await pidToggleFavorite(appId, true); await refreshAll(); } catch (e) { removeBtn.disabled = false; await pidAlert(e.message, { titulo: "Não foi possível remover o favorito" }); } return; } const card = event.target.closest(".app-card"); if (!card) return; const app = apps.find((a) => a.id === card.getAttribute("data-fav-id")); if (app) openApp(app); }); grid.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; if (event.target.closest("[data-fav-remove]")) return; const card = event.target.closest(".app-card"); if (!card) return; event.preventDefault(); const app = apps.find((a) => a.id === card.getAttribute("data-fav-id")); if (app) openApp(app); }); let draggedAppId = null; let dropTargetAppId = null; let dropBefore = true; function clearDropHighlight() { grid.querySelectorAll(".app-card--drop-before, .app-card--drop-after").forEach((el) => { el.classList.remove("app-card--drop-before", "app-card--drop-after"); }); } grid.addEventListener("dragstart", (event) => { const card = event.target.closest(".app-card"); if (!card) return; draggedAppId = card.getAttribute("data-fav-id"); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", draggedAppId); requestAnimationFrame(() => card.classList.add("app-card--dragging")); }); grid.addEventListener("dragover", (event) => { if (draggedAppId === null) return; const card = event.target.closest(".app-card"); if (!card || card.getAttribute("data-fav-id") === draggedAppId) return; event.preventDefault(); const rect = card.getBoundingClientRect(); dropBefore = event.clientX < rect.left + rect.width / 2; dropTargetAppId = card.getAttribute("data-fav-id"); clearDropHighlight(); card.classList.add(dropBefore ? "app-card--drop-before" : "app-card--drop-after"); }); grid.addEventListener("drop", async (event) => { if (draggedAppId === null) return; event.preventDefault(); clearDropHighlight(); const fromId = draggedAppId; const toId = dropTargetAppId; draggedAppId = null; dropTargetAppId = null; if (toId === null || fromId === toId) return; const fromIndex = favorites.findIndex((f) => f.app_id === fromId); if (fromIndex === -1) return; const reordered = favorites.slice(); const [moved] = reordered.splice(fromIndex, 1); let insertIndex = reordered.findIndex((f) => f.app_id === toId); if (insertIndex === -1) return; if (!dropBefore) insertIndex += 1; reordered.splice(insertIndex, 0, moved); const updates = reordered .map((favorito, index) => ({ favorito, index })) .filter(({ favorito, index }) => favorito.ordem !== index); if (!updates.length) return; favorites = reordered; favIds = favorites.map((f) => f.app_id); renderDashboard(); try { await Promise.all(updates.map(({ favorito, index }) => pidUpdateFavoriteOrdem(favorito.app_id, index))); } catch (e) { await pidAlert(e.message, { titulo: "Não foi possível salvar a nova ordem" }); } // Recarrega sempre do servidor: se algum PATCH falhou, a tela volta à // ordem realmente gravada em vez de mostrar uma ordem que não persistiu. try { await refreshAll(); } catch (e) { await pidAlert(e.message, { titulo: "Não foi possível recarregar os favoritos" }); } }); grid.addEventListener("dragend", () => { draggedAppId = null; dropTargetAppId = null; clearDropHighlight(); grid.querySelectorAll(".app-card--dragging").forEach((el) => el.classList.remove("app-card--dragging")); }); } try { await refreshAll(); } catch (e) { // Erro de carga não pode virar "você ainda não tem favoritos". if (favoritesEmpty) { favoritesEmpty.textContent = "Não foi possível carregar seus favoritos. Recarregue a página; se persistir, contate a Inovação."; favoritesEmpty.hidden = false; } } });