/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/layout/melissa/composables/useMelissaAgendaHistorico.js | Data: 2026-05-04 | | Histórico recente de ações na agenda do terapeuta logado. | | Lê de `audit_logs` (populado automaticamente pela trigger | `trg_audit_agenda_eventos`). Não precisa criar nada — todas as ações | INSERT/UPDATE/DELETE em agenda_eventos já viram linhas auditadas. | | Filtros aplicados: | - entity_type = 'agenda_eventos' | - user_id = uid do user logado (mostra só ações dele) | - created_at >= 7 dias atrás | - tenant_id = tenant ativo | - LIMIT 20 (mais recentes primeiro) | | Pra exibir nome do paciente, fazemos um lookup separado em `patients` | usando os IDs extraídos de new_values/old_values (não dá pra fazer JOIN | na audit_logs porque entity_id é dinâmico). | | Returns: | - entries: ref de objetos normalizados: | { id, kind, label, when, paciente, evento_id, raw } | onde kind ∈ { 'create' | 'move' | 'status' | 'edit' | 'delete' } | - loading: ref | - refetch: function() |-------------------------------------------------------------------------- */ import { ref } from 'vue'; import { supabase } from '@/lib/supabase/client'; import { useTenantStore } from '@/stores/tenantStore'; const STATUS_LABEL = { agendado: 'Agendado', realizado: 'Realizada', realizada: 'Realizada', faltou: 'Falta', cancelado: 'Cancelada', cancelada: 'Cancelada', remarcar: 'Remarcar', remarcado: 'Remarcado', confirmado: 'Confirmada' }; function fmtTime(iso) { if (!iso) return ''; const d = new Date(iso); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } function fmtDateBR(iso) { if (!iso) return ''; const d = new Date(iso); return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`; } // Classifica a entrada pra um dos 5 "kinds" visuais. Decisão por // changed_fields quando action=update — ordem importa: hora primeiro // (mais frequente em movimentação), depois status, depois "edit" genérico. function classify(row) { const action = String(row.action || '').toLowerCase(); if (action === 'insert') return 'create'; if (action === 'delete') return 'delete'; if (action === 'update') { const fields = new Set(row.changed_fields || []); if (fields.has('inicio_em') || fields.has('fim_em')) return 'move'; if (fields.has('status')) return 'status'; return 'edit'; } return 'edit'; } function buildLabel(kind, row) { const oldV = row.old_values || {}; const newV = row.new_values || {}; switch (kind) { case 'create': { const ini = newV.inicio_em ? `${fmtDateBR(newV.inicio_em)} ${fmtTime(newV.inicio_em)}` : '—'; return `Criou sessão em ${ini}`; } case 'delete': { const ini = oldV.inicio_em ? `${fmtDateBR(oldV.inicio_em)} ${fmtTime(oldV.inicio_em)}` : '—'; return `Removeu sessão de ${ini}`; } case 'move': { const from = oldV.inicio_em ? `${fmtDateBR(oldV.inicio_em)} ${fmtTime(oldV.inicio_em)}` : '—'; const to = newV.inicio_em ? `${fmtDateBR(newV.inicio_em)} ${fmtTime(newV.inicio_em)}` : '—'; return `Moveu ${from} → ${to}`; } case 'status': { const lbl = STATUS_LABEL[String(newV.status || '').toLowerCase()] || newV.status || '—'; return `Status: ${lbl}`; } case 'edit': default: { const fields = (row.changed_fields || []).filter((f) => f !== 'updated_at'); if (!fields.length) return 'Editou'; return `Editou ${fields.join(', ')}`; } } } // Resolve o ID do paciente a partir de new_values/old_values (delete usa OLD). function extractPatientId(row) { return row.new_values?.patient_id || row.old_values?.patient_id || null; } export function useMelissaAgendaHistorico(opts = {}) { const limit = opts.limit ?? 20; const days = opts.days ?? 7; const tenantStore = useTenantStore(); const entries = ref([]); const loading = ref(false); const error = ref(''); async function _ensureUid() { const { data: ses } = await supabase.auth.getSession(); if (ses?.session?.user?.id) return ses.session.user.id; const { data, error: err } = await supabase.auth.getUser(); if (err) return null; return data?.user?.id || null; } async function refetch() { loading.value = true; error.value = ''; try { const userId = await _ensureUid(); if (typeof tenantStore.ensureLoaded === 'function') { await tenantStore.ensureLoaded(); } const tid = tenantStore.activeTenantId || tenantStore.tenantId || null; if (!userId || !tid) { entries.value = []; return; } const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); const { data: rows, error: err } = await supabase .from('audit_logs') .select('id, action, entity_id, changed_fields, old_values, new_values, created_at, user_id, tenant_id') .eq('entity_type', 'agenda_eventos') .eq('user_id', userId) .eq('tenant_id', tid) .gte('created_at', since) .order('created_at', { ascending: false }) .limit(limit); if (err) throw err; const list = rows || []; // Resolve nomes dos pacientes em uma única query. const patientIds = [...new Set(list.map(extractPatientId).filter(Boolean))]; const patientMap = new Map(); if (patientIds.length) { const { data: pats } = await supabase .from('patients') .select('id, nome_completo') .in('id', patientIds); for (const p of pats || []) patientMap.set(p.id, p.nome_completo); } entries.value = list.map((r) => { const kind = classify(r); const pid = extractPatientId(r); return { id: r.id, kind, label: buildLabel(kind, r), when: r.created_at, paciente: pid ? (patientMap.get(pid) || '') : '', evento_id: r.entity_id, raw: r }; }); } catch (e) { error.value = e?.message || 'Falha ao carregar histórico'; entries.value = []; // eslint-disable-next-line no-console console.warn('[useMelissaAgendaHistorico]', e); } finally { loading.value = false; } } return { entries, loading, error, refetch }; }