diff --git a/src/features/agenda/pages/AgendaClinicaPage.vue b/src/features/agenda/pages/AgendaClinicaPage.vue index a586d51..036b608 100644 --- a/src/features/agenda/pages/AgendaClinicaPage.vue +++ b/src/features/agenda/pages/AgendaClinicaPage.vue @@ -36,6 +36,12 @@ import { useDeterminedCommitments } from '@/features/agenda/composables/useDeter import { useFeriados } from '@/composables/useFeriados'; import { useAgendaBloqueios } from '@/features/agenda/composables/useAgendaBloqueios'; +// Fase D (replicação Clínica): adopta agendaBilling.service via composable +// reutilizável. Cobre status change com confirm dialog + multa + reverse + +// pacote saldo/upfront (C7-C13 de Melissa, espelho da Fase C do Rail). +import { useAgendaStatusChange } from '@/features/agenda/composables/useAgendaStatusChange'; +import AgendaStatusChangeConfirmDialog from '@/features/agenda/components/AgendaStatusChangeConfirmDialog.vue'; + import { mapAgendaEventosToCalendarEvents, minutesToDuration } from '@/features/agenda/services/agendaMappers'; import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettings'; import { useTenantStore } from '@/stores/tenantStore'; @@ -502,6 +508,15 @@ const ownerOptions = computed(() => staffCols.value.map((p) => ({ label: p.title // -------------------- events -------------------- const { loading: loadingEvents, error: eventsError, rows, loadClinicRange, createClinic, updateClinic, removeClinic } = useAgendaClinicEvents(); +// Fase D: status change com confirm dialog + billing (Melissa pattern). +const { + dialogOpen: statusDialogOpen, + dialogProps: statusDialogProps, + onDialogConfirm: onStatusDialogConfirm, + onDialogCancel: onStatusDialogCancel, + applyStatusChange +} = useAgendaStatusChange({ toast }); + const { loadAndExpand, createRule, updateRule, cancelRule, splitRuleAt, cancelRuleFrom, upsertException } = useRecurrence(); const { saveRuleItems, propagateToSerie } = useCommitmentServices(); @@ -1189,38 +1204,58 @@ function onEditSeriesOccurrence({ id, recurrence_date, inicio_em, fim_em, is_vir async function onUpdateSeriesEvent({ id, status, recurrence_date, inicio_em, fim_em, is_virtual }) { const tid = tenantId.value; try { - if (id) { - await updateClinic(id, { status }, { tenantId: tid }); - return; + const row = dialogEventRow.value || {}; + + // 1) Materializar virtual se preciso (resolve eventoId real) + let eventoId = id; + if (!id) { + if (!is_virtual || !inicio_em) return; + const rid = row.recurrence_id ?? row.serie_id ?? null; + const rDate = recurrence_date || inicio_em?.slice(0, 10); + const { data: existing } = await supabase + .from('agenda_eventos') + .select('id') + .eq('recurrence_id', rid) + .eq('recurrence_date', rDate) + .maybeSingle(); + if (existing?.id) { + eventoId = existing.id; + } else { + // Materializa com status='agendado'; o status final aplica + // após applyStatusChange ramificar pelo dialog se preciso. + const created = await createClinic( + { + owner_id: dialogOwnerId.value || clinicOwnerId.value, + tenant_id: tid, + recurrence_id: rid, + recurrence_date: rDate, + tipo: 'sessao', + status: 'agendado', + inicio_em, + fim_em, + visibility_scope: 'public', + titulo: row.titulo || 'Sessão', + patient_id: row.patient_id || row.paciente_id || null, + determined_commitment_id: row.determined_commitment_id || null + }, + { tenantId: tid } + ); + eventoId = created?.id || null; + } } - if (!is_virtual || !inicio_em) return; - const rid = dialogEventRow.value?.recurrence_id ?? dialogEventRow.value?.serie_id ?? null; - const rDate = recurrence_date || inicio_em?.slice(0, 10); + // 2) Atualiza status no DB + if (eventoId) { + await updateClinic(eventoId, { status }, { tenantId: tid }); + } - const { data: existing } = await supabase.from('agenda_eventos').select('id').eq('recurrence_id', rid).eq('recurrence_date', rDate).maybeSingle(); + // 3) Fluxo de billing (load context + dialog + apply) + const rowWithStatus = { ...row, id: eventoId, status, inicio_em, fim_em }; + const { applied } = await applyStatusChange({ eventoId, row: rowWithStatus, novoStatus: status }); - if (existing?.id) { - await updateClinic(existing.id, { status }, { tenantId: tid }); - } else { - const row = dialogEventRow.value || {}; - await createClinic( - { - owner_id: dialogOwnerId.value || clinicOwnerId.value, - tenant_id: tid, - recurrence_id: rid, - recurrence_date: rDate, - tipo: 'sessao', - status, - inicio_em, - fim_em, - visibility_scope: 'public', - titulo: row.titulo || 'Sessão', - patient_id: row.patient_id || row.paciente_id || null, - determined_commitment_id: row.determined_commitment_id || null - }, - { tenantId: tid } - ); + // 4) Refetch se aplicou (UI reflete novo estado) + if (applied && typeof loadClinicRange === 'function') { + await loadClinicRange(); } } catch (e) { toast.add({ severity: 'warn', summary: 'Erro', detail: e?.message || 'Falha ao atualizar status.', life: 3000 }); @@ -2463,6 +2498,21 @@ function goRecorrencias() { + + +