Analytics 7.1: tempo médio de 1ª resposta WhatsApp no dashboard
Card novo pra clínica e terapeuta com 3 métricas + sparkline: - Tempo médio (e mediana) de 1ª resposta no período - Taxa de SLA cumprido — % de respostas dentro do threshold configurado - Contagem total de respostas no período - Sparkline da evolução com indicador de tendência (melhorando/piorando) - Ranking top 5 terapeutas (só no ClinicDashboard) Filtro de período: 7/30/90 dias (muda granularidade do bucket: 1/7/15 dias pra sparkline com ~5-6 pontos). Banco (migration 20260423000006): - Helper interno _first_response_runs: identifica "runs" de inbound (sequências do paciente sem outbound entre) e calcula delta até a próxima outbound. Evita contar múltiplas mensagens repetidas do paciente. responder_id vem de conversation_assignments. - first_response_stats: agregados (count, avg, median, min, max, sla_compliance_rate baseado em conversation_sla_rules). - first_response_by_therapist: ranking com avg e count por assigned_to. - first_response_evolution: série temporal com bucket alinhado a p_from (p_from + bucket_index * N days). Parâmetro p_bucket_days deixa o frontend escolher granularidade por período. Todas SECURITY DEFINER + GRANT authenticated/service_role. Filtro opcional por therapist_id nas funções que aplicam. Frontend: - useFirstResponseAnalytics composable wraps as 3 RPCs com cache via Promise.all paralelo. Helper formatSeconds (Ns/Xmin/Xh). - FirstResponseCard.vue renderiza sparkline SVG nativo (sem lib extra), cor da taxa SLA por threshold (verde ≥80%, âmbar ≥50%, vermelho). - Integrado em ClinicDashboard (visão global) e TherapistDashboard (filtrado por ownerId, sem ranking). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import Menu from 'primevue/menu';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useClinicKPIs } from '@/composables/useClinicKPIs';
|
||||
import FirstResponseCard from '@/components/dashboard/FirstResponseCard.vue';
|
||||
|
||||
// Fase 3a — KPIs financeiros/operacionais da clínica
|
||||
const kpis = useClinicKPIs();
|
||||
@@ -953,6 +954,9 @@ onMounted(async () => {
|
||||
<div class="dash-card__foot" @click="$router.push('/admin/agendamentos-recebidos')">Ver todas →</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics: tempo de 1ª resposta WhatsApp -->
|
||||
<FirstResponseCard v-if="!loading" />
|
||||
|
||||
<!-- Cadastros externos -->
|
||||
<div v-if="!loading" class="dash-card">
|
||||
<div class="dash-card__head gap-2.5 p-2.5">
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useAgendaEvents } from '@/features/agenda/composables/useAgendaEvents';
|
||||
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
|
||||
import { useDeterminedCommitments } from '@/features/agenda/composables/useDeterminedCommitments';
|
||||
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
||||
import FirstResponseCard from '@/components/dashboard/FirstResponseCard.vue';
|
||||
|
||||
const dashHeroSentinelRef = ref(null);
|
||||
const heroStuck = ref(false);
|
||||
@@ -1082,6 +1083,11 @@ onMounted(async () => {
|
||||
<div class="dash-card__foot" @click="$router.push('/therapist/agendamentos-recebidos')">Ver todas →</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics: tempo de 1ª resposta WhatsApp (minhas) -->
|
||||
<div id="card-first-response" class="anim-child [--delay:40ms]">
|
||||
<FirstResponseCard :therapistId="ownerId" :showRanking="false" />
|
||||
</div>
|
||||
|
||||
<!-- Cadastros externos -->
|
||||
<div id="card-cadastros" class="anim-child [--delay:80ms] dash-card">
|
||||
<div class="dash-card__head gap-2.5 p-2.5">
|
||||
|
||||
Reference in New Issue
Block a user