/** `access` diz qual permissão o usuário precisa ter pra essa notificação de * "nova ferramenta" aparecer (sino e histórico) — mesmo critério que já * esconde o item correspondente no menu (ver `pidApplyAccessVisibility` em * access.js): `{type:"gerencia"}` espelha o gate de `gerencia_permissoes` * usado por Perfis de Acesso/Usuários, `{type:"module", module:"..."}` * espelha `permissoes_efetivas[module].enabled`. */ const PID_NEW_TOOLS_NOTIFICATIONS = [ { id: "tool-calendario-individual", dataIso: "2026-08-01", title: "CALENDÁRIO INDIVIDUAL", breadcrumb: "Menu > Calendário Individual", access: { type: "module", module: "calendario-individual" }, }, { id: "tool-perfis-acesso", dataIso: "2026-08-03", title: "PERFIS DE ACESSO", breadcrumb: "Menu > Administração > Perfis de Acesso", access: { type: "gerencia" }, }, { id: "tool-usuarios", dataIso: "2026-08-03", title: "USUÁRIOS", breadcrumb: "Menu > Administração > Usuários", access: { type: "gerencia" }, }, { id: "tool-widgets", dataIso: "2026-08-10", title: "WIDGETS NA TELA INICIAL", breadcrumb: "Menu > Principal > Widgets", access: { type: "module", module: "principal" }, }, ]; /** Notificação de "nova ferramenta" só conta como nova por 10 dias a partir * de `dataIso` — depois disso ela é dispensada automaticamente (ver uso em * DOMContentLoaded abaixo), passando a valer as mesmas regras de * histórico/restauração de qualquer outra notificação dispensada. */ const PID_NOTIF_TOOL_EXPIRA_DIAS = 10; function pidNotifToolElegivel(n, me) { if (!n.access) return true; if (n.access.type === "gerencia") return !!me.gerencia_permissoes; if (n.access.type === "module") { const perm = (me.permissoes_efetivas || {})[n.access.module]; return !!(perm && perm.enabled); } return true; } function pidNotifToolExpirada(dataIso) { const [ano, mes, dia] = dataIso.split("-").map(Number); const limite = new Date(ano, mes - 1, dia); limite.setDate(limite.getDate() + PID_NOTIF_TOOL_EXPIRA_DIAS); const hoje = new Date(); hoje.setHours(0, 0, 0, 0); return hoje > limite; } const PID_NOTIF_ICON_TOOL = ''; const PID_NOTIF_ICON_EVENT = ''; const PID_NOTIF_ICON_RESTORE = ''; function pidFormatNotifDate(iso) { const [, month, day] = iso.split("-"); return `${day}/${month}`; } async function pidFetchDismissedNotifIds() { const list = await pidApiRequest("/notificacoes-dispensadas/"); return (list || []).map((n) => n.notif_id); } async function pidDismissNotification(id) { await pidApiRequest("/notificacoes-dispensadas/", { method: "POST", body: { notif_id: id } }); } async function pidRestoreNotification(id) { await pidApiRequest(`/notificacoes-dispensadas/${id}/`, { method: "DELETE" }); } function pidSortEventsByDate(a, b) { const dateCompare = a.data.localeCompare(b.data); return dateCompare !== 0 ? dateCompare : (a.horario || "").localeCompare(b.horario || ""); } function pidMapEventToNotification(ev) { return { id: `event-${ev.id}`, type: "event", date: pidFormatNotifDate(ev.data), title: ev.titulo.toUpperCase(), breadcrumb: ev.sou_dono ? "Menu > Calendário Individual" : `Menu > Calendário Individual > Compartilhado por ${ev.dono_nome}`, }; } /** Só os compromissos "elegíveis" a notificação agora (lembrete configurado, já * atingido e não no passado) — a mesma regra do sino. O histórico (ver * `renderHistory` abaixo) reconstrói a partir de todos os eventos, sem essas * duas restrições, já que um item dispensado pode ter saído desse recorte. */ function pidEventosElegiveisAgora(events) { const now = new Date(); const todayIso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; return events .filter((ev) => ev.data >= todayIso && ev.notificar_em && new Date(ev.notificar_em) <= now) .sort(pidSortEventsByDate) .slice(0, 5) .map(pidMapEventToNotification); } document.addEventListener("DOMContentLoaded", async () => { const bell = document.getElementById("notif-bell"); const dropdown = document.getElementById("notif-dropdown"); const list = document.getElementById("notif-list"); const badge = document.getElementById("notif-badge"); const clearAllBtn = document.getElementById("notif-clear-all"); const historyBtn = document.getElementById("notif-history-btn"); const historyModal = document.getElementById("notif-history-modal"); const historyList = document.getElementById("notif-history-list"); const historyCloseBtn = document.getElementById("notif-history-close-btn"); if (!bell || !dropdown || !list) return; const me = await pidGetMe(); const events = me ? await pidFetchEvents() : []; const eventNotifications = pidEventosElegiveisAgora(events); const allEventNotifications = events.slice().sort(pidSortEventsByDate).map(pidMapEventToNotification); // Só entra na lista quem o usuário tem permissão de ver — uma notificação // de ferramenta cujo módulo/aplicação ele não acessa nunca aparece, nem no // sino nem no histórico. const toolNotifications = me ? PID_NEW_TOOLS_NOTIFICATIONS.filter((n) => pidNotifToolElegivel(n, me)).map((n) => ({ ...n, type: "tool", date: pidFormatNotifDate(n.dataIso), })) : []; let dismissedIds = me ? await pidFetchDismissedNotifIds() : []; // Notificação de ferramenta com mais de 10 dias vira dispensada sozinha, // sem esperar o usuário clicar no X — daqui em diante ela segue as mesmas // regras de qualquer outra notificação dispensada (sai do sino, entra no // histórico, pode ser restaurada). const expiradasAgora = toolNotifications.filter((n) => !dismissedIds.includes(n.id) && pidNotifToolExpirada(n.dataIso)); if (expiradasAgora.length) { await Promise.all(expiradasAgora.map((n) => pidDismissNotification(n.id))); dismissedIds.push(...expiradasAgora.map((n) => n.id)); } // "Agora" exclui as já expiradas por prazo (mesmo espírito de // `eventNotifications` acima, que também é um recorte mais restrito que a // lista completa) — restaurar uma notificação de ferramenta velha pelo // histórico não a traz de volta pro sino, só mantém o rastro dela lá. const toolNotificationsAgora = toolNotifications.filter((n) => !pidNotifToolExpirada(n.dataIso)); let notifications = [...eventNotifications, ...toolNotificationsAgora].filter((n) => !dismissedIds.includes(n.id)); function render() { list.innerHTML = notifications.length ? notifications .map( (n) => `
  • ${n.type === "event" ? PID_NOTIF_ICON_EVENT : PID_NOTIF_ICON_TOOL}

    ${n.date} - ${n.title}

    ${n.breadcrumb}

  • ` ) .join("") : `
  • Nenhuma notificação no momento.
  • `; if (badge) { badge.hidden = notifications.length === 0; badge.textContent = notifications.length; } } bell.addEventListener("click", (event) => { event.stopPropagation(); dropdown.hidden = !dropdown.hidden; }); list.addEventListener("click", async (event) => { const dismissBtn = event.target.closest("[data-notif-dismiss]"); if (!dismissBtn) return; event.stopPropagation(); const id = dismissBtn.getAttribute("data-notif-dismiss"); await pidDismissNotification(id); dismissedIds.push(id); notifications = notifications.filter((n) => n.id !== id); render(); }); if (clearAllBtn) { clearAllBtn.addEventListener("click", async () => { await Promise.all(notifications.map((n) => pidDismissNotification(n.id))); dismissedIds.push(...notifications.map((n) => n.id)); notifications = []; render(); }); } document.addEventListener("click", (event) => { if (!dropdown.hidden && !dropdown.contains(event.target) && event.target !== bell) { dropdown.hidden = true; } if (historyModal && !historyModal.hidden && event.target === historyModal) { historyModal.hidden = true; } }); function renderHistory() { if (!historyList) return; const historyItems = [...allEventNotifications, ...toolNotifications].filter((n) => dismissedIds.includes(n.id)); historyList.innerHTML = historyItems.length ? historyItems .map( (n) => `
  • ${n.type === "event" ? PID_NOTIF_ICON_EVENT : PID_NOTIF_ICON_TOOL}

    ${n.date} - ${n.title}

    ${n.breadcrumb}

  • ` ) .join("") : `
  • Nenhuma notificação no histórico.
  • `; } if (historyBtn && historyModal) { historyBtn.addEventListener("click", (event) => { event.stopPropagation(); dropdown.hidden = true; renderHistory(); historyModal.hidden = false; }); } if (historyCloseBtn) { historyCloseBtn.addEventListener("click", () => { historyModal.hidden = true; }); } if (historyList) { historyList.addEventListener("click", async (event) => { const restoreBtn = event.target.closest("[data-notif-restore]"); if (!restoreBtn) return; const id = restoreBtn.getAttribute("data-notif-restore"); await pidRestoreNotification(id); dismissedIds = dismissedIds.filter((dismissedId) => dismissedId !== id); notifications = [...eventNotifications, ...toolNotificationsAgora].filter((n) => !dismissedIds.includes(n.id)); renderHistory(); render(); }); } render(); });