# HANDOFF — 2026-05-19 madrugada (C1-C8 ✅ + UI saldo + Usar/Revogar, próximo C9) Documento de continuidade. **Quando voltar, comece lendo esta página até o fim.** > **🎯 SE A FORÇA CAIR / SESSÃO PERDER CONTEXTO:** estamos na rodada de > testes manuais dos 13 cenários do doc viva > `src/docs/agenda-compromisso-financeiro-cenarios.html`. **C1-C8 ✅**. > Próximo: **Cenário 9** (Michael Balint · per_session · 12 × R$ 150 — > materializa todas 12 sessões + cria 12 records pendentes upfront). > **🟢 WORKING TREE LIMPO** após commit/push de 19/05 madrugada. UI de > pacote saldo completa (info card violeta + botão Usar/Revogar atômico > que materializa+realiza+cobra ou desfaz). Backfill de > determined_commitment_id em revogar+usar (evita campo "Título" indevido > em sessões legadas). --- ## 🔴 PRÓXIMO PASSO IMEDIATO — Cenário 9 (Per-session) | Campo | Valor | |---|---| | Paciente | **Michael Balint** | | Frequência | **Semanal · 12 ocorrências** | | Serviço | Sessão R$ 150 | | Cobrança ao salvar | **1 por sessão** (chargeMode=per_session) | **Esperado:** - 1 row em `recurrence_rules` - **12 rows em `agenda_eventos`** (TODAS materializadas — diferença chave vs C6-C8) - 0 rows em `billing_contracts` (per_session NÃO usa contrato) - **12 rows em `financial_records`** (1 por sessão, todos pending, amount=150 cada) - Agenda: 12 sessões com **badge $ amber** em todas (cada uma tem seu record próprio) - Popover de cada uma: linha amber "A receber R$ 150,00 (cobrança pendente)" Diferença chave vs cenários anteriores: - C6 (none): 1 row materializada + 0 records - C7 (upfront): 1 row + 1 record (R$ totalPacote) - C8 (saldo): 0 rows + 0 records (modelo Cliniko, geram on-the-fly via Usar) - **C9 (per_session): N rows + N records pré-criados** Após C9: C10-C13 (status change + edit cobrada). Quando todos passarem, replicar em **Rail** (`AgendaTerapeutaPage.vue`) e **Clínica** (`AgendaClinicaPage.vue`). --- ## 📦 O que foi feito em 19/05 madrugada (C8 + Usar/Revogar saldo + UI de pacote) ### Cenário 8 ✅ (Pacote SALDO — Otávio Souza Ferreira 12 × R$ 50) Testado e validado. Contract criado com `charging_style='saldo'`, 0 events materializadas, 0 records. Modelo Cliniko: sessões materializam on-demand via Usar. ### UI do pacote (saldo + upfront) - **`_ruleContractMap`** em useMelissaAgenda: bulk-load agora popula contract info (id, style, totalSessions, sessionsUsed, packagePrice) por `recurrence_id`. Query usa `recurrence_rules.patient_id` como fonte autoritativa (cobre saldo sem materializadas). - **Normalize** injeta `contract` no evento → popover acessa via `ev.contract`. - **Popover** (`MelissaEventoPanel`): nova linha colorida abaixo do payment: - Saldo: violeta `"Pacote saldo · N/M usadas"` + botão verde **"Usar"** (paymentState=none) OU vermelho **"Revogar"** (paymentState=pending) - Upfront: verde `"Pacote · N/M realizadas"` (sem botão; cobrança já tratada) - **AgendaEventDialog**: info card mt-4 (saldo violeta / upfront emerald) com header (pacote+contador), body (total/per-session/restam), botão "Usar agora" ou "Revogar uso", hint explicando o modelo. Gateado por `occFinancialLoading` (spinner durante carga) pra evitar piscar entre estados. ### Handlers Usar/Revogar atômicos **`onUsarSessao`** em MelissaLayout (aceita payload do popover OU do dialog): 1. Materializa virtual se necessário (preserva `determined_commitment_id` da regra) 2. Status='realizado' + link `billing_contract_id` 3. `create_financial_record_for_session` RPC com per-session amount 4. Incrementa `billing_contracts.sessions_used` 5. Se atingiu total → contract `status='completed'` 6. Toast verde + fecha popover/dialog **`onRevogarSessao`** desfaz tudo: 1. Cancela financial_record (status='cancelled') 2. Decrementa sessions_used (não fica negativo) 3. Reativa contract se estava completed 4. Status volta pra 'agendado' 5. Bloqueia se record já está `paid` (precisa estorno formal pelo Financeiro) 6. **Backfill** de `determined_commitment_id` se NULL (fix de legado) ### Fix: enum status_evento_agenda Era `'realizada'` no insert/update, DB exige `'realizado'` (masculino). Corrigido em todas as ocorrências. ### Fix: campo "Título" indevido no dialog Sessão sem `determined_commitment_id` → `selectedCommitment=null` → `isSessionEvent=false` → mostra campo Título (que é só pra não-sessão). Fix: - Materialize do Usar inclui `determined_commitment_id` da regra - Update path do Usar (sessão real após revogar) backfilla via query da rule - Revogar também backfilla — garante consistência mesmo sem novo Usar - SQL massivo de backfill disponível no HANDOFF pra limpar rows legadas ### Fix: "Gerar fatura" não cabe em sessão de saldo Hide do botão "Gerar fatura" no popover quando há `contractInfo`. Geraria cobrança solta sem incrementar saldo → duplicação. Fluxo correto: usar "Usar". ### Recorrências Aplicadas: cores + badges - Header stats: total **azul**, realizadas **verde**, faltaram **amber**, canceladas **cinza**, remarcadas **violeta** - Pills: badge sólido por status (Realizado=emerald-600, Faltou=amber-600, Cancelado=stone-500, Remarcado=violet-600) ### Race condition no dialog - AgendaEventDialog mostrava botões "Usar"/"Revogar" baseado em `occFinancialRecord` que carrega async - Durante load (~500ms), botão errado podia aparecer → snap pro correto depois - Fix: spinner "Verificando estado…" enquanto `occFinancialLoading=true`; botões só renderizam após - Popover decidiu manter como está (race window pequena, fechar/reabrir resolve) --- ## 📦 O que foi feito em 19/05 noite (C7 + lock-edit + propagação cross-week) ### Cenário 7 ✅ (Pacote UPFRONT — Ana Souza Ferreira) Testado e validado. Usuária criou Ana, R$ 200/sessão × 4 = R$ 800, marcou como pago em dinheiro pelo Financeiro. Visualização correta em mês AND em semana navegando pelas 4 semanas. ### Fase 6 (lock-edit cobrada) ativada em Melissa Antes: `loadOccFinancialRecord` tinha guard `if (!props.occurrenceMode) return;` — só carregava em Rail/Clínica (edição de ocorrência). Em Melissa, `sessionPaymentRecord` paralelo alimentava só o Resumo lateral, sem trigger de lock. Agora unificado: `occFinancialRecord` carrega em ambos modos: - Card Sessão / Honorários ganha **Tag** (em vez de Select billingType) quando há cobrança - Body do card mostra **Message "Cobrança de R$ X já emitida"** + cadeado - Tipo de cobrança (Particular/Convênio/Gratuito) bloqueado - Edição de serviços/preço bloqueada ### Propagação cross-week de pacote upfront pago/pendente **Bug descoberto durante C7:** ao navegar pra semanas futuras (onde só virtual da Ana 2/3/4 aparecia, sem real event paid na view), o `_rulePaymentMap` era zerado pelo else branch do bulk-load → virtuais perdiam estado paid. Fix em `useMelissaAgenda.js _reloadRange`: - Maps (paymentStateMap, amountMap, rulePaymentMap) inicializados SEMPRE no início - Propagação agora roda **independente de realIds.length** (ie, mesmo em semanas só-com-virtuais) - Coleta `ruleIdsInView` de TODOS eventos da view (reais + virtuais com recurrence_id) - Cross-week query: pra cada rule em view, busca QUALQUER evento sibling (inclusive em outras semanas) + seus records paid/pending → determina estado do contrato - Propaga estado pra eventos reais (via map) + virtuais (via rulePaymentMap acessado pelo normalize) ### Atalho "Gerar fatura" no popover - Pill amber pequeno ao lado de "A cobrar R$ X" no popover (`paymentVariant === 'none' && !is_occurrence`) - Click → `gerarCobrancaManual` direto, fecha popover pra impedir double-click - Tooltip: "Gerar fatura agora" ### 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 faz hard delete: `financial_records` pendentes → `agenda_eventos` materializados → `recurrence_rules` (CASCADE leva exceptions + rule_services) - Bloqueia se algum record tem `status='paid'` (estornar primeiro) ### RPC `create_financial_record_for_session` ignora cancelled **Migration 20260519000001:** idempotência da RPC passou a filtrar `AND status != 'cancelled'` além de `deleted_at IS NULL`. Antes: cancelar cobrança sem querer → todo "Gerar fatura" subsequente retornava o cancelado em vez de criar nova. Toast verde mentindo. Memória durável em `memory/project_rpc_idempotency_cancelled.md`. ### `cancel_session` exception some da agenda - `useRecurrence.expandRules` agora pula ocorrência com `exception.type === 'cancel_session'` (era visível com status cancelado; doc dizia "some da agenda" mas código mantinha) - `patient_missed` / `therapist_canceled` / `holiday_block` permanecem visíveis como histórico ### `recurrence_exceptions` cancel idempotente - Cancel de ocorrência (virtual e materializada) usa `upsert` com `onConflict: 'recurrence_id,original_date'` — não quebra mais com unique violation quando há exception zumbi de tentativa anterior. ### Visualização paid/pending de upfront em virtuais - `MelissaEventoPanel.showPaymentRow` antes excluía virtuais incondicionalmente. Agora só esconde quando `paymentState === 'none'` (saldo/sem pacote continua limpo; upfront propagado mostra). - `MelissaAgenda.fcEvents`: removida exigência de `!is_occurrence` no `isPaidSession` e no badge $ pendente. Virtuais herdadas via propagação mostram borda verde/badge amber. ### `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. ### Confirmação 3 decisões UX (não codar) Antes de C7, user perguntou e concordou: 1. Editar serviço já lançado e pago → **NÃO** (cobrança fiscal imutável) 2. Alternar Particular/Convênio/Gratuito em série com cobrança ativa → **NÃO** (mesma razão) 3. "Gerar fatura" extra em sessão coberta por contrato upfront → **NÃO** (duplicaria cobrança) Tudo isso o lock-edit (Fase 6 ativada acima) cobre. --- ## 📦 O que foi feito em 18/05 ### Cenário 4 (Joyce · "Já recebi") ✅ - Testado e passou: toast "Cobrança paga R$ 180,00 recebido via PIX", record nasceu `paid + payment_method=pix + paid_at=now()`. ### Novo indicador: barra esquerda verde para sessão paga - Brainstorm de 6 opções; user escolheu #6 (3 canais visuais distintos por estado). - `MelissaAgenda.vue:395-419` — computa `isPaidSession` (sessão+paciente+não-virtual+`paymentState==='paid'`) e adiciona classe `ma-evt--paid` ao FC event (combina com `ma-evt--inactive-patient` se ambos). - `MelissaAgenda.vue:2325-2335` — CSS força `border-left-color: #10b981 !important` (emerald-500, 4px). `!important` necessário porque FC seta `borderColor` inline. Trata também list view (`.fc-list-event-dot`). - Doc HTML atualizado: legenda "Indicadores visuais" agora descreve **3 estados** (pendente / pago / neutro) com 3 mocks empilhados; estado-alvo do C4 reescrito mencionando a barra verde. - Decisão salva em `memory/project_agenda_payment_indicators.md`. ### Linha "Cobrança" no popover + Resumo do dialog - **Popover `MelissaEventoPanel`** — antes só mostrava amber "A receber R$ X" pra pendente. Agora cobre os 3 estados, com cor + ícone por variante: - `paid` → `pi-check-circle` verde, label **"Pago · R$ X,XX"** - `pending` → `pi-dollar` amber, label **"A receber R$ X (cobrança pendente)"** (mantido) - `none` → `pi-dollar` amber, label **"A cobrar R$ X"** ou **"Cobrança ainda não gerada"** (mantido) - CSS reescrito em 3 modificadores `.evento-row--pay-{paid|pending|none}` (com dark mode). - **Resumo lateral do `AgendaEventDialog`** — nova linha entre `pi-clock` e `pi-map-marker` em ambas as cópias (mobile inline + desktop floating). - Novo ref `sessionPaymentRecord` em `useAgendaEventLifecycle.js:104+` (sem guard de `occurrenceMode`, contrário ao `occFinancialRecord` que continua só pra Rail/Clínica). Loader `loadSessionPaymentRecord` chamado no mesmo lifecycle. - Computed `paymentSummary` em `AgendaEventDialog.vue:951+` retorna `{icon, cls, label}` pra 5 casos: paid (verde + paid_at), overdue (vermelho + due_date), pending (amber + due_date), sem cobrança c/ valor (neutro), sem cobrança s/ valor (neutro). - `@cobranca-atualizada` do `AgendaEventoFinanceiroPanel` agora também dispara `loadSessionPaymentRecord` pra a linha refrescar. - **Importante:** `occFinancialRecord` (que aciona lock-edit) NÃO foi tocado de propósito — esse é território da Fase 6/C13 (Edit cobrada). Manter dois refs separados evita ativar lock prematuro em Melissa. ### Preparação do C5 (Sándor + Unimed Nacional) — UX de convênio refinado (3 issues) User tentou rodar C5 e bateu em 3 problemas seguidos. Cada um virou um fix: 1. **Botão "Cadastrar" do procedimento navegava pra `/pages/notfound`** - Root cause: `goToConveniosConfig` em `AgendaEventDialog.vue` prefixava com `/therapist` ou `/admin`, mas `/configuracoes/*` é rota **raiz** sob `AppLayout` (sibling, não filho). Em Melissa, convênios mora dentro do próprio layout via `secao: 'cfg-convenios'` (sem URL própria). - Fix descartado: o user não queria sair da agenda. Em vez disso, criamos um quick-create inline (ver #2). `goToConveniosConfig` foi removida (dead code virou armadilha). 2. **Quick-create de procedimento inline (sem sair da agenda)** - Novo componente `InsurancePlanServiceQuickCreateDialog.vue` (modelo do `InsurancePlanQuickCreateDialog`). 2 campos: nome do procedimento + valor que o convênio paga. Insere em `insurance_plan_services` pro `insurance_plan_id` ativo. - Wiring em `useAgendaEventLifecycle.js`: novo `planServiceQuickDlgOpen` + `openPlanServiceQuickCreate()` + `onPlanServiceCreated(service)`. Após criar, recarrega `loadInsurancePlans` e **auto-seleciona** o novo procedimento **só quando nada estava selecionado antes** (preserva escolha quando user já tinha selecionado X e está só cadastrando Y pra próxima). - UI refatorada (`AgendaEventDialog.vue:3110+`): a caixa cinza com botão "Cadastrar" agora aparece **sempre** que um convênio está selecionado. Quando 0 procedimentos: **"Este convênio ainda não tem procedimentos cadastrados."** Quando 1+: **"Se quiser adicionar mais procedimentos a este convênio:"**. - `planServiceQuickDlgOpen` adicionado ao `anyChildDialogOpen` pra esconder o Resumo flutuante enquanto o quick-create está aberto. 3. **Botão "+ Novo convênio" faltando em `/melissa/cfg-convenios` (e na rota canônica também)** - Root cause: `ConfiguracoesConveniosPage.vue` tinha o form de "Novo convênio" condicionado a `addingNew === true`, mas **nenhum botão setava esse flag**. Empty state mandava "Clique em 'Novo convênio'" sem botão pra clicar. - Fix: toolbar simples no topo do template `