Files
agenciapsilmno/src/router/routes.configs.js
T
Leonardo 771b636cee SLA de conversas WhatsApp (Grupo 3.4): config + detecção + alerta
Completa o Grupo 3 do CRM com alerta de conversa sem resposta além
do tempo configurado — reutiliza o pipeline system_alert (toast
vermelho sticky + sininho + drawer).

Banco (migration 20260423000005):
- conversation_sla_rules: 1 linha por tenant com threshold global
  (1-1440 min), respect_business_hours, business_hours_start/end,
  business_days (ISO 1=seg..7=dom), alert_scope (assigned_only|all),
  notify_admin_on_breach. Default: enabled=false.
- conversation_sla_breaches: incidents com UNIQUE parcial
  (tenant_id, thread_key) WHERE resolved_at IS NULL — idempotência.
- Trigger AFTER INSERT em conversation_messages resolve o breach
  automaticamente quando chega nova outbound na thread.
- RPCs service_role: sla_open_breach (idempotente), sla_mark_notified.
- RLS: membros do tenant leem; clinic_admin/tenant_admin/saas_admin
  escrevem na config; service_role escreve em breaches.

Edge function conversation-sla-check (cron 5min):
- Varre tenants com enabled=true.
- Query conversation_threads onde last_message_direction='inbound'
  (+ assigned_to NOT NULL se scope='assigned_only').
- Se respect_business_hours: calcula businessMinutesElapsed em TS
  iterando dia por dia a interseção da janela [start,end] com
  [last_inbound_at, now], só em dias marcados em business_days. TZ
  fixa em America/Sao_Paulo via Intl.DateTimeFormat.
- Se elapsed >= threshold: sla_open_breach (idempotente) + notifica
  assigned_to sempre + admins se notify_admin_on_breach (deduplicado
  via Set).
- Anti-spam: só notifica 1x por incident (checa notified_at).
- Notification leva deeplink pra /crm/conversas e payload.thread_key
  pro frontend destacar a conversa (fora de escopo deste commit).

UI em /configuracoes/conversas-sla:
- Toggle enabled + InputNumber threshold com preview "≈ Xh Ymin".
- Toggle respect_business_hours → revela start/end + seletor de dias
  úteis (pills toggleáveis Seg..Dom, ISO order).
- Select scope.
- Toggle notify_admin_on_breach.
- Card abaixo com breaches dos últimos 7 dias (status aberto/resolvido,
  thread_key, limite configurado no momento do breach, duração).
- Adicionada na ConfiguracoesPage landing + rota /configuracoes/conversas-sla.

Cron template comentado no fim da migration (mesmo padrão do heartbeat).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:17:41 -03:00

168 lines
6.1 KiB
JavaScript

