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