/** `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 = ''; const PID_NOTIF_DATA_FORMATO = new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit" }); /** `iso` é data pura ("AAAA-MM-DD"): monta a Date pelas partes, no fuso local. * `new Date("AAAA-MM-DD")` seria meia-noite UTC e, no Brasil, cairia no dia anterior. */ function pidFormatNotifDate(iso) { const [ano, mes, dia] = iso.split("-").map(Number); return PID_NOTIF_DATA_FORMATO.format(new Date(ano, mes - 1, dia)); } 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 || ""); } /** `date`/`title`/`breadcrumb` já saem daqui escapados (dado da API) — são * interpolados direto no innerHTML de `render`/`renderHistory`, sem escapar de * novo (o breadcrumb tem `>` fixo de propósito). */ function pidMapEventToNotification(ev) { return { id: `event-${ev.id}`, type: "event", date: pidEscapeHtml(pidFormatNotifDate(ev.data)), title: pidEscapeHtml(ev.titulo.toUpperCase()), breadcrumb: ev.sou_dono ? "Menu > Calendário Individual" : `Menu > Calendário Individual > Compartilhado por ${pidEscapeHtml(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(); // Cada carga é tratada à parte: uma falha não pode impedir o sino de abrir // nem esconder o que carregou certo. A falha vira aviso dentro da lista // (nunca "Nenhuma notificação", que daria a entender que não há nada). const PID_NOTIF_ERRO_CARGA = "Não foi possível carregar. Recarregue a página; se persistir, contate a Inovação."; let falhaEventos = false; let falhaDispensadas = false; let events = []; if (me) { try { events = (await pidFetchEvents()) || []; } catch (e) { falhaEventos = true; } } 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 = []; if (me) { try { dismissedIds = await pidFetchDismissedNotifIds(); } catch (e) { falhaDispensadas = true; } } // 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). Sem a lista de dispensadas não dá pra // saber quais já foram gravadas, então pula. Falha aqui é silenciosa de // propósito: a notificação já não aparece no sino (ver toolNotificationsAgora), // só deixa de ir pro histórico até a próxima tentativa. const expiradasAgora = falhaDispensadas ? [] : toolNotifications.filter((n) => !dismissedIds.includes(n.id) && pidNotifToolExpirada(n.dataIso)); if (expiradasAgora.length) { const resultados = await Promise.allSettled(expiradasAgora.map((n) => pidDismissNotification(n.id))); expiradasAgora.forEach((n, idx) => { if (resultados[idx].status === "fulfilled") dismissedIds.push(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)); // Sem saber o que já foi dispensado, não mostra nada no sino (mostraria de // volta tudo que o usuário já tinha limpado). let notifications = falhaDispensadas ? [] : [...eventNotifications, ...toolNotificationsAgora].filter((n) => !dismissedIds.includes(n.id)); function avisoErroCargaHtml() { return `
${n.date} - ${n.title}
${n.date} - ${n.title}