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() {
+
+ !v && onStatusDialogCancel()"
+ />
+