e95ed9b585
DB
- drop agenda_excecoes (substituida por financial_exceptions + lock-edit
baseado em financial_records)
- financial_records.payment_link (Asaas + link compartilhavel)
- financial_exceptions.consume_on_miss (rotular nao-show consome ou nao)
- billing_contracts.charging_style (upfront/saldo/per_session)
Payment refactor
- paymentSettlement -> paymentMethod (string) + markPaidNow (bool).
Handler aplica payment_method sempre; status='paid'+paid_at apenas
quando markPaidNow=true && method != 'link'. Asaas (link) sempre
liquida via webhook, nunca nasce paid.
- create_financial_record_for_session com pos-RPC patch pra payment_method
e (opcional) status='paid' quando user marca "ja recebi".
Indicadores visuais (3 canais distintos por estado)
- Paid: barra esquerda emerald-500 4px na agenda (MelissaAgenda),
pi-check-circle no popover/Resumo.
- Pending: badge \$ amber canto direito (mantido); linha amber no popover/
Resumo "A receber R\$ X (cobranca pendente)".
- Neutro: sem badge nem barra (compromisso pessoal, bloqueio, ou
ocorrencia virtual de pacote upfront/saldo).
- Bulk-load de paymentState em _reloadRange etapa 4 (1 query unica em
financial_records mapeada por agenda_evento_id).
- AgendaEventDialog Resumo lateral ganha linha entre pi-clock e
pi-map-marker via novo sessionPaymentRecord (sem guard de
occurrenceMode, contrario ao occFinancialRecord que continua so pra
Rail/Clinica). 5 estados: paid+paid_at, overdue+venceu, pending+vence,
sem cobranca c/ valor, sem cobranca s/ valor.
UX de convenio
- InsurancePlanServiceQuickCreateDialog novo: cadastra procedimento
POR CIMA do AgendaEventDialog sem sair da agenda. Auto-seleciona
novo procedimento so quando nada estava selecionado antes.
- Caixa cinza "Cadastrar procedimento" sempre visivel quando convenio
selecionado, com copy variavel (0 procedimentos: chamada urgente;
1+: "se quiser adicionar mais").
- "+ Novo convenio" toolbar em ConfiguracoesConveniosPage (botao
estava faltando, empty state mandava clicar em botao inexistente).
- Hint contextual abaixo do card Sessao/Honorarios: convenio = "N da
guia eh opcional", gratuito = "sem cobranca", particular = sem hint.
Label "N da Guia" tambem ganhou "(opcional)" no service-picker dialog.
Bug fixes
- pickDbFields whitelist faltando 'modalidade' (useMelissaAgenda.js:74)
— sessoes avulsas eram salvas como presencial independente da
escolha visual. Adicionado.
- goToConveniosConfig removida — fazia router.push("/therapist/
configuracoes/convenios") mas /configuracoes/* eh rota raiz, nao
filha. Substituida pelo quick-create inline (#1).
- bloqueioCobrindo + dialogBlockOverlap passados via deps em
_buildHandlers (refs do useMelissaAgenda nao sao acessiveis no
escopo de _buildHandlers).
Fase 5 (status change + edit cobrada)
- AgendaStatusChangeConfirmDialog: confirm dialog quando user muda
status pra realizada/faltou/cancelado, com opcoes de markPaid ou
gerar cobranca conforme o caso.
- useAgendaBloqueios novo composable: extrai logica de bloqueios
cinza (background events) do MelissaAgenda.
Doc viva
- src/docs/agenda-compromisso-financeiro-cenarios.html: 13 cenarios
de teste manual. C1-C4 ja validados. Cada teste validado vira parte
da doc final pra area de ajuda (pos-Fase 9).
Wiki/handoff
- agenda-compromisso-fluxo e agenda-billing-pesquisa-mercado (decisoes
arquiteturais sobre billing).
- HANDOFF.md atualizado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
582 lines
25 KiB
JavaScript
582 lines
25 KiB
JavaScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Arquivo: src/features/agenda/composables/useAgendaEventLifecycle.js
|
|
| Data: 2026-05-04
|
|
|
|
|
| A66 sub-sessão 1C-ii-b — lifecycle do AgendaEventDialog:
|
|
| - Watcher props.modelValue (init form ao abrir — orquestra
|
|
| loadPatients/ensureServicesLoaded/loadInsurancePlans/
|
|
| _loadCommitmentItemsForEvent + reset de refs)
|
|
| - Watcher [tenantId, restrictPatients, patientScopeOwnerId]
|
|
| - Watcher [dia, startTime] (solicitação pendente do agendador público)
|
|
| - Watcher [dia, modalidade] (online slots loader)
|
|
| - Series pills (loadSerieEvents + 4 handlers + generateRuleDates)
|
|
| - selectSlot
|
|
| - Quick-creates wiring (service + insurance)
|
|
| - onSendManualReminder (lembrete WhatsApp)
|
|
|
|
|
| Recebe via argumento:
|
|
| composer — composer (1B)
|
|
| actions — actions (1C-i): _skipStatusWatch, _restoringConvenio,
|
|
| samePatientConflict
|
|
| pickerBilling — picker/billing (1C-ii-a): ensureServicesLoaded,
|
|
| _loadCommitmentItemsForEvent, clearPatientsCache,
|
|
| loadPatients, addItem
|
|
| commitmentItems — ref<Item[]>
|
|
| serieEvents — ref<SerieEvent[]>
|
|
| servicePickerSel — ref do picker
|
|
| selectedPlanService — ref do procedure de convênio
|
|
| serieValorMode — ref<'multiplicar' | 'dividir'>
|
|
| services — ref<Service[]> (de useServices)
|
|
| loadServices — fn(ownerId)
|
|
| loadInsurancePlans — fn(ownerId)
|
|
| props — props do dialog
|
|
| emit — emitter ('updateSeriesEvent', 'editSeriesOccurrence', 'delete')
|
|
| confirm — useConfirm()
|
|
| toast — useToast()
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
import { ref, computed, watch, nextTick } from 'vue';
|
|
import { supabase } from '@/lib/supabase/client';
|
|
|
|
export function generateRuleDates(rule) {
|
|
const { type, interval = 1, weekdays = [], start_date, end_date, max_occurrences } = rule || {};
|
|
if (!start_date || !weekdays?.length) return [];
|
|
const maxOcc = Math.min(max_occurrences || 365, 365);
|
|
const endLimit = end_date ? new Date(end_date + 'T23:59:59') : null;
|
|
const dates = [];
|
|
|
|
if (type === 'custom_weekdays') {
|
|
const cursor = new Date(start_date + 'T12:00:00');
|
|
let safety = 0;
|
|
while (dates.length < maxOcc && safety < 2000) {
|
|
safety++;
|
|
if (endLimit && cursor > endLimit) break;
|
|
if (weekdays.includes(cursor.getDay())) dates.push(cursor.toISOString().slice(0, 10));
|
|
cursor.setDate(cursor.getDate() + 1);
|
|
}
|
|
} else {
|
|
// weekly (interval=1) ou quinzenal (interval=2)
|
|
const cursor = new Date(start_date + 'T12:00:00');
|
|
while (dates.length < maxOcc) {
|
|
if (endLimit && cursor > endLimit) break;
|
|
dates.push(cursor.toISOString().slice(0, 10));
|
|
cursor.setDate(cursor.getDate() + 7 * (interval || 1));
|
|
}
|
|
}
|
|
return dates;
|
|
}
|
|
|
|
export function useAgendaEventLifecycle({
|
|
composer,
|
|
actions,
|
|
pickerBilling,
|
|
commitmentItems,
|
|
serieEvents,
|
|
servicePickerSel,
|
|
selectedPlanService,
|
|
serieValorMode,
|
|
services,
|
|
loadServices,
|
|
loadInsurancePlans,
|
|
props,
|
|
emit,
|
|
confirm,
|
|
toast
|
|
}) {
|
|
// ── refs locais ────────────────────────────────────────────
|
|
const solicitacaoPendente = ref(null);
|
|
const onlineSlots = ref([]);
|
|
const loadingOnlineSlots = ref(false);
|
|
const serieLoading = ref(false);
|
|
const pillDeleteMenuRef = ref(null);
|
|
const pillDeleteTarget = ref(null);
|
|
const sendingReminder = ref(false);
|
|
const serviceQuickDlgOpen = ref(false);
|
|
const insuranceQuickDlgOpen = ref(false);
|
|
const planServiceQuickDlgOpen = ref(false);
|
|
|
|
// occurrenceMode: financial_record da ocorrencia atual (se existir).
|
|
// Usado pra travar edicao de tipo/servicos quando ja ha cobranca emitida
|
|
// (padrao SimplePractice — cobranca emitida e imutavel; ajustes via fluxo
|
|
// do Financeiro, nao via dialog). 2026-05-12.
|
|
const occFinancialRecord = ref(null);
|
|
const occFinancialLoading = ref(false);
|
|
|
|
// sessionPaymentRecord (2026-05-18): financial_record da sessão (mesmo
|
|
// shape do occFinancialRecord) mas SEM o guard de occurrenceMode.
|
|
// Carregado em qualquer edit de sessão pra alimentar a linha "Cobrança"
|
|
// do Resumo lateral do AgendaEventDialog. Não dispara lock — esse
|
|
// continua via occFinancialRecord (território da Fase 6/C13).
|
|
const sessionPaymentRecord = ref(null);
|
|
|
|
// ── computeds locais ───────────────────────────────────────
|
|
const serieCountByStatus = computed(() => {
|
|
const counts = {};
|
|
for (const ev of serieEvents.value) {
|
|
const s = ev._status || 'agendado';
|
|
counts[s] = (counts[s] || 0) + 1;
|
|
}
|
|
return counts;
|
|
});
|
|
|
|
const pillDeleteMenuItems = computed(() => {
|
|
if (!pillDeleteTarget.value) return [];
|
|
const ev = pillDeleteTarget.value;
|
|
return [
|
|
{ label: 'Remover apenas esta', icon: 'pi pi-minus-circle', command: () => onPillDelete(ev, 'somente_este') },
|
|
{ label: 'Remover esta e as seguintes', icon: 'pi pi-forward', command: () => onPillDelete(ev, 'este_e_seguintes') },
|
|
{ separator: true },
|
|
{ label: 'Remover todas as futuras', icon: 'pi pi-trash', command: () => onPillDelete(ev, 'todos') }
|
|
];
|
|
});
|
|
|
|
// ── series pills ───────────────────────────────────────────
|
|
async function loadSerieEvents() {
|
|
const rid = props.eventRow?.recurrence_id ?? props.eventRow?.serie_id ?? null;
|
|
if (!rid) {
|
|
serieEvents.value = [];
|
|
return;
|
|
}
|
|
serieLoading.value = true;
|
|
try {
|
|
const { data: rule, error: ruleErr } = await supabase.from('recurrence_rules').select('*').eq('id', rid).maybeSingle();
|
|
if (ruleErr) throw ruleErr;
|
|
|
|
const { data: excData } = await supabase.from('recurrence_exceptions').select('original_date, type, reason').eq('recurrence_id', rid);
|
|
const exMap = new Map((excData || []).map((e) => [e.original_date, e]));
|
|
|
|
const { data: realData } = await supabase
|
|
.from('agenda_eventos')
|
|
.select('id, inicio_em, fim_em, status, recurrence_date')
|
|
.eq('recurrence_id', rid)
|
|
.is('mirror_of_event_id', null)
|
|
.order('inicio_em', { ascending: true });
|
|
const realMap = new Map((realData || []).map((e) => [e.recurrence_date || e.inicio_em?.slice(0, 10), e]));
|
|
|
|
const dates = rule ? generateRuleDates(rule) : [];
|
|
const startTime = rule?.start_time || '00:00:00';
|
|
const durMin = rule?.duration_min || 50;
|
|
|
|
const list = dates.map((dateISO) => {
|
|
const real = realMap.get(dateISO);
|
|
const exc = exMap.get(dateISO);
|
|
const isCancelled = exc?.type === 'cancel_session' || exc?.type === 'holiday_block';
|
|
const inicioStr = real?.inicio_em || `${dateISO}T${startTime}`;
|
|
const fimDate = new Date(`${dateISO}T${startTime}`);
|
|
fimDate.setMinutes(fimDate.getMinutes() + durMin);
|
|
const fimStr = real?.fim_em || fimDate.toISOString();
|
|
return {
|
|
id: real?.id || null,
|
|
inicio_em: inicioStr,
|
|
fim_em: fimStr,
|
|
status: real?.status || (isCancelled ? 'cancelado' : 'agendado'),
|
|
recurrence_date: dateISO,
|
|
_status: real?.status || (isCancelled ? 'cancelado' : 'agendado'),
|
|
_is_virtual: !real?.id,
|
|
_cancelled: isCancelled,
|
|
_reason: exc?.reason || null
|
|
};
|
|
});
|
|
|
|
for (const [dateISO, real] of realMap) {
|
|
if (!dates.includes(dateISO)) {
|
|
list.push({
|
|
id: real.id,
|
|
inicio_em: real.inicio_em,
|
|
fim_em: real.fim_em,
|
|
status: real.status || 'agendado',
|
|
recurrence_date: dateISO,
|
|
_status: real.status || 'agendado',
|
|
_is_virtual: false,
|
|
_cancelled: false,
|
|
_reason: null
|
|
});
|
|
}
|
|
}
|
|
|
|
list.sort((a, b) => new Date(a.inicio_em) - new Date(b.inicio_em));
|
|
serieEvents.value = list;
|
|
} catch (e) {
|
|
console.error('[serie] erro ao carregar:', e);
|
|
serieEvents.value = [];
|
|
} finally {
|
|
serieLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// ── occurrence financial record loader ────────────────────
|
|
async function loadOccFinancialRecord() {
|
|
occFinancialRecord.value = null;
|
|
if (!props.occurrenceMode) return;
|
|
const evId = props.eventRow?.id;
|
|
if (!evId) return;
|
|
occFinancialLoading.value = true;
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('financial_records')
|
|
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
|
.eq('agenda_evento_id', evId)
|
|
.in('status', ['pending', 'paid', 'overdue'])
|
|
.order('created_at', { ascending: false })
|
|
.limit(1)
|
|
.maybeSingle();
|
|
if (error) throw error;
|
|
occFinancialRecord.value = data ?? null;
|
|
} catch (e) {
|
|
console.warn('[occurrence] erro ao carregar financial_record:', e?.message);
|
|
occFinancialRecord.value = null;
|
|
} finally {
|
|
occFinancialLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// sessionPaymentRecord loader (2026-05-18): mesma query, sem guard
|
|
// de occurrenceMode. Alimenta a linha "Cobrança" do Resumo do dialog
|
|
// em qualquer edit de sessão (Melissa/Rail/Clínica) com eventRow.id.
|
|
async function loadSessionPaymentRecord() {
|
|
sessionPaymentRecord.value = null;
|
|
const evId = props.eventRow?.id;
|
|
if (!evId) return;
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('financial_records')
|
|
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
|
.eq('agenda_evento_id', evId)
|
|
.in('status', ['pending', 'paid', 'overdue'])
|
|
.order('created_at', { ascending: false })
|
|
.limit(1)
|
|
.maybeSingle();
|
|
if (error) throw error;
|
|
sessionPaymentRecord.value = data ?? null;
|
|
} catch (e) {
|
|
console.warn('[session-payment] erro ao carregar financial_record:', e?.message);
|
|
sessionPaymentRecord.value = null;
|
|
}
|
|
}
|
|
|
|
function onPillEditClick(ev) {
|
|
emit('editSeriesOccurrence', {
|
|
id: ev.id,
|
|
recurrence_date: ev.recurrence_date,
|
|
inicio_em: ev.inicio_em,
|
|
fim_em: ev.fim_em,
|
|
is_virtual: ev._is_virtual
|
|
});
|
|
}
|
|
|
|
function onPillStatusChange(ev) {
|
|
emit('updateSeriesEvent', {
|
|
id: ev.id,
|
|
status: ev._status,
|
|
recurrence_date: ev.recurrence_date,
|
|
inicio_em: ev.inicio_em,
|
|
fim_em: ev.fim_em,
|
|
is_virtual: ev._is_virtual
|
|
});
|
|
if (ev._is_virtual) {
|
|
setTimeout(() => loadSerieEvents(), 700);
|
|
}
|
|
}
|
|
|
|
function onPillDeleteClick(ev, event) {
|
|
pillDeleteTarget.value = ev;
|
|
nextTick(() => pillDeleteMenuRef.value?.toggle(event));
|
|
}
|
|
|
|
function onPillDelete(ev, mode) {
|
|
const isTodos = mode === 'todos';
|
|
confirm.require({
|
|
header: isTodos ? 'Encerrar toda a série' : 'Excluir sessão recorrente',
|
|
message: isTodos
|
|
? 'Todos os agendamentos futuros da série serão removidos permanentemente. Esta sessão será mantida como avulsa. Esta ação é irreversível.'
|
|
: mode === 'este_e_seguintes'
|
|
? 'Esta sessão e todas as seguintes serão removidas. Tem certeza?'
|
|
: 'Esta sessão será cancelada. Tem certeza?',
|
|
icon: isTodos ? 'pi pi-trash' : 'pi pi-exclamation-triangle',
|
|
acceptClass: 'p-button-danger',
|
|
acceptLabel: isTodos ? 'Sim, encerrar série' : 'Confirmar',
|
|
rejectLabel: 'Cancelar',
|
|
accept: () =>
|
|
emit('delete', {
|
|
id: ev.id,
|
|
editMode: mode,
|
|
recurrence_id: props.eventRow?.recurrence_id ?? props.eventRow?.serie_id ?? null,
|
|
original_date: ev.recurrence_date || (ev.inicio_em ? ev.inicio_em.slice(0, 10) : null),
|
|
serie_id: props.eventRow?.serie_id ?? null
|
|
})
|
|
});
|
|
}
|
|
|
|
// ── slot selection ─────────────────────────────────────────
|
|
function selectSlot(hhmm) {
|
|
const [h, m] = String(hhmm).split(':').map(Number);
|
|
const d = new Date();
|
|
d.setHours(h, m, 0, 0);
|
|
composer.startTimeDate.value = d;
|
|
}
|
|
|
|
// ── quick-creates ──────────────────────────────────────────
|
|
function openServiceQuickCreate() {
|
|
serviceQuickDlgOpen.value = true;
|
|
}
|
|
async function onServiceCreated(svc) {
|
|
await loadServices(props.ownerId);
|
|
if (svc?.id) {
|
|
const list = services?.value;
|
|
const fresh = (Array.isArray(list) ? list.find((s) => s.id === svc.id) : null) || svc;
|
|
if (typeof pickerBilling.addItem === 'function') {
|
|
pickerBilling.addItem(fresh);
|
|
}
|
|
}
|
|
}
|
|
|
|
function openInsuranceQuickCreate() {
|
|
insuranceQuickDlgOpen.value = true;
|
|
}
|
|
async function onInsuranceCreated(plan) {
|
|
await loadInsurancePlans(props.planOwnerId || props.ownerId);
|
|
if (plan?.id) {
|
|
composer.form.value.insurance_plan_id = plan.id;
|
|
}
|
|
}
|
|
|
|
// Quick-create de procedimento (insurance_plan_services) — inline,
|
|
// sem sair do dialog. Trigger no card Sessao/Honorarios quando o
|
|
// convenio selecionado nao tem procedimentos ou quando user quer
|
|
// adicionar mais. Apos criar, recarrega os planos pra refletir no
|
|
// computed planServices.
|
|
function openPlanServiceQuickCreate() {
|
|
if (!composer.form.value.insurance_plan_id) return;
|
|
planServiceQuickDlgOpen.value = true;
|
|
}
|
|
async function onPlanServiceCreated(service) {
|
|
await loadInsurancePlans(props.planOwnerId || props.ownerId);
|
|
// Auto-seleciona o procedimento recem-criado se o user nao
|
|
// tinha nenhum selecionado ainda (caso comum: convenio sem
|
|
// procedimentos -> cadastra o primeiro -> ja entra selecionado).
|
|
if (service?.id && !pickerBilling.selectedPlanService.value) {
|
|
pickerBilling.selectedPlanService.value = service.id;
|
|
pickerBilling.onProcedureSelect(service.id);
|
|
}
|
|
}
|
|
|
|
// ── lembrete WhatsApp manual (8.2) ─────────────────────────
|
|
async function onSendManualReminder() {
|
|
if (!composer.form.value?.id) return;
|
|
confirm.require({
|
|
header: 'Enviar lembrete WhatsApp?',
|
|
message: `Vou mandar o template "lembrete_sessao" pra ${composer.form.value.paciente_nome || 'o paciente'} agora. Pode disparar?`,
|
|
icon: 'pi pi-whatsapp',
|
|
acceptLabel: 'Enviar',
|
|
rejectLabel: 'Cancelar',
|
|
accept: async () => {
|
|
sendingReminder.value = true;
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke('send-session-reminder-manual', {
|
|
body: { event_id: composer.form.value.id }
|
|
});
|
|
if (error || !data?.ok) {
|
|
const err = data?.error || error?.message || 'unknown_error';
|
|
let friendly = err;
|
|
if (err === 'no_phone') friendly = 'Paciente sem telefone cadastrado.';
|
|
else if (err === 'invalid_phone') friendly = 'Telefone do paciente inválido.';
|
|
else if (err === 'no_active_channel') friendly = 'Nenhum canal WhatsApp ativo. Configure em Configurações → WhatsApp.';
|
|
else if (err === 'template_not_found') friendly = 'Template "lembrete_sessao" não encontrado. Configure em Configurações → WhatsApp.';
|
|
else if (err === 'forbidden') friendly = 'Você não tem permissão pra enviar por este canal.';
|
|
else if (String(err).startsWith('send_failed')) friendly = 'Não conseguimos enviar. Verifique a conexão do WhatsApp.';
|
|
throw new Error(friendly);
|
|
}
|
|
toast.add({ severity: 'success', summary: 'Lembrete enviado', detail: data.to ? `Para ${data.to}` : undefined, life: 3500 });
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Erro ao enviar lembrete', detail: e.message, life: 5000 });
|
|
} finally {
|
|
sendingReminder.value = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── watchers ───────────────────────────────────────────────
|
|
// Init form ao abrir o dialog (orquestra tudo)
|
|
watch(
|
|
() => props.modelValue,
|
|
async (open) => {
|
|
if (!open) return;
|
|
await nextTick();
|
|
|
|
actions._skipStatusWatch.value = true;
|
|
composer.form.value = composer.resetForm();
|
|
await nextTick();
|
|
actions._skipStatusWatch.value = false;
|
|
actions.samePatientConflict.value = null;
|
|
composer.recorrenciaType.value = 'avulsa';
|
|
composer.diasSelecionados.value = [];
|
|
composer.dataLimiteManual.value = null;
|
|
composer.qtdSessoesMode.value = '4';
|
|
composer.qtdSessoesCustom.value = 12;
|
|
composer.editScope.value = 'somente_este';
|
|
if (serieValorMode) serieValorMode.value = 'multiplicar';
|
|
|
|
if (composer.isEdit.value && composer.form.value.paciente_id && !composer.form.value.paciente_nome) {
|
|
supabase
|
|
.from('patients')
|
|
.select('id, nome_completo')
|
|
.eq('id', composer.form.value.paciente_id)
|
|
.maybeSingle()
|
|
.then(({ data }) => {
|
|
if (data?.nome_completo) composer.form.value.paciente_nome = data.nome_completo;
|
|
});
|
|
}
|
|
|
|
if (composer.hasSerie.value) loadSerieEvents();
|
|
else serieEvents.value = [];
|
|
|
|
// occurrenceMode: carrega financial_record desta ocorrencia
|
|
// pra decidir se o card Sessao/Honorarios fica locked (cobranca
|
|
// ja emitida) ou unlocked (sem cobranca, edicao livre).
|
|
loadOccFinancialRecord();
|
|
|
|
// sessionPaymentRecord: carrega em qualquer edit (Melissa
|
|
// tambem) pra alimentar a linha "Cobrança" do Resumo lateral.
|
|
loadSessionPaymentRecord();
|
|
|
|
// occurrenceMode: editando UMA ocorrencia de serie ja existente —
|
|
// tipo de compromisso ja foi escolhido (paciente + sessao). Pular
|
|
// step 1 incondicionalmente. Defesa em camadas: useMelissaAgenda
|
|
// ja seta is_occurrence=true na row (faz isEdit=true), mas se outro
|
|
// call site esquecer essa flag o guard aqui salva.
|
|
if (props.occurrenceMode || composer.isEdit.value) {
|
|
composer.step.value = 2;
|
|
} else {
|
|
const preset = props.presetCommitmentId;
|
|
if (preset) {
|
|
composer.form.value.commitment_id = preset;
|
|
composer.step.value = 2;
|
|
} else composer.step.value = 1;
|
|
}
|
|
|
|
pickerBilling.clearPatientsCache();
|
|
if (composer.requiresPatient.value) pickerBilling.loadPatients(true);
|
|
|
|
pickerBilling.ensureServicesLoaded();
|
|
const insuranceOwner = props.planOwnerId || props.ownerId;
|
|
if (insuranceOwner) {
|
|
await loadInsurancePlans(insuranceOwner);
|
|
}
|
|
|
|
selectedPlanService.value = null;
|
|
actions._restoringConvenio.value = false;
|
|
|
|
commitmentItems.value = [];
|
|
servicePickerSel.value = null;
|
|
if (composer.isEdit.value && (composer.form.value.id || props.eventRow?.recurrence_id)) {
|
|
pickerBilling._loadCommitmentItemsForEvent(composer.form.value.id);
|
|
} else {
|
|
composer.billingType.value = 'particular';
|
|
}
|
|
}
|
|
);
|
|
|
|
// Tenant/scope mudou — recarrega lista de pacientes
|
|
watch(
|
|
() => [props.tenantId, props.restrictPatientsToOwner, props.patientScopeOwnerId],
|
|
() => {
|
|
if (!composer.visible.value) return;
|
|
pickerBilling.clearPatientsCache();
|
|
if (composer.requiresPatient.value) pickerBilling.loadPatients(true);
|
|
}
|
|
);
|
|
|
|
// Solicitação pendente do agendador público no horário escolhido
|
|
watch(
|
|
() => [composer.form.value.dia?.toString(), composer.form.value.startTime],
|
|
async ([dia, startTime]) => {
|
|
solicitacaoPendente.value = null;
|
|
if (!composer.isSessionEvent.value || !composer.visible.value || composer.isEdit.value) return;
|
|
if (!props.ownerId || !dia || !startTime) return;
|
|
|
|
const d = new Date(composer.form.value.dia);
|
|
const isoDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
const { data } = await supabase
|
|
.from('agendador_solicitacoes')
|
|
.select('id, paciente_nome, paciente_sobrenome, paciente_email')
|
|
.eq('owner_id', props.ownerId)
|
|
.eq('status', 'pendente')
|
|
.eq('data_solicitada', isoDate)
|
|
.eq('hora_solicitada', startTime)
|
|
.maybeSingle();
|
|
solicitacaoPendente.value = data || null;
|
|
}
|
|
);
|
|
|
|
// Online slots: depende de [dia, modalidade]
|
|
watch(
|
|
[() => composer.form.value.dia, () => composer.form.value.modalidade],
|
|
async ([dia, mod]) => {
|
|
if (mod !== 'online' || !dia || !props.ownerId) {
|
|
onlineSlots.value = [];
|
|
return;
|
|
}
|
|
const dow = new Date(dia).getDay();
|
|
loadingOnlineSlots.value = true;
|
|
try {
|
|
const { data } = await supabase
|
|
.from('agenda_online_slots')
|
|
.select('time')
|
|
.eq('owner_id', props.ownerId)
|
|
.eq('weekday', dow)
|
|
.eq('enabled', true)
|
|
.order('time');
|
|
onlineSlots.value = (data || []).map((s) => ({ hhmm: String(s.time || '').slice(0, 5) }));
|
|
} catch {
|
|
onlineSlots.value = [];
|
|
} finally {
|
|
loadingOnlineSlots.value = false;
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
return {
|
|
// refs
|
|
solicitacaoPendente,
|
|
onlineSlots,
|
|
loadingOnlineSlots,
|
|
serieLoading,
|
|
pillDeleteMenuRef,
|
|
pillDeleteTarget,
|
|
sendingReminder,
|
|
serviceQuickDlgOpen,
|
|
insuranceQuickDlgOpen,
|
|
planServiceQuickDlgOpen,
|
|
occFinancialRecord,
|
|
occFinancialLoading,
|
|
sessionPaymentRecord,
|
|
// computeds
|
|
serieCountByStatus,
|
|
pillDeleteMenuItems,
|
|
// series
|
|
loadSerieEvents,
|
|
loadOccFinancialRecord,
|
|
loadSessionPaymentRecord,
|
|
onPillEditClick,
|
|
onPillStatusChange,
|
|
onPillDeleteClick,
|
|
onPillDelete,
|
|
// slot
|
|
selectSlot,
|
|
// quick-creates
|
|
openServiceQuickCreate,
|
|
onServiceCreated,
|
|
openInsuranceQuickCreate,
|
|
onInsuranceCreated,
|
|
openPlanServiceQuickCreate,
|
|
onPlanServiceCreated,
|
|
// reminder
|
|
onSendManualReminder
|
|
};
|
|
}
|