portal_publico/static/js/notifications.js

277 lines
11 KiB
JavaScript

/** `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 =
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z" stroke-linejoin="round"/></svg>';
const PID_NOTIF_ICON_EVENT =
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18M8 3v4M16 3v4" stroke-linecap="round"/><path d="M9 15l2 2 4-4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
const PID_NOTIF_ICON_RESTORE =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 3-6.7L3 8" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 3v5h5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
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 &gt; Calendário Individual"
: `Menu &gt; Calendário Individual &gt; 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) => `
<li class="notif-item notif-item--${n.type}" data-notif-id="${n.id}">
<span class="notif-item__icon">${n.type === "event" ? PID_NOTIF_ICON_EVENT : PID_NOTIF_ICON_TOOL}</span>
<div class="notif-item__body">
<p class="notif-item__title">${n.date} - ${n.title}</p>
<p class="notif-item__breadcrumb">${n.breadcrumb}</p>
</div>
<button type="button" class="notif-item__dismiss" data-notif-dismiss="${n.id}" aria-label="Remover notificação">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
</li>`
)
.join("")
: `<li class="notif-empty">Nenhuma notificação no momento.</li>`;
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) => `
<li class="notif-item notif-item--${n.type}" data-notif-id="${n.id}">
<span class="notif-item__icon">${n.type === "event" ? PID_NOTIF_ICON_EVENT : PID_NOTIF_ICON_TOOL}</span>
<div class="notif-item__body">
<p class="notif-item__title">${n.date} - ${n.title}</p>
<p class="notif-item__breadcrumb">${n.breadcrumb}</p>
</div>
<button type="button" class="notif-item__restore" data-notif-restore="${n.id}" aria-label="Restaurar notificação" title="Restaurar">
${PID_NOTIF_ICON_RESTORE}
</button>
</li>`
)
.join("")
: `<li class="notif-empty">Nenhuma notificação no histórico.</li>`;
}
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();
});