Browser notification: click leva pro destino real (drawer ou rota)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -15,9 +15,12 @@
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
import { onMounted, onUnmounted } from 'vue';
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { supabase } from '@/lib/supabase/client';
|
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.
|
// 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).
|
// 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.
|
// Fallback polling — garante catch-up mesmo se Realtime perder eventos.
|
||||||
const POLLING_INTERVAL_MS = 60_000;
|
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() {
|
export function useNotifications() {
|
||||||
const store = useNotificationStore();
|
const store = useNotificationStore();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const conversationDrawer = useConversationDrawerStore();
|
||||||
|
const tenantStore = useTenantStore();
|
||||||
const alertedIds = new Set(); // ids que já dispararam toast nesta sessão
|
const alertedIds = new Set(); // ids que já dispararam toast nesta sessão
|
||||||
let ownerId = null;
|
let ownerId = null;
|
||||||
let pollTimer = 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) {
|
function onRealtimeNotification(item) {
|
||||||
showToastFor(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
|
// Re-carrega notifs do DB e dispara toast AGREGADO pras system_alert
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function browserNotifEnabled() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fireBrowserNotification(item) {
|
export function fireBrowserNotification(item, onClick) {
|
||||||
if (!browserNotifEnabled()) return;
|
if (!browserNotifEnabled()) return;
|
||||||
if (document.hasFocus() && document.visibilityState === 'visible') return; // não notifica se tab ativa
|
if (document.hasFocus() && document.visibilityState === 'visible') return; // não notifica se tab ativa
|
||||||
try {
|
try {
|
||||||
@@ -52,9 +52,11 @@ function fireBrowserNotification(item) {
|
|||||||
});
|
});
|
||||||
n.onclick = () => {
|
n.onclick = () => {
|
||||||
window.focus();
|
window.focus();
|
||||||
if (item?.payload?.deeplink) {
|
if (typeof onClick === 'function') {
|
||||||
window.location.hash = '';
|
try { onClick(item); } catch { /* ignore */ }
|
||||||
window.location.pathname = item.payload.deeplink;
|
} else if (item?.payload?.deeplink) {
|
||||||
|
// fallback: navegação direta se ninguém registrou handler
|
||||||
|
window.location.href = item.payload.deeplink;
|
||||||
}
|
}
|
||||||
n.close();
|
n.close();
|
||||||
};
|
};
|
||||||
@@ -129,7 +131,6 @@ export const useNotificationStore = defineStore('notifications', {
|
|||||||
},
|
},
|
||||||
(payload) => {
|
(payload) => {
|
||||||
this.items.unshift(payload.new);
|
this.items.unshift(payload.new);
|
||||||
fireBrowserNotification(payload.new);
|
|
||||||
if (typeof onInsert === 'function') {
|
if (typeof onInsert === 'function') {
|
||||||
try { onInsert(payload.new); } catch { /* ignore */ }
|
try { onInsert(payload.new); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user