Toast system_alert ganha botão de ação com deeplink

Novo <Toast group="system-alerts"> no AppLayout com template custom
(vive no bloco global — persiste em qualquer layout/rota). Renderiza:
- Ícone de alerta + título em bold
- Detail em texto menor com opacity
- Botão com deeplink quando payload.deeplink existe, severity danger

Label do botão inferido do deeplink:
- /configuracoes/creditos-whatsapp → "Ir pra loja"
- /configuracoes/whatsapp-pessoal  → "Ver conexão"
- /configuracoes/whatsapp-oficial  → "Ver canal oficial"
- outros → "Abrir" (ou payload.actionLabel se vier explícito)

Clique navega via router.push se é path interno, senão
window.location.href. Toast continua sticky (24h) + closable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-04-23 10:00:18 -03:00
parent 5c50db6704
commit 6db06abfc2
2 changed files with 53 additions and 2 deletions
+15 -1
View File
@@ -33,6 +33,15 @@ export function useNotifications() {
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;
@@ -43,7 +52,12 @@ export function useNotifications() {
summary: payload.title || 'Alerta do sistema',
detail: payload.detail || '',
life: STICKY_TOAST_LIFE_MS,
closable: true
closable: true,
group: 'system-alerts',
data: {
deeplink: payload.deeplink,
actionLabel: payload.actionLabel || defaultActionLabel(payload.deeplink)
}
});
}
+38 -1
View File
@@ -17,7 +17,18 @@
<script setup>
import { useLayout } from '@/layout/composables/layout';
import { computed, onMounted, onBeforeUnmount, provide, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
const router = useRouter();
function goToDeeplink(link) {
if (!link) return;
if (typeof link === 'string' && link.startsWith('/')) {
router.push(link);
} else {
window.location.href = link;
}
}
import AppFooter from './AppFooter.vue';
import AppSidebar from './AppSidebar.vue';
@@ -175,6 +186,32 @@ onBeforeUnmount(() => {
<GlobalNoticeBanner />
<ConversationDrawer />
<GlobalInboundNotifier />
<!-- Toast especial pra alertas de sistema (heartbeat, saldo baixo, etc)
vermelho sticky com botão de ação opcional via payload.deeplink -->
<Toast group="system-alerts" position="top-right" :pt="{ root: { style: 'width: 28rem; max-width: 92vw' } }">
<template #message="slotProps">
<div class="flex flex-col gap-2 w-full">
<div class="flex items-start gap-2">
<i class="pi pi-exclamation-circle text-lg mt-0.5" style="color: var(--p-red-500)" />
<div class="flex-1 min-w-0">
<div class="font-bold text-sm">{{ slotProps.message.summary }}</div>
<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>
</template>
</Toast>
</template>
<style>