diff --git a/src/composables/useNotifications.js b/src/composables/useNotifications.js index 06cf300..e49c8a9 100644 --- a/src/composables/useNotifications.js +++ b/src/composables/useNotifications.js @@ -23,14 +23,20 @@ import { useNotificationStore } from '@/stores/notificationStore'; // 24h em ms — na prática "sticky" (PrimeVue Toast não aceita Infinity). const STICKY_TOAST_LIFE_MS = 24 * 60 * 60 * 1000; +// Fallback polling — garante catch-up mesmo se Realtime perder eventos. +const POLLING_INTERVAL_MS = 60_000; + export function useNotifications() { const store = useNotificationStore(); const toast = useToast(); + const alertedIds = new Set(); // ids que já dispararam toast nesta sessão + let ownerId = null; + let pollTimer = null; - function onRealtimeNotification(item) { - // Toast aparece pra alertas de sistema (heartbeat, infra, etc). - // Inbound/agendas têm seus próprios notifiers visuais. + function showToastFor(item) { if (item?.type !== 'system_alert') return; + if (alertedIds.has(item.id)) return; + alertedIds.add(item.id); const payload = item.payload || {}; toast.add({ severity: 'error', @@ -41,16 +47,53 @@ export function useNotifications() { }); } + function onRealtimeNotification(item) { + showToastFor(item); + } + + // Re-carrega notifs do DB e dispara toast pras system_alert não-lidas + // que ainda não foram vistas nesta sessão. Usado no mount, no visibilitychange + // e no polling de fallback pro caso de Realtime perder eventos. + async function refreshAndMaybeAlert() { + if (!ownerId) return; + await store.load(ownerId); + for (const item of store.items || []) { + if (item.type !== 'system_alert') continue; + if (item.read_at || item.archived) continue; + showToastFor(item); + } + } + + function onVisibilityChange() { + if (document.visibilityState === 'visible') { + refreshAndMaybeAlert(); + } + } + onMounted(async () => { const { data, error } = await supabase.auth.getUser(); if (error || !data?.user?.id) return; - const ownerId = data.user.id; + ownerId = data.user.id; await store.load(ownerId); + + // Seed do set: notifs system_alert já lidas/vistas não redisparam toast + for (const item of store.items || []) { + if (item.type === 'system_alert' && (item.read_at || item.archived)) { + alertedIds.add(item.id); + } + } + store.subscribeRealtime(ownerId, onRealtimeNotification); + + document.addEventListener('visibilitychange', onVisibilityChange); + pollTimer = setInterval(refreshAndMaybeAlert, POLLING_INTERVAL_MS); }); onUnmounted(() => { + document.removeEventListener('visibilitychange', onVisibilityChange); + if (pollTimer) clearInterval(pollTimer); + pollTimer = null; store.unsubscribe(); });