agenda Fase D: adapter Clinica usa agendaBilling.service
AgendaClinicaPage espelha Fase C: useAgendaStatusChange composable
+ AgendaStatusChangeConfirmDialog plugado.
onUpdateSeriesEvent reescrito:
- Materializa virtual se preciso (via createClinic com status='agendado'
+ tenantId)
- updateClinic({ status }) no DB
- applyStatusChange(eventoId, row, novoStatus) ramifica via dialog
quando preciso
- loadClinicRange() refetch apos applied
Mesma feature parity de Melissa pra status change na Clinica:
multa, taxa cancelamento tardio, consumir saldo, gerar cobranca
pacote saldo, reverse transition trava — tudo via agendaBilling.service.
Fase C (Rail) + Fase D (Clinica) fechadas. Os 3 layouts (Melissa/
Rail/Clinica) agora compartilham o billing core do agendaBilling.
service via composable useAgendaStatusChange.
Pendente (residual incremental):
- Indicadores visuais (3 canais) nos 3 layouts
- Antecipar/Revogar pagamento no popover de Rail (Rail nao tem
popover separado — usa AgendaEventDialog direto; precisa
refactor maior)
- Doc de ajuda
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,12 @@ import { useDeterminedCommitments } from '@/features/agenda/composables/useDeter
|
|||||||
import { useFeriados } from '@/composables/useFeriados';
|
import { useFeriados } from '@/composables/useFeriados';
|
||||||
import { useAgendaBloqueios } from '@/features/agenda/composables/useAgendaBloqueios';
|
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 { mapAgendaEventosToCalendarEvents, minutesToDuration } from '@/features/agenda/services/agendaMappers';
|
||||||
import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettings';
|
import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettings';
|
||||||
import { useTenantStore } from '@/stores/tenantStore';
|
import { useTenantStore } from '@/stores/tenantStore';
|
||||||
@@ -502,6 +508,15 @@ const ownerOptions = computed(() => staffCols.value.map((p) => ({ label: p.title
|
|||||||
// -------------------- events --------------------
|
// -------------------- events --------------------
|
||||||
const { loading: loadingEvents, error: eventsError, rows, loadClinicRange, createClinic, updateClinic, removeClinic } = useAgendaClinicEvents();
|
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 { loadAndExpand, createRule, updateRule, cancelRule, splitRuleAt, cancelRuleFrom, upsertException } = useRecurrence();
|
||||||
|
|
||||||
const { saveRuleItems, propagateToSerie } = useCommitmentServices();
|
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 }) {
|
async function onUpdateSeriesEvent({ id, status, recurrence_date, inicio_em, fim_em, is_virtual }) {
|
||||||
const tid = tenantId.value;
|
const tid = tenantId.value;
|
||||||
try {
|
try {
|
||||||
if (id) {
|
const row = dialogEventRow.value || {};
|
||||||
await updateClinic(id, { status }, { tenantId: tid });
|
|
||||||
return;
|
// 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;
|
// 2) Atualiza status no DB
|
||||||
const rDate = recurrence_date || inicio_em?.slice(0, 10);
|
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) {
|
// 4) Refetch se aplicou (UI reflete novo estado)
|
||||||
await updateClinic(existing.id, { status }, { tenantId: tid });
|
if (applied && typeof loadClinicRange === 'function') {
|
||||||
} else {
|
await loadClinicRange();
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.add({ severity: 'warn', summary: 'Erro', detail: e?.message || 'Falha ao atualizar status.', life: 3000 });
|
toast.add({ severity: 'warn', summary: 'Erro', detail: e?.message || 'Falha ao atualizar status.', life: 3000 });
|
||||||
@@ -2463,6 +2498,21 @@ function goRecorrencias() {
|
|||||||
<!-- Dialog de Bloqueio -->
|
<!-- Dialog de Bloqueio -->
|
||||||
<BloqueioDialog v-model="bloqueioDialogOpen" :mode="bloqueioMode" :workRules="workRules" :settings="settings" :ownerId="clinicOwnerId" :tenantId="tenantId || ''" @saved="refetch" />
|
<BloqueioDialog v-model="bloqueioDialogOpen" :mode="bloqueioMode" :workRules="workRules" :settings="settings" :ownerId="clinicOwnerId" :tenantId="tenantId || ''" @saved="refetch" />
|
||||||
|
|
||||||
|
<!-- Fase D: confirma status change com decisões de billing
|
||||||
|
(multa, consumir saldo, gerar cobrança, reverse transition). -->
|
||||||
|
<AgendaStatusChangeConfirmDialog
|
||||||
|
v-model="statusDialogOpen"
|
||||||
|
:evento="statusDialogProps.evento"
|
||||||
|
:novoStatus="statusDialogProps.novoStatus"
|
||||||
|
:regraExcecao="statusDialogProps.regraExcecao"
|
||||||
|
:billingContract="statusDialogProps.billingContract"
|
||||||
|
:billingContractStyle="statusDialogProps.billingContractStyle"
|
||||||
|
:pendingRecord="statusDialogProps.pendingRecord"
|
||||||
|
:sessionPrice="statusDialogProps.sessionPrice"
|
||||||
|
@confirm="onStatusDialogConfirm"
|
||||||
|
@update:modelValue="(v) => !v && onStatusDialogCancel()"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Dialog: feriados próximos (todos os dias úteis — bloqueados e pendentes) -->
|
<!-- Dialog: feriados próximos (todos os dias úteis — bloqueados e pendentes) -->
|
||||||
<Dialog v-model:visible="feriadosAlertaOpen" modal :draggable="false" header="Feriados nos próximos 30 dias" :style="{ width: '520px', maxWidth: '96vw' }">
|
<Dialog v-model:visible="feriadosAlertaOpen" modal :draggable="false" header="Feriados nos próximos 30 dias" :style="{ width: '520px', maxWidth: '96vw' }">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user