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:
Leonardo
2026-05-19 20:54:23 -03:00
parent c23d0a574f
commit 1feb7112ff
8 changed files with 530 additions and 95 deletions
@@ -965,12 +965,15 @@ const occBillingStatusSeverity = computed(() => {
// da cobrança (Pago / Pendente / Atrasada / Sem cobrança). Espelha
// os 3 canais visuais da agenda (verde pago / amber pendente /
// neutro). Sources:
// - sessionPaymentRecord (1 query em useAgendaEventLifecycle)
// - occFinancialRecord (lifecycle, agora carrega em ambos modos —
// guard de occurrenceMode foi removido em 2026-05-19 pra ativar
// lock-edit em Melissa tambem). Cobre record direto OU sintetizado
// a partir de contrato upfront ativo.
// - eventRow.price (fallback pra "Sem cobrança · R$ X" quando
// ainda nao ha record)
const paymentSummary = computed(() => {
if (!isSessionEvent.value) return null;
const rec = sessionPaymentRecord.value;
const rec = occFinancialRecord.value;
const fmtDate = (d) => {
if (!d) return '';
try {
@@ -1770,16 +1773,22 @@ onBeforeUnmount(() => {
/>
</div>
<div class="field-card__body">
<!-- LOCKED: cobranca ja emitida -->
<!-- LOCKED: cobranca ja emitida.
AgendaEventoFinanceiroPanel renderiza quando
o record é REAL (não sintetizado de contrato
upfront em sibling). Sintetizado: a sessão não
tem record próprio, então o panel exibiria
"Gerar cobrança" indevidamente. O lock Message
comunica o estado. -->
<template v-if="occFinancialRecord">
<Message severity="info" :closable="false" class="m-2">
<div class="text-sm">
<i class="pi pi-lock mr-1" />
Cobrança de <b>{{ fmtBRL(occFinancialRecord.final_amount ?? occFinancialRecord.amount) }}</b> emitida.
Cobrança de <b>{{ fmtBRL(occFinancialRecord.final_amount ?? occFinancialRecord.amount) }}</b> {{ occFinancialRecord._synthesized ? 'do pacote' : 'já emitida' }}.
Para alterar tipo ou serviços, ajuste a cobrança no Financeiro abaixo.
</div>
</Message>
<AgendaEventoFinanceiroPanel :evento="eventRow" class="m-3" @cobranca-atualizada="loadOccFinancialRecord" />
<AgendaEventoFinanceiroPanel v-if="!occFinancialRecord._synthesized" :evento="eventRow" class="m-3" @cobranca-atualizada="loadOccFinancialRecord" />
</template>
<!-- LOADING -->
@@ -2204,7 +2213,13 @@ onBeforeUnmount(() => {
<!-- Mobile: label visível; Desktop: label some e Select
ocupa o espaço. Mesmo padrão do Paciente e Data/Horário. -->
<span class="md:hidden">Sessão / Honorários</span>
<!-- Lock-edit (Fase 6, ativada em Melissa 2026-05-19):
quando cobrança paga/pendente OU contrato upfront
ativo, mostra Tag em vez do Select pra impedir
troca de billingType. -->
<Tag v-if="occFinancialRecord" :value="occBillingStatusLabel" :severity="occBillingStatusSeverity" class="ml-auto" />
<Select
v-else-if="!occFinancialLoading"
v-model="billingType"
:options="billingTypeOptions"
optionLabel="label"
@@ -2213,7 +2228,20 @@ onBeforeUnmount(() => {
class="aed-pay-mod-select ml-auto md:ml-0"
/>
</div>
<Transition name="aed-pay-expand">
<!-- LOCKED: cobrança emitida (real OU sintetizada via
contrato upfront pago/pendente). Trava edição de
tipo/serviços/preço. Ajustes via Financeiro. -->
<template v-if="occFinancialRecord">
<Message severity="info" :closable="false" class="m-2">
<div class="text-sm">
<i class="pi pi-lock mr-1" />
Cobrança de <b>{{ fmtBRL(occFinancialRecord.final_amount ?? occFinancialRecord.amount) }}</b> {{ occFinancialRecord._synthesized ? 'do pacote' : 'já emitida' }}.
Para alterar tipo ou serviços, ajuste a cobrança no Financeiro abaixo.
</div>
</Message>
<AgendaEventoFinanceiroPanel v-if="!occFinancialRecord._synthesized && isEdit && eventRow?.id" :evento="eventRow" class="m-3" @cobranca-atualizada="loadOccFinancialRecord" />
</template>
<Transition v-else-if="!occFinancialLoading" name="aed-pay-expand">
<div class="field-card__body aed-pay-body">
<!-- Gratuito: aviso informativo, sem ações -->
<div v-if="billingType === 'gratuito'" class="aed-pay-gratuito">
@@ -2273,10 +2301,15 @@ onBeforeUnmount(() => {
</div>
</Transition>
<!-- Cobrança da sessão (financial_records ja criado)
em edição. Movido pra dentro do Sessao/Honorarios
em 2026-05-11 (antes ficava num side-card separado). -->
<AgendaEventoFinanceiroPanel v-if="isSessionEvent && isEdit && eventRow?.id" :evento="eventRow" class="m-3" @cobranca-atualizada="loadSessionPaymentRecord" />
<!-- UNLOCKED: panel pra gerar cobrança inline (atalho do
popover é o caminho principal, mas mantido aqui pra
paridade com flow anterior). aparece sem record. -->
<AgendaEventoFinanceiroPanel
v-if="!occFinancialRecord && !occFinancialLoading && isEdit && eventRow?.id"
:evento="eventRow"
class="m-3"
@cobranca-atualizada="loadOccFinancialRecord"
/>
<!-- Botão "Ver lançamentos" (2026-05-14): abre dialog
listando todos os financial_records vinculados à