Toast SLA: botão "Abrir conversa" abre drawer direto da thread

O alerta já vem com payload.thread_key vindo do edge conversation-sla-
check. Agora o toast renderiza 2 botões lado a lado quando thread_key
existe:
- "Abrir conversa" (outlined) → abre ConversationDrawer global direto
  na thread, sem navegar de página. Usa o store global que já existe.
- "Abrir CRM →" (solid) → fallback pra lista inteira via deeplink alias.

openConversationDrawer busca o row da view conversation_threads pelo
tenant+thread_key e delega pro conversationDrawerStore.openForThread.
Se a thread sumiu (arquivada/paciente deletado), cai no fallback de
navegar pra /conversas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-04-23 11:33:38 -03:00
parent 5f51bc068e
commit f646efe522
2 changed files with 53 additions and 11 deletions
+2 -1
View File
@@ -56,7 +56,8 @@ export function useNotifications() {
group: 'system-alerts',
data: {
deeplink: payload.deeplink,
actionLabel: payload.actionLabel || defaultActionLabel(payload.deeplink)
actionLabel: payload.actionLabel || defaultActionLabel(payload.deeplink),
thread_key: payload.thread_key || null
}
});
}
+51 -10
View File
@@ -19,6 +19,9 @@ import { useLayout } from '@/layout/composables/layout';
import { computed, onMounted, onBeforeUnmount, provide, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
const router = useRouter();
// Aliases "semânticos" → resolvidos pra rota real com base no role atual.
@@ -47,6 +50,32 @@ function goToDeeplink(link) {
}
}
// Abre o drawer global de conversa a partir do thread_key (do payload do alerta).
// Busca o row da view conversation_threads e delega pro store.
async function openConversationDrawer(threadKey) {
if (!threadKey) return;
try {
const tenantId = tenantStore.activeTenantId;
const { data, error } = await supabase
.from('conversation_threads')
.select('*')
.eq('tenant_id', tenantId)
.eq('thread_key', threadKey)
.maybeSingle();
if (error) throw error;
if (!data) {
// Thread sumiu (foi arquivada ou paciente deletado) — fallback pra lista
goToDeeplink('/conversas');
return;
}
const drawerStore = useConversationDrawerStore();
await drawerStore.openForThread(data);
} catch (e) {
console.warn('[AppLayout] openConversationDrawer falhou:', e?.message);
goToDeeplink('/conversas');
}
}
import AppFooter from './AppFooter.vue';
import AppSidebar from './AppSidebar.vue';
import AppTopbar from './AppTopbar.vue';
@@ -216,16 +245,28 @@ onBeforeUnmount(() => {
<div class="text-xs mt-0.5 leading-relaxed opacity-85">{{ slotProps.message.detail }}</div>
</div>
</div>
<Button
v-if="slotProps.message.data?.deeplink"
:label="slotProps.message.data?.actionLabel || 'Abrir'"
icon="pi pi-arrow-right"
iconPos="right"
size="small"
severity="danger"
class="rounded-full self-end"
@click="goToDeeplink(slotProps.message.data.deeplink)"
/>
<div class="flex gap-2 self-end flex-wrap">
<Button
v-if="slotProps.message.data?.thread_key"
label="Abrir conversa"
icon="pi pi-comment"
size="small"
severity="danger"
outlined
class="rounded-full"
@click="openConversationDrawer(slotProps.message.data.thread_key)"
/>
<Button
v-if="slotProps.message.data?.deeplink"
:label="slotProps.message.data?.actionLabel || 'Abrir'"
icon="pi pi-arrow-right"
iconPos="right"
size="small"
severity="danger"
class="rounded-full"
@click="goToDeeplink(slotProps.message.data.deeplink)"
/>
</div>
</div>
</template>
</Toast>