const PID_WIDGET_TYPES = { "calendario-individual": { label: "Calendário Individual", description: "Próximos compromissos da sua agenda pessoal.", href: "calendario-individual.html", linkLabel: "Ver calendário completo", }, "links-favoritos": { label: "Links Favoritos", description: "Seus atalhos favoritos de Links & Ferramentas.", href: "links-ferramentas.html", linkLabel: "Ver todos os links", visibleIf: (me) => !!(me.permissoes_efetivas?.["links-ferramentas"]?.apps?.["links-ferramentas-visualizar"]), }, }; const PID_WIDGET_LINK_DEFAULT_ICON = ''; const PID_WIDGET_DEFAULT_WIDTH = 320; const PID_WIDGET_DEFAULT_HEIGHT = 240; async function pidFetchWidgets() { return (await pidApiRequest("/widgets/")) || []; } async function pidAddWidget(tipo) { return pidApiRequest("/widgets/", { method: "POST", body: { tipo } }); } async function pidRemoveWidget(tipo) { await pidApiRequest(`/widgets/${encodeURIComponent(tipo)}/`, { method: "DELETE" }); } async function pidUpdateWidget(tipo, payload) { return pidApiRequest(`/widgets/${encodeURIComponent(tipo)}/`, { method: "PATCH", body: payload }); } document.addEventListener("DOMContentLoaded", async () => { const widgetsGrid = document.getElementById("widgets-grid"); if (!widgetsGrid) return; const me = await pidGetMe(); if (!me) return; const widgetsEmpty = document.getElementById("widgets-empty"); const addBtn = document.getElementById("widget-add-btn"); const pickerModal = document.getElementById("widget-picker-modal"); const pickerList = document.getElementById("widget-picker-list"); function todayIso() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function renderCalendarWidgetBody(events) { const today = todayIso(); const upcoming = events .filter((ev) => ev.data >= today) .sort((a, b) => { const dateCompare = a.data.localeCompare(b.data); return dateCompare !== 0 ? dateCompare : (a.horario || "").localeCompare(b.horario || ""); }) .slice(0, 5); if (!upcoming.length) { return `

Nenhum compromisso agendado.

`; } return upcoming .map((ev) => { const [, month, day] = ev.data.split("-"); const horario = ev.horario ? ev.horario.slice(0, 5) : ""; const dateLabel = horario ? `${day}/${month} ${horario}` : `${day}/${month}`; return `
${dateLabel} ${ev.titulo} ${!ev.sou_dono ? `— ${ev.dono_nome}` : ""}
`; }) .join(""); } function renderLinksFavoritosWidgetBody(links, favoriteIds) { const favoriteSet = new Set(favoriteIds); const favoritos = links.filter((link) => favoriteSet.has(link.id)); if (!favoritos.length) { return `

Nenhum link favoritado ainda.

`; } return ` `; } async function widgetBodyFor(typeKey) { if (typeKey === "calendario-individual") return renderCalendarWidgetBody(await pidFetchEvents()); if (typeKey === "links-favoritos") { const [links, favoriteIds] = await Promise.all([pidFetchLinks(), pidFetchLinkFavoritos()]); return renderLinksFavoritosWidgetBody(links, favoriteIds); } return ""; } let widgets = []; let widgetBodyCache = {}; let resizeObserver = null; const resizeSaveTimers = new Map(); function widgetCardHtml(widget) { const type = PID_WIDGET_TYPES[widget.tipo]; const width = widget.largura || PID_WIDGET_DEFAULT_WIDTH; const height = widget.altura || PID_WIDGET_DEFAULT_HEIGHT; const body = widgetBodyCache[widget.tipo] || ""; return `

${type.label}

${body}
${type.linkLabel} →
`; } function observeWidgetSizes() { if (resizeObserver) resizeObserver.disconnect(); resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const card = entry.target; const tipo = card.getAttribute("data-widget-id"); const width = Math.round(card.offsetWidth); const height = Math.round(card.offsetHeight); const widget = widgets.find((w) => w.tipo === tipo); if (!widget || (widget.largura === width && widget.altura === height)) continue; widget.largura = width; widget.altura = height; clearTimeout(resizeSaveTimers.get(tipo)); resizeSaveTimers.set( tipo, setTimeout(() => { pidUpdateWidget(tipo, { largura: width, altura: height }).catch(() => {}); }, 500) ); } }); widgetsGrid.querySelectorAll(".widget-card").forEach((card) => resizeObserver.observe(card)); } function renderWidgetsDom() { widgetsGrid.querySelectorAll(".widget-card").forEach((el) => el.remove()); const cardsHtml = widgets.map((widget) => widgetCardHtml(widget)).join(""); if (widgetsEmpty) { widgetsEmpty.insertAdjacentHTML("beforebegin", cardsHtml); widgetsEmpty.hidden = widgets.length !== 0; } else { widgetsGrid.insertAdjacentHTML("beforeend", cardsHtml); } observeWidgetSizes(); } async function renderWidgets() { widgets = (await pidFetchWidgets()).filter((w) => { const type = PID_WIDGET_TYPES[w.tipo]; return type && (!type.visibleIf || type.visibleIf(me)); }); const bodies = await Promise.all(widgets.map((w) => widgetBodyFor(w.tipo))); widgetBodyCache = {}; widgets.forEach((w, idx) => { widgetBodyCache[w.tipo] = bodies[idx]; }); renderWidgetsDom(); } async function renderPicker() { const added = widgets.map((w) => w.tipo); const available = Object.keys(PID_WIDGET_TYPES).filter((key) => { if (added.includes(key)) return false; const type = PID_WIDGET_TYPES[key]; return !type.visibleIf || type.visibleIf(me); }); pickerList.innerHTML = available.length ? available .map( (key) => ` ` ) .join("") : `

Você já adicionou todos os widgets disponíveis.

`; } addBtn.addEventListener("click", async () => { await renderPicker(); pickerModal.hidden = false; }); pickerModal.addEventListener("click", async (event) => { if (event.target === pickerModal) { pickerModal.hidden = true; return; } const choice = event.target.closest("[data-widget-choice]"); if (!choice) return; const key = choice.getAttribute("data-widget-choice"); await pidAddWidget(key); pickerModal.hidden = true; await renderWidgets(); }); widgetsGrid.addEventListener("click", async (event) => { const removeBtn = event.target.closest("[data-widget-remove]"); if (!removeBtn) return; await pidRemoveWidget(removeBtn.getAttribute("data-widget-remove")); await renderWidgets(); }); let draggedTipo = null; let dropTargetTipo = null; let dropBefore = true; function clearDropHighlight() { widgetsGrid.querySelectorAll(".widget-card--drop-before, .widget-card--drop-after").forEach((el) => { el.classList.remove("widget-card--drop-before", "widget-card--drop-after"); }); } widgetsGrid.addEventListener("dragstart", (event) => { const header = event.target.closest(".widget-card__header"); const card = header && header.closest(".widget-card"); if (!card) return; draggedTipo = card.getAttribute("data-widget-id"); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", draggedTipo); requestAnimationFrame(() => card.classList.add("widget-card--dragging")); }); widgetsGrid.addEventListener("dragover", (event) => { if (draggedTipo === null) return; const card = event.target.closest(".widget-card"); if (!card || card.getAttribute("data-widget-id") === draggedTipo) return; event.preventDefault(); const rect = card.getBoundingClientRect(); dropBefore = event.clientX < rect.left + rect.width / 2; dropTargetTipo = card.getAttribute("data-widget-id"); clearDropHighlight(); card.classList.add(dropBefore ? "widget-card--drop-before" : "widget-card--drop-after"); }); widgetsGrid.addEventListener("drop", async (event) => { if (draggedTipo === null) return; event.preventDefault(); clearDropHighlight(); const fromTipo = draggedTipo; const toTipo = dropTargetTipo; draggedTipo = null; dropTargetTipo = null; if (toTipo === null || fromTipo === toTipo) return; const fromIndex = widgets.findIndex((w) => w.tipo === fromTipo); if (fromIndex === -1) return; const reordered = widgets.slice(); const [moved] = reordered.splice(fromIndex, 1); let insertIndex = reordered.findIndex((w) => w.tipo === toTipo); if (insertIndex === -1) return; if (!dropBefore) insertIndex += 1; reordered.splice(insertIndex, 0, moved); const updates = reordered .map((widget, index) => ({ widget, index })) .filter(({ widget, index }) => widget.ordem !== index); if (!updates.length) return; widgets = reordered; renderWidgetsDom(); await Promise.all(updates.map(({ widget, index }) => pidUpdateWidget(widget.tipo, { ordem: index }))); await renderWidgets(); }); widgetsGrid.addEventListener("dragend", () => { draggedTipo = null; dropTargetTipo = null; clearDropHighlight(); widgetsGrid .querySelectorAll(".widget-card--dragging") .forEach((el) => el.classList.remove("widget-card--dragging")); }); await renderWidgets(); });