957e912a7f
Sprints B (05-03) e C (05-04) acumulados: - NotificationDrawer/Item redesign (visual mais limpo, ações inline) - Dock pins compose (useMelissaDockPins) + cache store global (melissaCacheStore) - MelissaAgenda: timeline FullCalendar parity + cards resumo, histórico card com useMelissaAgendaHistorico, MelissaEventoPanel ajustado - useFeriados: cache opt-in pra evitar fetch redundante de feriados - PatientProntuario: aba Visão Geral nova; PatientConversationsTab polish - AgendaClinicMosaic / AgendaTerapeutaPage / useAgendaSettings: ajustes de paridade com Melissa - DocumentsListPage: pequenos ajustes - DB migration 20260504000001: fix do trigger pra status 'excluido' nas cancel_notifications Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
194 lines
7.0 KiB
JavaScript
194 lines
7.0 KiB
JavaScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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<boolean>
|
|
| - 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 };
|
|
}
|