368 lines
15 KiB
JavaScript
368 lines
15 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>';
|
|
|
|
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 `<li class="notif-empty">${PID_NOTIF_ERRO_CARGA}</li>`;
|
|
}
|
|
|
|
function render() {
|
|
if (falhaDispensadas) {
|
|
list.innerHTML = avisoErroCargaHtml();
|
|
if (badge) badge.hidden = true;
|
|
if (clearAllBtn) clearAllBtn.disabled = true;
|
|
return;
|
|
}
|
|
const itensHtml = 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("")
|
|
: falhaEventos
|
|
? ""
|
|
: `<li class="notif-empty">Nenhuma notificação no momento.</li>`;
|
|
// Compromissos não carregaram: o que veio (ferramentas) aparece, mais o aviso.
|
|
list.innerHTML = itensHtml + (falhaEventos ? avisoErroCargaHtml() : "");
|
|
|
|
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();
|
|
if (dismissBtn.disabled) return;
|
|
const id = dismissBtn.getAttribute("data-notif-dismiss");
|
|
dismissBtn.disabled = true;
|
|
try {
|
|
await pidDismissNotification(id);
|
|
} catch (e) {
|
|
dismissBtn.disabled = false;
|
|
await pidAlert(e.message, { titulo: "Não foi possível remover a notificação" });
|
|
return;
|
|
}
|
|
dismissedIds.push(id);
|
|
notifications = notifications.filter((n) => n.id !== id);
|
|
render();
|
|
});
|
|
|
|
if (clearAllBtn) {
|
|
clearAllBtn.addEventListener("click", async () => {
|
|
if (clearAllBtn.disabled || !notifications.length) return;
|
|
const textoOriginal = clearAllBtn.textContent;
|
|
clearAllBtn.disabled = true;
|
|
clearAllBtn.textContent = "Processando…";
|
|
const alvo = notifications.slice();
|
|
// allSettled: o que foi dispensado com sucesso sai do sino mesmo que
|
|
// outra dispensa falhe; só as que falharam continuam na lista.
|
|
const resultados = await Promise.allSettled(alvo.map((n) => pidDismissNotification(n.id)));
|
|
const dispensadas = alvo.filter((n, idx) => resultados[idx].status === "fulfilled").map((n) => n.id);
|
|
dismissedIds.push(...dispensadas);
|
|
notifications = notifications.filter((n) => !dispensadas.includes(n.id));
|
|
clearAllBtn.disabled = false;
|
|
clearAllBtn.textContent = textoOriginal;
|
|
render();
|
|
const falha = resultados.find((r) => r.status === "rejected");
|
|
if (falha) {
|
|
await pidAlert(falha.reason && falha.reason.message ? falha.reason.message : PID_ERRO_GENERICO, {
|
|
titulo: "Não foi possível limpar todas as notificações",
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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;
|
|
if (falhaDispensadas) {
|
|
historyList.innerHTML = avisoErroCargaHtml();
|
|
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("")
|
|
: falhaEventos
|
|
? ""
|
|
: `<li class="notif-empty">Nenhuma notificação no histórico.</li>`;
|
|
if (falhaEventos) historyList.insertAdjacentHTML("beforeend", avisoErroCargaHtml());
|
|
}
|
|
|
|
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 || restoreBtn.disabled) return;
|
|
const id = restoreBtn.getAttribute("data-notif-restore");
|
|
restoreBtn.disabled = true;
|
|
try {
|
|
await pidRestoreNotification(id);
|
|
} catch (e) {
|
|
restoreBtn.disabled = false;
|
|
await pidAlert(e.message, { titulo: "Não foi possível restaurar a notificação" });
|
|
return;
|
|
}
|
|
dismissedIds = dismissedIds.filter((dismissedId) => dismissedId !== id);
|
|
notifications = [...eventNotifications, ...toolNotificationsAgora].filter((n) => !dismissedIds.includes(n.id));
|
|
renderHistory();
|
|
render();
|
|
});
|
|
}
|
|
|
|
render();
|
|
});
|