/*
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/router/routes.configs.js
| Data: 2026
| Local: São Carlos/SP — Brasil
|--------------------------------------------------------------------------
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
// ConfiguracoesPage já tem <router-view> próprio — serve de layout intermediário.
// Não precisa de RouterPassthrough.
export default {
path: 'configuracoes',
component: () => import('@/layout/ConfiguracoesPage.vue'),
redirect: { name: 'ConfiguracoesAgenda' },
meta: {
requiresAuth: true,
roles: ['admin', 'tenant_admin', 'therapist']
},
children: [
{
path: 'agenda',
name: 'ConfiguracoesAgenda',
component: () => import('@/layout/configuracoes/ConfiguracoesAgendaPage.vue')
},
{
path: 'bloqueios',
name: 'ConfiguracoesBloqueios',
component: () => import('@/layout/configuracoes/BloqueiosPage.vue')
},
{
path: 'agendador',
name: 'ConfiguracoesAgendador',
component: () => import('@/layout/configuracoes/ConfiguracoesAgendadorPage.vue')
},
{
path: 'pagamento',
name: 'ConfiguracoesPagamento',
component: () => import('@/layout/configuracoes/ConfiguracoesPagamentoPage.vue')
},
{
path: 'precificacao',
name: 'ConfiguracoesPrecificacao',
component: () => import('@/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue')
},
{
path: 'descontos',
name: 'ConfiguracoesDescontos',
component: () => import('@/layout/configuracoes/ConfiguracoesDescontosPage.vue')
},
{
path: 'excecoes-financeiras',
name: 'ConfiguracoesExcecoesFinanceiras',
component: () => import('@/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue')
},
{
path: 'convenios',
name: 'ConfiguracoesConvenios',
component: () => import('@/layout/configuracoes/ConfiguracoesConveniosPage.vue')
},
{
path: 'email-templates',
name: 'ConfiguracoesEmailTemplates',
component: () => import('@/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue')
},
{
path: 'empresa',
name: 'ConfiguracoesMinhaEmpresa',
component: () => import('@/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue')
},
{
path: 'canais',
name: 'ConfiguracoesCanais',
component: () => import('@/layout/configuracoes/ConfiguracoesCanaisPage.vue')
},
{
path: 'whatsapp',
name: 'ConfiguracoesWhatsapp',
component: () => import('@/layout/configuracoes/ConfiguracoesWhatsappChooserPage.vue')
},
{
path: 'whatsapp-pessoal',
name: 'ConfiguracoesWhatsappPessoal',
component: () => import('@/layout/configuracoes/ConfiguracoesWhatsappPage.vue')
},
{
path: 'whatsapp-oficial',
name: 'ConfiguracoesWhatsappOficial',
component: () => import('@/layout/configuracoes/ConfiguracoesTwilioWhatsappPage.vue')
},
{
path: 'whatsapp-templates',
name: 'ConfiguracoesWhatsappTemplates',
component: () => import('@/layout/configuracoes/ConfiguracoesWhatsappTemplatesPage.vue')
},
// Backcompat: old /whatsapp-twilio → redirect
{
path: 'whatsapp-twilio',
redirect: { name: 'ConfiguracoesWhatsappOficial' }
},
{
path: 'sms',
name: 'ConfiguracoesSms',
component: () => import('@/layout/configuracoes/ConfiguracoesSmsPage.vue')
},
{
path: 'sms-canal',
name: 'ConfiguracoesSmsCanal',
component: () => import('@/views/pages/notifications/SmsChannelSetupPage.vue')
},
{
path: 'recursos-extras',
name: 'ConfiguracoesRecursosExtras',
component: () => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')
},
{
path: 'recursos-extras/extrato',
name: 'ConfiguracoesRecursosExtrasExtrato',
component: () => import('@/layout/configuracoes/AddonsExtratoPage.vue')
},
{
path: 'auditoria',
name: 'ConfiguracoesAuditoria',
component: () => import('@/layout/configuracoes/AuditoriaPage.vue')
},
{
path: 'conversas-tags',
name: 'ConfiguracoesConversasTags',
component: () => import('@/layout/configuracoes/ConfiguracoesConversasTagsPage.vue')
},
{
path: 'conversas-autoreply',
name: 'ConfiguracoesConversasAutoreply',
component: () => import('@/layout/configuracoes/ConfiguracoesConversasAutoreplyPage.vue')
},
{
path: 'conversas-optouts',
name: 'ConfiguracoesConversasOptouts',
component: () => import('@/layout/configuracoes/ConfiguracoesConversasOptoutsPage.vue')
},
{
path: 'conversas-sla',
name: 'ConfiguracoesConversasSla',
component: () => import('@/layout/configuracoes/ConfiguracoesConversasSlaPage.vue')
},
{
path: 'lembretes-sessao',
name: 'ConfiguracoesLembretesSessao',
component: () => import('@/layout/configuracoes/ConfiguracoesLembretesSessaoPage.vue')
},
{
path: 'creditos-whatsapp',
name: 'ConfiguracoesCreditosWhatsapp',
component: () => import('@/layout/configuracoes/ConfiguracoesCreditosWhatsappPage.vue')
}
]
};