agenda: C8 OK + Usar/Revogar pacote saldo + UI de contract + ajustes UX

Cenário 8 (Pacote SALDO — Otávio Souza Ferreira 12 × R$ 50)
- Testado e passou. DB: 1 rule, 0 events, 1 contract (saldo), 0 records.
  Visual: 12 virtuais limpas no calendário.

UI de pacote (saldo + upfront)
- _ruleContractMap em useMelissaAgenda: bulk-load popula contract info
  (id, style, totalSessions, sessionsUsed, packagePrice) por
  recurrence_id. Query recurrence_rules.patient_id como fonte
  autoritativa — cobre saldo sem materializadas (sem isso, ruleToPatient
  via records vinha vazio pra saldo)
- normalize injeta `contract` no evento via ruleContractMap
- MelissaEventoPanel: nova linha colorida (violeta saldo, verde upfront)
  com "Pacote saldo · N/M usadas" ou "Pacote · N/M realizadas"
- AgendaEventDialog: info card mt-4 com header+body+hint explicando
  modelo, gateado por occFinancialLoading (spinner durante carga
  pra evitar piscar entre Usar/Revogar)

Handlers Usar/Revogar atômicos
- onUsarSessao em MelissaLayout: materializa virtual (preserva
  determined_commitment_id da regra) → status=realizado +
  billing_contract_id → create_financial_record_for_session →
  sessions_used++ → (se atingiu total) contract.status=completed
- onRevogarSessao: cancela record + sessions_used-- + reativa contract
  se estava completed + status=agendado. Bloqueia se record paid
  (precisa estorno formal pelo Financeiro)
- Ambos aceitam payload {eventRow, contract} do dialog OU fallback
  pra eventoSelecionado do popover
- Botão "Usar" verde no popover (paymentState=none) substituído por
  "Revogar" vermelho (paymentState=pending). Equivalente "Usar agora"/
  "Revogar uso" no info card do dialog

Fix enum status_evento_agenda
- 'realizada' não existe no enum — DB exige 'realizado' (masculino).
  Corrigido em todas as ocorrências do handler

Fix campo "Título" indevido em sessão
- 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 (insert
  path); update path backfilla via query da rule se NULL; Revogar
  também backfilla pra consistência

Fix "Gerar fatura" não cabe em saldo
- Botão "Gerar fatura" do popover hide quando há contractInfo. Em
  saldo, gerar fatura solta criaria cobrança duplicada sem incrementar
  sessions_used. Fluxo correto: "Usar"

Recorrências Aplicadas — UI
- Header stats coloridos: total **azul**, realizadas **verde**,
  faltaram **amber**, canceladas **cinza**, remarcadas **violeta**
- Pills com badge sólido por status (emerald-600 realizado, amber-600
  faltou, stone-500 cancelado, violet-600 remarcado)

Race condition no dialog
- AgendaEventDialog mostrava botões Usar/Revogar baseado em
  occFinancialRecord async; durante ~500ms de load, botão errado
  podia piscar. Fix: spinner "Verificando estado…" enquanto
  occFinancialLoading=true; botões só renderizam após
- Popover não fixado (race window pequena, fechar/reabrir resolve)

3 decisões UX confirmadas antes de codar
- Editar serviço pago → NÃO (cobrança fiscal imutável)
- Alternar Particular/Convênio/Gratuito em série cobrada → NÃO
- Gerar fatura individual em pacote upfront → NÃO (duplicação)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-19 23:27:20 -03:00
parent 1feb7112ff
commit fad1f4ebd4
7 changed files with 893 additions and 57 deletions
+82 -25
View File
@@ -1,47 +1,104 @@
# HANDOFF — 2026-05-19 noite (C1-C7, próximo C8 — pacote saldo)
# 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-C7 ✅**.
> Próximo: **Cenário 8** (Carl Jung · pacote SALDO · 4 × R$ 40 — modelo
> Cliniko: contrato sem cobrança imediata, cada sessão gera record ao
> ser realizada).
> `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 noite. Fase 6
> (lock-edit cobrada) ativada em Melissa também. Lock + popover atalho
> "Gerar fatura" + propagação cross-week de pacote upfront tudo
> funcionando.
> **🟢 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 8 (Pacote SALDO)
## 🔴 PRÓXIMO PASSO IMEDIATO — Cenário 9 (Per-session)
| Campo | Valor |
|---|---|
| Paciente | **Carl Jung** |
| Frequência | **Semanal · 4 ocorrências** |
| Serviço | Sessão (R$ 40 cada) |
| Cobrança ao salvar | **Pacote único** + estilo **"Saldo (Cliniko)"** |
| Total contrato | **R$ 160** (4 × 40) |
| 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`
- **0 rows em `agenda_eventos`** materializadas inicialmente (saldo NÃO materializa 1ª)
- 1 row em `billing_contracts` (type=package, charging_style=**saldo**, total_sessions=4, package_price=160)
- **0 rows em `financial_records`** (sem cobrança imediata — modelo Cliniko)
- Agenda: 4 ocorrências virtuais **TODAS LIMPAS** (sem badge $, sem barra verde) — saldo intencionalmente não propaga estado, cada sessão gera cobrança individual quando vira realidade
- **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 upfront:
- Upfront: 1 cobrança única (paga ou pendente) cobre todas as 4 sessões
- Saldo: contrato sem cobrança; cada sessão materializada GERA sua própria cobrança ao virar realidade (status realizado/faltou via flow C10-C12)
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 C8: C9 (per_session) → C10-C13 (status change + edit cobrada).
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)
@@ -224,8 +281,8 @@ User tentou rodar C5 e bateu em 3 problemas seguidos. Cada um virou um fix:
| 5 | Avulsa convênio (Sándor + Unimed) | ✅ |
| 6 | Recorrente sem pacote (Maria Magali / Anna Freud) | ✅ |
| 7 | Pacote upfront (Ana Souza Ferreira 4 × R$ 200) | ✅ |
| **8** | **Pacote saldo (Carl Jung 4 × R$ 40)** | 🔴 **PRÓXIMO** |
| 9 | 1 por sessão (Michael Balint 12 sem) | ⏳ |
| 8 | Pacote saldo (Otávio 12 × R$ 50) | ✅ |
| **9** | **1 por sessão (Michael Balint 12 × R$ 150)** | 🔴 **PRÓXIMO** |
| 10 | Status change avulsa (realizado/faltou/cancelado) | ⏳ |
| 11 | Status change pacote saldo | ⏳ |
| 12 | Antecipar pagamento (Carl Jung) | ⏳ |