/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/composables/useNotifications.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ import { onMounted, onUnmounted } from 'vue'; import { useToast } from 'primevue/usetoast'; import { supabase } from '@/lib/supabase/client'; import { useNotificationStore } from '@/stores/notificationStore'; // Alertas de sistema ficam fixos em vermelho até o usuário fechar manualmente. // 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; // Label curto do botão do toast baseado no deeplink function defaultActionLabel(deeplink) { if (!deeplink) return null; if (deeplink.includes('creditos-whatsapp')) return 'Ir pra loja'; if (deeplink.includes('whatsapp-pessoal')) return 'Ver conexão'; if (deeplink.includes('whatsapp-oficial')) return 'Ver canal oficial'; return 'Abrir'; } 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', summary: payload.title || 'Alerta do sistema', detail: payload.detail || '', life: STICKY_TOAST_LIFE_MS, closable: true, group: 'system-alerts', data: { deeplink: payload.deeplink, actionLabel: payload.actionLabel || defaultActionLabel(payload.deeplink) } }); } function onRealtimeNotification(item) { showToastFor(item); } // Re-carrega notifs do DB e dispara toast AGREGADO pras system_alert // não-lidas ainda não vistas nesta sessão. Se tiver várias, mostra só // a mais recente com sufixo "(+N outros alertas)" no detail pra evitar // enxurrada de toasts ao voltar pra aba / recarregar. Eventos novos via // Realtime continuam aparecendo individualmente (showToastFor direto). async function refreshAndMaybeAlert() { if (!ownerId) return; await store.load(ownerId); const pending = (store.items || []) .filter((i) => i.type === 'system_alert' && !i.read_at && !i.archived && !alertedIds.has(i.id)); if (pending.length === 0) return; // Marca os demais como já "alertados" nesta sessão pra não redisparar // nos próximos ticks de polling/visibility — eles continuam no sininho. const newest = pending[0]; // store.items já vem ordenado por created_at DESC for (let i = 1; i < pending.length; i++) { alertedIds.add(pending[i].id); } if (pending.length > 1) { const extra = pending.length - 1; const suffix = ` • +${extra} outro${extra === 1 ? '' : 's'} alerta${extra === 1 ? '' : 's'} no sino`; const patched = { ...newest, payload: { ...(newest.payload || {}), detail: `${newest.payload?.detail || ''}${suffix}` } }; showToastFor(patched); } else { showToastFor(newest); } } function onVisibilityChange() { if (document.visibilityState === 'visible') { refreshAndMaybeAlert(); } } onMounted(async () => { const { data, error } = await supabase.auth.getUser(); if (error || !data?.user?.id) return; 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(); }); return store; }