From 36fbc02e9fe4d0964264a5d839bcceacccbd2556 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 23 Apr 2026 11:43:29 -0300 Subject: [PATCH] Browser notification: click leva pro destino real (drawer ou rota) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: onclick da Notification do browser (nativa do Chrome/Windows) fazia window.location.pathname = payload.deeplink direto, sem resolver alias semântico e sem abrir o drawer em alertas com thread_key. Como praticamente todos os nossos alertas do SLA vêm com deeplink '/conversas' (alias), o click na notificação do Chrome caía em NotFound. Fix: - fireBrowserNotification agora aceita um callback onClick e é exportada. - Removido o fireBrowserNotification hardcoded do subscribeRealtime do store (passa a ser responsabilidade do composable useNotifications). - useNotifications.onRealtimeNotification dispara toast + browser notif passando handleNotificationAction como handler. - handleNotificationAction: se tem thread_key → abre ConversationDrawer global direto na thread; senão resolve alias e router.push. Mesma lógica que já existe no toast e no clique do NotificationItem do sino. Agora os 3 pontos de click (toast, sininho, notificação nativa do OS) convergem pro mesmo comportamento. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/composables/useNotifications.js | 57 ++++++++++++++++++++++++++++- src/stores/notificationStore.js | 11 +++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/composables/useNotifications.js b/src/composables/useNotifications.js index da6233d..b043b16 100644 --- a/src/composables/useNotifications.js +++ b/src/composables/useNotifications.js @@ -15,9 +15,12 @@ |-------------------------------------------------------------------------- */ import { onMounted, onUnmounted } from 'vue'; +import { useRouter } from 'vue-router'; import { useToast } from 'primevue/usetoast'; import { supabase } from '@/lib/supabase/client'; -import { useNotificationStore } from '@/stores/notificationStore'; +import { useNotificationStore, fireBrowserNotification } from '@/stores/notificationStore'; +import { useConversationDrawerStore } from '@/stores/conversationDrawerStore'; +import { useTenantStore } from '@/stores/tenantStore'; // 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). @@ -26,9 +29,25 @@ 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; +// Aliases semânticos do deeplink → rota real por role. Mesmo map do AppLayout. +const DEEPLINK_ALIASES = { + '/crm/conversas': { therapist: '/therapist/conversas', clinic_admin: '/admin/conversas' }, + '/conversas': { therapist: '/therapist/conversas', clinic_admin: '/admin/conversas' } +}; + +function resolveDeeplink(link, role) { + if (!link || typeof link !== 'string') return link; + const alias = DEEPLINK_ALIASES[link]; + if (!alias) return link; + return alias[role] || alias.therapist; +} + export function useNotifications() { const store = useNotificationStore(); const toast = useToast(); + const router = useRouter(); + const conversationDrawer = useConversationDrawerStore(); + const tenantStore = useTenantStore(); const alertedIds = new Set(); // ids que já dispararam toast nesta sessão let ownerId = null; let pollTimer = null; @@ -62,8 +81,44 @@ export function useNotifications() { }); } + // Ação principal ao clicar numa notificação (seja do toast, sino ou OS): + // 1. Se tem thread_key, abre o drawer global na thread + // 2. Senão, resolve alias e navega via router + async function handleNotificationAction(item) { + if (!item) return; + const payload = item.payload || {}; + + if (payload.thread_key) { + try { + const tenantId = tenantStore.activeTenantId; + const { data } = await supabase + .from('conversation_threads') + .select('*') + .eq('tenant_id', tenantId) + .eq('thread_key', payload.thread_key) + .maybeSingle(); + if (data) { + await conversationDrawer.openForThread(data); + return; + } + } catch { + // cai no fallback abaixo + } + } + + const role = tenantStore?.activeRole || 'therapist'; + const link = resolveDeeplink(payload.deeplink, role); + if (link) { + if (typeof link === 'string' && link.startsWith('/')) router.push(link); + else window.location.href = link; + } + } + function onRealtimeNotification(item) { showToastFor(item); + // Dispara a notificação nativa do browser com handler de click que + // aplica a mesma lógica (drawer / alias) do toast e do sininho. + fireBrowserNotification(item, handleNotificationAction); } // Re-carrega notifs do DB e dispara toast AGREGADO pras system_alert diff --git a/src/stores/notificationStore.js b/src/stores/notificationStore.js index 8639e87..ec19e83 100644 --- a/src/stores/notificationStore.js +++ b/src/stores/notificationStore.js @@ -38,7 +38,7 @@ function browserNotifEnabled() { } } -function fireBrowserNotification(item) { +export function fireBrowserNotification(item, onClick) { if (!browserNotifEnabled()) return; if (document.hasFocus() && document.visibilityState === 'visible') return; // não notifica se tab ativa try { @@ -52,9 +52,11 @@ function fireBrowserNotification(item) { }); n.onclick = () => { window.focus(); - if (item?.payload?.deeplink) { - window.location.hash = ''; - window.location.pathname = item.payload.deeplink; + if (typeof onClick === 'function') { + try { onClick(item); } catch { /* ignore */ } + } else if (item?.payload?.deeplink) { + // fallback: navegação direta se ninguém registrou handler + window.location.href = item.payload.deeplink; } n.close(); }; @@ -129,7 +131,6 @@ export const useNotificationStore = defineStore('notifications', { }, (payload) => { this.items.unshift(payload.new); - fireBrowserNotification(payload.new); if (typeof onInsert === 'function') { try { onInsert(payload.new); } catch { /* ignore */ } }