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:
@@ -881,31 +881,58 @@ async function confirmAnteciparPagamento() {
|
||||
async function onVerLancamentos() {
|
||||
const ev = eventoSelecionado.value;
|
||||
if (!ev?.id) return;
|
||||
// Ocorrência virtual ainda não foi materializada — id é sintético
|
||||
// `rec::<rule>::<date>`, não bate com agenda_evento_id (uuid).
|
||||
// Aborta sem query e avisa o user. 2026-05-14.
|
||||
// Ocorrência virtual: id é sintético `rec::<rule>::<date>`, não bate com
|
||||
// agenda_evento_id (uuid). Mas se a virtual pertence a uma série com
|
||||
// contrato upfront, os records do contrato (linkados a sibling
|
||||
// materializada) cobrem ela — busca via recurrence_id.
|
||||
const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::');
|
||||
if (ev.is_occurrence || isVirtualId) {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Sem lançamentos ainda',
|
||||
detail: 'Esta ocorrência ainda não foi materializada. Lançamentos aparecem após a primeira ação na sessão (status, edição etc).',
|
||||
life: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
const isVirtual = ev.is_occurrence || isVirtualId;
|
||||
|
||||
lancamentosEventoTitulo.value = ev.pacienteNome || ev.label || ev.titulo || 'Sessão';
|
||||
lancamentosDialogOpen.value = true;
|
||||
lancamentosLoading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
|
||||
.eq('agenda_evento_id', ev.id)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: true });
|
||||
if (error) throw error;
|
||||
lancamentosList.value = data || [];
|
||||
let records = [];
|
||||
if (isVirtual && ev.recurrence_id) {
|
||||
// Pega records de QUALQUER sibling materializada (cobre o caso
|
||||
// pacote upfront onde só a 1ª tem record do pacote inteiro).
|
||||
const { data: siblings } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', ev.recurrence_id);
|
||||
const ids = (siblings || []).map((s) => s.id);
|
||||
if (ids.length) {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
|
||||
.in('agenda_evento_id', ids)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: true });
|
||||
if (error) throw error;
|
||||
records = data || [];
|
||||
}
|
||||
} else if (!isVirtual) {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
|
||||
.eq('agenda_evento_id', ev.id)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: true });
|
||||
if (error) throw error;
|
||||
records = data || [];
|
||||
}
|
||||
lancamentosList.value = records;
|
||||
if (!records.length && isVirtual) {
|
||||
// Caso virtual sem records de contrato (saldo, sem pacote): fecha
|
||||
// o dialog e avisa que ainda não materializou (comportamento antigo).
|
||||
lancamentosDialogOpen.value = false;
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Sem lançamentos ainda',
|
||||
detail: 'Esta ocorrência ainda não foi materializada. Lançamentos aparecem após a primeira ação na sessão (status, edição etc).',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'warn', summary: 'Erro', detail: e?.message || 'Falha ao carregar lançamentos.', life: 4000 });
|
||||
lancamentosList.value = [];
|
||||
|
||||
Reference in New Issue
Block a user