agenda: C7 OK + Fase 6 lock-edit ativada em Melissa + cross-week payment propagation
Cenário 7 (Pacote UPFRONT — Ana Souza Ferreira 4×R$ 200 = R$ 800)
- Testado e passou. User criou Ana, pagou os R$ 800 em dinheiro pelo
Financeiro. Borda verde + popover "Pago R$ 800" funcionando.
Fase 6 (lock-edit cobrada) ativada em Melissa
- Removido guard `if (!props.occurrenceMode) return;` em
loadOccFinancialRecord (useAgendaEventLifecycle.js:217+). Agora ele
carrega em ambos modos (Rail/Clínica E Melissa)
- loadOccFinancialRecord SINTETIZA record paid/pending pra siblings de
contrato upfront ativo — assim TODAS as ocorrências da série mostram
"Cobrança paga R$ 800 do pacote" no AgendaEventDialog
- AgendaEventDialog card Sessão/Honorários (flow Melissa) ganhou lock
template: Tag em vez de Select billingType quando occFinancialRecord
existe; Message com cadeado "Cobrança de R$ X já emitida"
- AgendaEventoFinanceiroPanel só renderiza dentro do lock quando record
é REAL (não sintetizado) — evita "Gerar cobrança" indevido em sibling
- paymentSummary do Resumo lateral unificado pra usar occFinancialRecord
(em vez do sessionPaymentRecord paralelo de antes)
Cross-week propagation de pacote upfront
- BUG: ao navegar pra semana só com virtuais (sem reais), bulk-load
caía no else `_rulePaymentMap.value = {}` — virtuais perdiam estado
paid herdado
- FIX em useMelissaAgenda._reloadRange:
* Maps (payment/amount/rule) inicializados SEMPRE no início
* Propagação roda independente de realIds.length (depende só de
ruleIdsInView.size>0, considera reais E virtuais com recurrence_id)
* Query cross-week: pra cada rule em view, busca QUALQUER evento
sibling em qualquer semana + seus records pra determinar estado do
contrato. Encontra o record do pacote mesmo em outra semana
- Saldo NÃO propaga (filter: charging_style='upfront' || NULL); cada
sessão de saldo gera cobrança individual ao realizar
- Memória durável: memory/project_cross_week_propagation.md
Visualização de virtuais cobertas
- MelissaEventoPanel.showPaymentRow: virtuais só escondem quando state
='none'. Com paid/pending herdado, exibem linha colorida
- MelissaAgenda fcEvents: isPaidSession e badge $ pendente removeram
exigência de !is_occurrence. Virtuais herdadas via propagação mostram
borda verde / badge amber
Atalho "Gerar fatura" no popover
- Pill amber pequeno ao lado de "A cobrar R$ X" quando paymentVariant
='none' && !is_occurrence. Click → gerarCobrancaManual direto, fecha
popover pra impedir double-click. Tooltip: "Gerar fatura agora"
- Wire em MelissaLayout via novo emit gerar-cobranca + handler
onGerarCobrancaQuick
Info de pacote no popover
- Header agora mostra "Sessão · Pacote · N sessões" (computed
seriesLabel lê de _raw do rule)
Botão "Excluir série inteira"
- Novo emit delete-series em MelissaEventoPanel + botão ao lado de
"Excluir sessão" quando evento tem recurrence_id
- Handler onDeleteSeries em MelissaLayout: hard delete em 3 etapas
(financial_records pendentes → agenda_eventos materializados →
recurrence_rules CASCADE leva exceptions). Bloqueia se algum record
paid (estorno via Financeiro primeiro)
cancel_session some da agenda
- useRecurrence.expandRules agora pula occurrence com exception.type=
'cancel_session' (era visível com status cancelado; doc dizia que
some). patient_missed/therapist_canceled/holiday_block permanecem
como histórico
recurrence_exceptions cancel idempotente
- MelissaLayout onDeleteEvento usa upsert com onConflict pra exception
cancel — não quebra mais com unique violation em re-cancel
billing_contract_id na 1ª materializada
- _createPackageContract agora .select() o contrato após insert e seta
billing_contract_id no insert da 1ª agenda_eventos materializada
onVerLancamentos cobre virtual de upfront
- Antes virtual sempre toast "Sem lançamentos". Agora busca records via
siblings da série pra encontrar o do pacote. Saldo/sem pacote continua
com toast
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -208,23 +208,89 @@ export function useAgendaEventLifecycle({
|
||||
}
|
||||
|
||||
// ── occurrence financial record loader ────────────────────
|
||||
// Tenta 3 caminhos pra encontrar um record relevante:
|
||||
// 1) Record direto por agenda_evento_id (materializada com cobrança própria)
|
||||
// 2) Se a sessão pertence a contrato upfront PAGO, sintetiza um record
|
||||
// 'paid' com o package_price → assim TODAS as ocorrências da série
|
||||
// mostram "Cobrança paga" no dialog (cobertas pelo pacote pago).
|
||||
// 3) Senão, null → card unlocked.
|
||||
async function loadOccFinancialRecord() {
|
||||
occFinancialRecord.value = null;
|
||||
if (!props.occurrenceMode) return;
|
||||
// Guard de occurrenceMode REMOVIDO em 2026-05-19 — necessario pra
|
||||
// que o lock de "edit cobrada" (Fase 6) tambem ative em Melissa,
|
||||
// nao so em Rail/Clinica. Padrao SimplePractice: cobranca emitida
|
||||
// eh imutavel pelo dialog; ajustes via estorno no Financeiro.
|
||||
const evId = props.eventRow?.id;
|
||||
if (!evId) return;
|
||||
const ruleId = props.eventRow?.recurrence_id;
|
||||
const patientId = props.eventRow?.paciente_id || props.eventRow?.patient_id;
|
||||
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;
|
||||
// 1) Record direto (materializada que tem agenda_evento_id real)
|
||||
const isVirtualId = typeof evId === 'string' && evId.startsWith('rec::');
|
||||
if (evId && !isVirtualId) {
|
||||
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;
|
||||
if (data) {
|
||||
occFinancialRecord.value = data;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 2) Sintetiza record 'paid' quando há contrato upfront pago.
|
||||
// A 1ª materializada tem o record real; siblings (virtuais ou
|
||||
// materializadas sem cobrança individual) herdam status do
|
||||
// contrato pra UI mostrar "Cobrança paga" coerentemente.
|
||||
if (ruleId && patientId) {
|
||||
const { data: contracts } = await supabase
|
||||
.from('billing_contracts')
|
||||
.select('id, package_price, charging_style, status')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'package')
|
||||
.eq('status', 'active')
|
||||
.order('created_at', { ascending: false });
|
||||
const upfront = (contracts || []).find((c) => c.charging_style === 'upfront');
|
||||
if (upfront) {
|
||||
// Confere se há record PAGO ligado a qualquer evento do
|
||||
// mesmo recurrence_id (ou seja, contrato foi quitado).
|
||||
const { data: siblingEvents } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', ruleId);
|
||||
const ids = (siblingEvents || []).map((e) => e.id);
|
||||
if (ids.length) {
|
||||
// Pega o record mais recente NAO cancelado (paid OU
|
||||
// pending OU overdue). Pacote upfront tem 1 record
|
||||
// unico cobrindo toda a serie — qualquer status dele
|
||||
// trava as siblings (cobranca ja emitida, imutavel).
|
||||
const { data: anyRec } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.in('agenda_evento_id', ids)
|
||||
.in('status', ['paid', 'pending', 'overdue'])
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (anyRec) {
|
||||
// Sintetiza usando o package_price (não o per-session
|
||||
// que pode ter mudado depois de editar).
|
||||
occFinancialRecord.value = {
|
||||
...anyRec,
|
||||
amount: upfront.package_price,
|
||||
final_amount: upfront.package_price,
|
||||
_synthesized: true
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
occFinancialRecord.value = null;
|
||||
} catch (e) {
|
||||
console.warn('[occurrence] erro ao carregar financial_record:', e?.message);
|
||||
occFinancialRecord.value = null;
|
||||
|
||||
Reference in New Issue
Block a user