From 64e76343fcc45995760bc0669748b3676b81f931 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 23 Apr 2026 11:38:22 -0300 Subject: [PATCH] =?UTF-8?q?NotificationItem:=20resolve=20alias=20+=20bot?= =?UTF-8?q?=C3=B5es=20inline=20"Conversa"/"Abrir"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 melhorias no item de notificação do sininho: 1. handleRowClick: agora resolve alias (/conversas → /therapist|admin/ conversas) baseado em tenantStore.activeRole. Antes caía em NotFound quando o deeplink era /conversas ou /crm/conversas. 2. Se payload tem thread_key (alertas do SLA), o clique abre o drawer global diretamente na thread em vez de navegar — experiência similar à do botão do toast. Fallback pra deeplink se a thread sumiu. 3. typeMap ganha entrada 'system_alert' (ícone pi-exclamation-circle, borda vermelha). 4. Botões inline "Conversa" e "Abrir" aparecem embaixo do detail quando o payload tem thread_key ou deeplink — atalhos pras ações mais comuns sem precisar clicar na área do item. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../notifications/NotificationItem.vue | 132 +++++++++++++++++- 1 file changed, 126 insertions(+), 6 deletions(-) diff --git a/src/components/notifications/NotificationItem.vue b/src/components/notifications/NotificationItem.vue index 8db1014..f8e6301 100644 --- a/src/components/notifications/NotificationItem.vue +++ b/src/components/notifications/NotificationItem.vue @@ -39,9 +39,24 @@ const typeMap = { new_patient: { icon: 'pi-user-plus', border: 'border-sky-500' }, recurrence_alert: { icon: 'pi-refresh', border: 'border-amber-500' }, session_status: { icon: 'pi-calendar-times', border: 'border-orange-500' }, - inbound_message: { icon: 'pi-whatsapp', border: 'border-emerald-500' } + inbound_message: { icon: 'pi-whatsapp', border: 'border-emerald-500' }, + system_alert: { icon: 'pi-exclamation-circle', border: 'border-red-600' } }; +// 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) { + if (!link || typeof link !== 'string') return link; + const alias = DEEPLINK_ALIASES[link]; + if (!alias) return link; + const role = tenantStore?.activeRole || 'therapist'; + return alias[role] || alias.therapist; +} + const meta = computed(() => typeMap[props.item.type] || { icon: 'pi-bell', border: 'border-gray-300' }); const isUnread = computed(() => !props.item.read_at); @@ -49,10 +64,30 @@ const timeAgo = computed(() => formatDistanceToNow(new Date(props.item.created_a const initials = computed(() => props.item.payload?.avatar_initials || '?'); -function handleRowClick() { +async function openConversationByThreadKey(threadKey) { + if (!threadKey) return false; + try { + const tenantId = tenantStore.activeTenantId; + const { supabase } = await import('@/lib/supabase/client'); + const { data } = await supabase + .from('conversation_threads') + .select('*') + .eq('tenant_id', tenantId) + .eq('thread_key', threadKey) + .maybeSingle(); + if (!data) return false; + await conversationDrawer.openForThread(data); + return true; + } catch { + return false; + } +} + +async function handleRowClick() { + const payload = props.item.payload || {}; + // Inbound message → abre drawer global (evita 403 em rota admin/therapist) if (props.item.type === 'inbound_message') { - const payload = props.item.payload || {}; if (payload.patient_id) { conversationDrawer.openForPatient(payload.patient_id); } else if (payload.from_number) { @@ -74,8 +109,39 @@ function handleRowClick() { return; } - // Outros tipos: segue o deeplink normal - const deeplink = props.item.payload?.deeplink; + // System alert com thread_key → abre drawer da conversa + if (payload.thread_key) { + const ok = await openConversationByThreadKey(payload.thread_key); + if (ok) { + store.drawerOpen = false; + emit('read', props.item.id); + return; + } + } + + // Fallback: segue deeplink resolvido por alias + const deeplink = resolveDeeplink(payload.deeplink); + if (deeplink) { + router.push(deeplink); + store.drawerOpen = false; + emit('read', props.item.id); + } +} + +async function handleOpenConversation(e) { + e.stopPropagation(); + const payload = props.item.payload || {}; + const ok = await openConversationByThreadKey(payload.thread_key); + if (ok) { + store.drawerOpen = false; + emit('read', props.item.id); + } +} + +function handleOpenDeeplink(e) { + e.stopPropagation(); + const payload = props.item.payload || {}; + const deeplink = resolveDeeplink(payload.deeplink); if (deeplink) { router.push(deeplink); store.drawerOpen = false; @@ -110,7 +176,27 @@ function handleArchive(e) {

{{ item.payload?.title }}

{{ item.payload?.detail }}

-

{{ timeAgo }}

+
@@ -193,12 +279,46 @@ function handleArchive(e) { overflow: hidden; text-overflow: ellipsis; } +.notif-item__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-top: 0.2rem; +} .notif-item__time { font-size: 0.7rem; color: var(--text-color-secondary); opacity: 0.7; margin: 0; } +.notif-item__quick { + display: flex; + gap: 0.3rem; + flex-wrap: wrap; +} +.notif-quick-btn { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.15rem 0.55rem; + border: 1px solid var(--surface-border); + background: var(--surface-card); + color: var(--text-color-secondary); + border-radius: 9999px; + font-size: 0.68rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +.notif-quick-btn:hover { + background: var(--surface-hover); + color: var(--text-color); + border-color: var(--text-color-secondary); +} +.notif-quick-btn i { + font-size: 0.65rem; +} .notif-item__actions { display: flex;