AgendaClinicaPage espelha Fase C: useAgendaStatusChange composable
+ AgendaStatusChangeConfirmDialog plugado.
onUpdateSeriesEvent reescrito:
- Materializa virtual se preciso (via createClinic com status='agendado'
+ tenantId)
- updateClinic({ status }) no DB
- applyStatusChange(eventoId, row, novoStatus) ramifica via dialog
quando preciso
- loadClinicRange() refetch apos applied
Mesma feature parity de Melissa pra status change na Clinica:
multa, taxa cancelamento tardio, consumir saldo, gerar cobranca
pacote saldo, reverse transition trava — tudo via agendaBilling.service.
Fase C (Rail) + Fase D (Clinica) fechadas. Os 3 layouts (Melissa/
Rail/Clinica) agora compartilham o billing core do agendaBilling.
service via composable useAgendaStatusChange.
Pendente (residual incremental):
- Indicadores visuais (3 canais) nos 3 layouts
- Antecipar/Revogar pagamento no popover de Rail (Rail nao tem
popover separado — usa AgendaEventDialog direto; precisa
refactor maior)
- Doc de ajuda
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AgendaTerapeutaPage (Rail) ganha o fluxo de status change do
Melissa via novo composable useAgendaStatusChange (reusable
wrapper sobre agendaBilling.service).
src/features/agenda/composables/useAgendaStatusChange.js (novo):
- Composable Tipo A pra qualquer page que precise do flow
load context -> dialog se necessario -> apply decisoes
- Mantem state do dialog + resolver promise
- Expoe applyStatusChange(eventoId, row, novoStatus)
- Resolve ownerId via supabase.auth + tenantId via tenantStore
AgendaTerapeutaPage:
- onUpdateSeriesEvent refatorado: materializa virtual se preciso ->
update status -> applyStatusChange (load ctx + dialog + apply)
- AgendaStatusChangeConfirmDialog plugado no template
Antes: Rail fazia so update(id, { status }) cru — sem multa,
sem pacote, sem reverse, sem nada de C7-C13. Era a versao
primitiva do status change.
Depois: Rail tem feature parity com Melissa pra status change.
Multa por falta, taxa de cancelamento tardio, consumir saldo do
pacote, gerar cobranca de pacote saldo, reverse transition trava
— tudo via mesmo agendaBilling.service.
Pendente Fase C: indicadores visuais (3 canais) + antecipar
pagamento (popover-specific, depende refactor maior do
AgendaEventDialog ou criar Rail popover). Fica pra iter
incremental.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registra a decomposicao end-to-end (A+B1+B2) totalizando -991L
no useMelissaAgenda. 3 layouts podem agora compartilhar o billing
core.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continua decomposicao da agenda. Extrai 3 mutations:
- applyStatusDecisions (~330L — reverse, consume saldo,
multa, mark paid, generate package
charge, antecipated payment)
- createPackageContract (~140L — upfront ou saldo)
- materializeAndChargePerSession (~90L — N events + N records)
Padrao das assinaturas:
- supabase como dep explicita (em vez de closure)
- toast OPCIONAL (callsite fora de UI pode passar null;
applyStatusDecisions ramifica via `if (toast?.add)`)
- ownerId/tenantId como args (em vez de capturar refs)
createPackageContract + materializeAndChargePerSession ja retornavam
{ toast: {...} } pra caller mostrar — pattern preservado.
useMelissaAgenda.js: 2593L -> 2042L (-551L). 3 wrappers finos
injetam supabase/toast/refs do escopo do composable. Comportamento
identico — codigo movido linha-a-linha, so refactor de signature.
TOTAL nas fases A+B1+B2: -1525L extraidas do useMelissaAgenda
(de 3033L original pra 2042L atual). Tres pages (Melissa/Rail/
Clinica) agora podem reusar mesmo billing core.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continua decomposicao da agenda (apos Fase A utils). Extrai pro
service os componentes read-only / pure:
- computeSeriePrice (puro)
- generateOccurrenceDates (puro)
- loadStatusChangeContext (read-only DB — assina supabase,
ownerId, tenantId, row, eventoId,
status)
- needsStatusConfirmDialog (puro — depende so do ctx)
useMelissaAgenda.js: 2792L -> 2593L (-199L). _loadStatusChangeContext
agora e wrapper fino que injeta supabase/ownerId/tenantId do
composable scope. _needsConfirmDialog vira alias direto.
_computeSeriePrice/_generateOccurrenceDates importados direto.
Fase B1 deixa Rail/Clínica capazes de reusar TODA a logica
read-only de status change. Mutations (applyStatusDecisions,
createPackageContract, materializeAndChargePerSession) ficam pra
Fase B2.
Risco: zero comportamental — toda chamada produz o mesmo ctx
de antes. Codigo movido sem mudancas de logica.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registra os 3 commits da sessao (C12 trocar metodo, C12 filtro
cancelled, Fase A utils extract). Memoria
project_c12_antecipar_iterar atualizada pra refletir patterns
prontos pra Rail/Clinica.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decomposicao da agenda em prep pra replicar Rail/Clinica.
4 arquivos novos em src/features/agenda/utils/:
- eventoTipo.js -> EVENTO_TIPO + normalize/derive + MAX_SESSION_MINUTES
- dbFields.js -> pickDbFields whitelist (memoria pickdbfields_whitelist)
- timeHelpers.js -> isUuid + addMinutesToTime + isoToDecimalHour + dateToISO
- colors.js -> pickColor (status+tipo+isOccurrence)
useMelissaAgenda.js (2863L -> 2792L): removeu definicoes locais
(83 linhas), passou a importar dos utils. Aliases _addMinutesToTime
e _dateToISO mantidos no escopo via import "as" pra nao mexer
em 70+ callsites internos.
Fase A = baseline zero-comportamental pra Rail/Clinica adotarem
os mesmos helpers. Fase B (service de billing — applyStatusDecisions,
createPackageContract, materializeAndCharge) vem em seguida.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iteracao UX #2 do C12: records cancelled (do ciclo Revogar+Antecipar
e tambem das multas) poluiam o dialog "Lancamentos da sessao",
escondendo o que importa (ativos).
lancamentosShowHistory ref (default false) + lancamentosFiltered
computed filtra status !== 'cancelled'. lancamentosCancelledCount
computa contagem pra feedback.
UI:
- Dialog abre limpo (sempre lancamentosShowHistory=false em
onVerLancamentos)
- Quando ha cancelled e existe ativo: linha acima da lista com
"{N} cancelado(s) ocultos" + botao toggle "Mostrar/Ocultar
historico"
- Quando todos sao cancelled: empty state especial "Sem
lancamentos ativos. {N} cancelado(s) no historico" + botao
pra expandir
- Cards cancelled atenuados (opacity 0.55, border-dashed,
background sutil, description com line-through) — claramente
audit trail, nao-ativo
Combina com "Trocar metodo" (commit anterior) — agora o caso 99%
do tempo ele ve so o record ativo, nao precisa nem expandir
historico.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iteracao UX do C12 (antecipar pagamento) — antes user que queria
trocar PIX por dinheiro precisava Revogar (cancela record) +
Antecipar de novo (cria record novo), acumulando lixo no audit
trail (memoria project_c12_antecipar_iterar: ciclos longos chegaram
a 5+ records cancelled num mesmo evento).
MelissaEventoPanel ganha 3 botoes quando isAntecipacaoAtiva:
- "Trocar metodo" (default, icone pi-sync)
- "Revogar pagamento" (danger, icone pi-times-circle)
Antes mostrava so "Revogar".
MelissaLayout:
- anteciparMode ref ('create' | 'update') + onTrocarMetodoAntecipacao
pre-seleciona o metodo atual lendo o paid record antes de abrir
o dialog
- confirmAnteciparPagamento ramifica: mode='update' faz UPDATE no
paid existente (payment_method + paid_at + notes audit "metodo
trocado: X -> Y"). Sem cancel cycle, sem record novo.
- Dialog header/labels/CTA dinamicos por mode
Result: ciclo trocar metodo agora gera 0 records cancelled (so
update + nota auditoria). Revogar continua disponivel pra quando
realmente precisar cancelar o pagamento.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Atualiza PADRONIZACAO.md marcando §1.3 UX como 3 de 4 fechados.
#12 papel timbrado documentado como bloqueado em codigo externo
do UniaoApp.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ROADMAP item #1.3 #13. exceljs e jspdf ja estavam no package.json
mas as paginas de relatorio so renderizavam UI — zero export.
src/services/reportExport.service.js (novo) com 3 funcoes:
- exportSessionsToPDF: layout HTML→PDF via pdf.service.js (header
com branding tenant, KPI grid, tabela A4 com striping)
- exportSessionsToXLSX: ExcelJS workbook formatado (titulo + subtitle
+ KPIs inline + tabela com header escuro + alternating row + frozen
header). Import dinamico — exceljs e pesado, so carrega no click.
- exportSessionsToCSV: vanilla (sem deps) com BOM UTF-8 + separador
';' (Excel-friendly em pt-BR)
3 botoes em ambas paginas:
- RelatoriosPage.vue (/therapist/relatorios): icones pi-file-pdf +
pi-file-excel + pi-table no header (rounded), tooltip, disabled
quando total=0 ou loading, toast de sucesso/erro
- MelissaRelatorios.vue (Melissa secao): mesma logica, botoes nativos
.mr-head-btn no padrao Melissa
Filtro de status da tabela e respeitado no export (exporta o que
o usuario esta vendo). KPIs incluidos no PDF e XLSX.
§1.3 UX = 3/4 fechado: #10 (busca global) + #11 (recently viewed) +
#13 (relatorios export). #12 (papel timbrado) bloqueado em codigo
externo do UniaoApp.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ROADMAP item #1.3 #11. localStorage por user_id pra isolar sessoes
diferentes no mesmo browser. ROADMAP sugeria localStorage OU tabela
user_recent_access — escolhi localStorage por simplicidade (sem
migration adicional + zero round-trip por visita).
composables/useRecentPatients.js:
- useRecentPatients() — composable reativo Tipo A: items + hasItems
+ addVisit + remove + clear + refresh
- registerPatientVisit(patient) — helper stateless pra usar fora
de setup (ex: navigation guards, action handlers)
- Sincroniza entre instancias na mesma aba via CustomEvent + 'storage'
- Max 5 items. Dedup por id, novo no topo.
Wire-up de visita (registra ao carregar prontuario):
- MelissaPaciente.vue: registerPatientVisit no loadAll apos detail.load
- PatientProntuario.vue: registerPatientVisit em loadDetail apos p resolved
Wire-up de visualizacao (mostra quando query vazia):
- GlobalSearch.vue: grupo "Acessados recentemente" antes dos Atalhos.
goTo("recent") navega pra /therapist/patients/:id.
- MelissaBusca.vue: grupo "Acessados recentemente". emit('paciente')
reusando a logica do MelissaLayout que ja navega pra
/melissa/paciente?id=X.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fecha ROADMAP #1.3 #10 (busca global topbar). GlobalSearch.vue
classic+rail ja usava RPC. MelissaBusca era client-side preview com
fallback nas props (pacientes+eventos do dia) — agora consulta a
mesma RPC search_global com debounce 200ms + searchSeq pra descartar
respostas obsoletas.
3 grupos novos exibidos quando RPC retorna:
- rpc-appointments -> sessoes qualquer data (alem de "hoje")
- rpc-documents -> documentos por nome/tipo
- rpc-intakes -> cadastros recebidos
Pacientes mescla: RPC tem prioridade (todos os pacientes); props
mantida como fallback rapido (digitacao curta antes do debounce).
Emits estendidos: novos 'documento' + 'intake' alem dos existentes
'acao' + 'paciente' + 'evento'.
MelissaLayout atualizado:
- @paciente agora navega pra /melissa/paciente?id=X (antes ignorava
payload e so abria secao generica — bug existente)
- @documento abre prontuario do paciente com tab=documentos
- @intake abre /melissa cadastros-recebidos
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marca ROADMAP #1.4 #14 done em PADRONIZACAO.md (Fase 3 Gaps de MVP)
e adiciona entrada no log.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ROADMAP item #1.4 #14. Fecha Fase 1.4 Fiscal minimo (parcial — #15
NFS-e fica pra depois).
DocumentGenerate.service estendido:
- loadTherapistData puxa registro profissional (#5 migration) e
expoe terapeuta_registro auto-formatado ("CRP 12345/SP", "CRM
98765/RJ"). terapeuta_crp legacy mantido por compat — preenche
somente quando tipo=CRP.
- loadClinicData formata tenants.cpf_cnpj (11 ou 14 digitos) em
CPF (XXX.XXX.XXX-XX) ou CNPJ (XX.XXX.XXX/XXXX-XX).
- loadAllVariables aceita {extras} (valor, formaPagamento) e
computa valor_extenso via novo helper utils/valorExtenso.js
(pt-BR completo ate 999 milhoes).
- saveGeneratedDocument ganha templateTipo + usa
TEMPLATE_TYPE_TO_DOC_TYPE mapping (recibo_pagamento -> 'recibo',
laudo -> 'laudo', atestado -> 'atestado' etc) em vez de
hardcoded 'laudo'.
- emitirReciboParaSessao(eventoId, opts) — quick path one-call:
busca template recibo_pagamento global, carrega variaveis,
gera PDF blob, salva no Storage + documents + document_generated,
dispara download.
Migration 20260521000008 substitui no template recibo_pagamento
"Psicologo(a) - CRP {{terapeuta_crp}}" por "{{terapeuta_registro}}"
e atualiza variaveis[]. Universal — funciona com qualquer conselho
(CRP/CRM/CRFa/CREFITO/CRESS/CRN).
DocumentTemplates.service.TEMPLATE_VARIABLES ganha terapeuta_
registro + _tipo + _numero + _uf (terapeuta_crp marcado legacy).
useDocumentGenerate.generateAndSave passa templateTipo no save.
AgendaEventoFinanceiroPanel ganha botao "Emitir recibo" (icon
pi-file-pdf, outlined, full width) que aparece SOMENTE quando
record.status === 'paid'. Toast de sucesso/erro. Loading state.
Fluxo end-to-end: terapeuta marca sessao como paga -> botao
"Emitir recibo" aparece -> click -> PDF baixado + aparece em
/clinic/documents/templates do paciente como tipo 'recibo'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Atualiza PADRONIZACAO.md (Fase 3 marca CFP completo, todos os 5
itens #5/#6/#7/#8/#9 done) e adiciona entrada no log.md detalhando
os 5 commits do dia + arquitetura end-to-end + proximos passos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ROADMAP #1.2 #7 — Assinatura eletronica no portal.
Migration 20260521000007 cria RPC list_my_signatures (SECURITY DEFINER)
que cruza auth.uid() por 3 caminhos (signatario_id, signatario_email,
patient.user_id) e devolve solicitacoes pendentes + share_token pra
link de assinatura. service.listMySignatures wrappa a RPC.
Composable useDocumentSignatures ganha loadMine().
PortalDocumentos.vue (nova) — lista signatures do paciente logado com
KPIs (total/pendentes/assinados/recusados), filtro, e botao "Assinar
agora" que navega pra /shared/document/:token. Item no portal.menu
"Documentos > Para assinar".
SharedDocumentPage.vue estendida: agora chama getSignableDocumentBy
Token primeiro (RPC nova). Quando o documento tem signatures pendentes,
mostra painel azul abaixo do preview com:
- Aviso LGPD/CFP explicando o que sera registrado (IP/UA/timestamp/hash)
- Checkbox aceite obrigatorio
- Selecao de signatario quando multi-signatario
- Botoes Assinar/Recusar com loading state
- Computacao SHA-256 server-fetched antes do click
Fluxo: terapeuta gera doc -> cria signature + share_link -> link e
listado em /portal/documentos -> paciente clica -> /shared/document/
:token mostra doc + painel -> aceite -> assinatura registrada via RPC
sign_document_by_token (IP/UA capturados server-side).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registra cronologia da leva noturna 20/05 evening -> 21/05 01:06:
Fase 0+0.5 sweep foundation, M1 Home/Components, M2 Pacientes batch,
M3+M4+M5+M6 foundation em batch, M5 quick wins, Fase 2 Graphify
hotspots, Asaas Gateway Tier 1 Fase A, Compliance CFP #5/#8/#9.
8 entradas no log.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DESIGN_ASAAS_GATEWAY.md documenta arquitetura. Schema novo: 2
migrations (tables + RLS) cobrindo asaas_customers + asaas_payments
+ asaas_webhook_events. Client service asaasGatewayService.js no
features/financeiro. 3 Edge Function stubs (create-payment-record,
cancel-payment, sync-payment) — webhook financial_records eh Fase B.
Bloqueadores Fase B (implementacao real): user precisa criar conta
Asaas, gerar API keys, configurar webhook, setar ENV vars no
Supabase. Decisao modelo de negocio (A/B/C) tambem pendente.
Stops marcados claramente no DESIGN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modulo 6 da Fase 1. noticesSelects.js extrai os 2 selects do
noticeService (GLOBAL_NOTICE_SELECT, NOTICE_DISMISSAL_SELECT) +
noticeService passa a usa-los (zero select inline). Conversations
ganha foundation: 3 services (_tenantGuards, conversationsSelects,
conversationsRepository). Channel factory (WhatsApp/SMS/Email) e
composables ficam pra sessao dedicada — escopo M6 era so destravar
o supabase.from() inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modulo 5 da Fase 1 + quick wins fechados. features/tenantship/ com
2 services + 2 composables (members + invites). MembersPage.vue
nova em views/pages/admin/ + rota /admin/members em routes.clinic.
Migration 20260520000005 cria RPC accept_tenant_invite (SECURITY
DEFINER + lock FOR UPDATE) — tenantInvitesRepository.acceptInvite
agora chama RPC real (nao mais stub). SaasTenantFeaturesPage
refatorada pra usar novo tenantFeatureAdminService. SetupWizardPage
2648 linhas deferido pra sessao dedicada.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modulo 4 da Fase 1. 9 arquivos novos em features/financeiro/:
4 services (_tenantGuards, financialSelects, financialRecords
Repository, financialExceptionsRepository, billingContractsRepository)
+ 4 composables (useFinancialRecords, useFinancialExceptions,
useBillingContracts, useBillingOrchestrator). Old composables ainda
em paralelo — Fase C (cutover) bloqueada pelas decisoes #2/#3/#6
de billing (memoria agenda_billing_decisoes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modulo 2 da Fase 1 de padronizacao em batch unico. patientsSelects.js
nova com 11 constantes de select. patientsRepository.js estendido com
~15 funcoes novas (markIntakeConverted, list/get/update por
contexto, etc). 8 composables refatorados em paralelo (usePatients,
useDetail, Financial, Sessions, Messages, Documents, Recurrences,
SupportContacts) — zero supabase.from() em qualquer composable de
patients. _lastPatientId movido pra DENTRO das functions nos 3
composables que tinham. CadastrosRecebidosPage + MelissaCadastros
Recebidos pegam carona dos selects. Aguarda teste batch consolidado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modulo 1 da Fase 1 de padronizacao. Novos features/medicos (services
+ composable useMedicos) e features/insurance (idem). 3 cadastros
rapidos (medicos, convenios, ComponentCadastroRapido + Insurance
PlanQuickCreateDialog) migrados pra usar os composables novos —
zero supabase.from() em UI components. TEST_ACCOUNTS extraido pra
src/config/devTestAccounts.js. Topbar ganhou switcher de layout
+ atalhos M1 via novo useTopbarDevMenuExtras. M1.6 MelissaLayout
90 imports deferida pra sessao dedicada (memoria padronizacao_sweep).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Relatorio standalone HTML (com print CSS otimizado pra PDF export):
- 10 paginas estruturadas
- Sumario executivo + metricas + pontos fortes
- 10 codes smells / dividas tecnicas detalhadas
- 8 issues de UX
- 7 riscos arquiteturais
- 15 recomendacoes priorizadas (P0-P3) com esforco e impacto
- Roadmap proposto em 3 horizontes
- Apendices: 14 bugs do dia, pendencias, commits, status dos cenarios
Visao senior eng: arquitetura solida em conceito, divida tecnica
em execucao. Top 5 achados:
1. 3 hotspots >2.8k LOC cada (AgendaEventDialog 6k, MelissaLayout 4.3k)
2. Logica de status change triplicada (Melissa/Rail/Clinica)
3. billing_contracts.updated_at gotcha
4. Snapshot stale popover (mitigado mas estrutural)
5. Audit trail acumulando ruido
Recomendacao chave: extrair status change orchestrator pra composable
shared ANTES da replicacao Rail/Clinica. Senao replica os mesmos
14 bugs vezes 2.
Para PDF: abrir relatorios/RELATORIO-AGENDA-2026-05-20.html no
browser e Ctrl+P -> Salvar como PDF.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C12 fluxo critico OK no DB (antecipar/revogar/re-antecipar/realizada
detecta paid). 5 bugs corrigidos no caminho: re-antecipar nao reusa
cancelled, popover watch sync com lookup virtual->materializada,
normalizeForMelissa expoe owner_id, etc.
User adiou C12 pra iterar UX depois (pos-Rail/Clinica). Salvo em
memoria project_c12_antecipar_iterar.
C13 prep: lock "edit cobrada" ja implementado na Fase 6 (commit
1feb711). User vai validar visualmente com Joao Almeida ou Andre.
14 commits no dia. Pendencias documentadas. Working tree limpo
exceto HANDOFF/log (este commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug do "Usar sumiu apos revogar antecipacao": o watch sincronizava
eventoSelecionado por id, mas quando virtual era materializada
(antecipar/Usar/Realizada flow) o id mudava de rec::rule::date
pra uuid real. Watch nao achava match -> popover ficava preso na
versao virtual stale -> botoes refletiam estado antigo.
Fix: lookup em 2 etapas:
1) match por id (caso comum)
2) match por recurrence_id+recurrence_date quando nao acha (caso
virtual->materializada). Pega a versao real correspondente
aquela data.
Estado final do teste C12 do user: status=realizado, saldo 3/4,
1 pending + 5 cancelled (audit trail de varios ciclos antecipar/
revogar). Funcionalmente OK; com o fix, retestes ficam mais limpos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug introduzido pelo watch sync do popover (commit b5e00a7).
Apos o sync com eventos computed, eventoSelecionado.value ficava
com apenas os campos do normalizeForMelissa return. owner_id,
tenant_id, terapeuta_id, billing_contract_id NAO estavam expostos
no normalize -> sumiam apos refresh. onAnteciparPagamento entao
mandava owner_id=null pro RPC create_financial_record_for_session
-> "null value in column owner_id violates not-null constraint".
Fix:
- normalizeForMelissa agora expoe owner_id, tenant_id,
terapeuta_id, billing_contract_id explicitos no return
- onAnteciparPagamento ganhou fallback robusto: ev.owner_id ||
ev._raw?.owner_id || M.ownerId.value, com throw explicito se
nada disponivel (em vez de mandar null pro RPC)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dois bugs descobertos no ciclo C12 antecipar -> revogar -> reantecipar:
1) confirmAnteciparPagamento reusava record cancelled:
Quando user revogava (vira cancelled) e antecipava de novo,
o existRec query pegava o cancelled e UPDATE-ava pra paid no
MESMO record id. Resultado: notes mantinham historico
"Cancelada via reversao" + "Antecipacao revogada" + record
reativo como paid, confuso pra audit trail. Fix: filtrar
.neq('status', 'cancelled') na busca de existRec — agora a
re-antecipacao via RPC cria record fresh.
2) Popover snapshot stale (pendencia documentada em
project_melissa_popover_snapshot, antecipada pra agora):
eventoSelecionado.value era snapshot do clique e nao acompanhava
updates do _paymentStateMap pos M.refetch. User antecipava, o
record paid era criado, mas o popover continuava com paymentState
antigo -> botao continuava "Antecipar pagamento" em vez de
alternar pra "Revogar pagamento". Fix: watch em M.eventos sincroniza
eventoSelecionado com a versao fresh quando id bate. flush:'post'
pra rodar apos o computed reagir.
Como o popover agora atualiza in-place, removido fecharEvento() de
confirmAnteciparPagamento e onRevogarAntecipacao — o user pode ver
o botao alternar live em vez de precisar reabrir o popover.
Cleanup do estado do Andre: deletado record orfa 3a4c79e0 pra reset
do teste C12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX gap descoberto durante teste C12: apos antecipar pagamento,
nao havia caminho via popover pra desfazer caso user tenha
errado (paciente nao pagou, errou o valor, etc).
Implementacao:
- Botao "Antecipar pagamento" agora alterna pra "Revogar
pagamento" (vermelho, --danger) quando isAntecipacaoAtiva
(status=agendado + paymentState=paid)
- Handler onRevogarAntecipacao em MelissaLayout: ConfirmDialog
vermelho + cancela record paid + nota de auditoria em notes
("[YYYY-MM-DD] Antecipacao revogada em ...") + refetch
- Apos revogar, botao volta pra "Antecipar pagamento" — user
pode antecipar de novo com valor/metodo corretos
Limites: so disponivel em status='agendado'. Apos Realizada o
paid representa pagamento real da sessao realizada, nao
antecipacao — estorno deve ir pelo /financeiro.
Sobre "Usar" desaparecer apos antecipar (questao do user): comportamento
correto. "Usar" cria record+consome saldo — duplicaria com paid
existente. Apos antecipar, fluxo correto e clicar Realizada (que
detecta paid pre-existente via fix anterior 00c4168).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preparacao pra teste C12 (antecipar pagamento). Fluxo:
1. User clica "Antecipar pagamento" em virtual futura -> cria
record paid R$ X sem consumir saldo
2. Depois marca a sessao como Realizada -> dialog deve detectar
o paid + so consumir saldo (NAO criar record novo, evitar
duplicidade)
Sem esse fix, marcar Realizada apos antecipar abriria o dialog
"Gerar cobranca?" com default true, gerando record novo duplicado.
Implementacao:
- _loadStatusChangeContext: carrega ctx.existingPaidRecord (qualquer
paid linkado ao evento, n=1)
- Dialog: nova prop existingPaidRecord + computed showAlreadyPaid
(substitui showCobrancaPacote quando paid existe)
- Template: bloco "Sessao ja paga via antecipacao" com info do
pagamento + preview do consumo de saldo
- _applyStatusDecisions: novo branch 4-pre roda ANTES do generatePackageCharge:
se realizado+pacote saldo+paid existe, roda tasks pendentes (1b
amarra) + incrementa saldo sem criar record. Return cedo.
Backfill: Andre 10/06 voltou pra agendado + saldo 2/4 (estado limpo
pra testar C12 com a sessao 10/06 antecipando).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cenario 11 completo. 5 bugs descobertos+corrigidos durante
a bateria (UI confusa, gotcha billing_contracts.updated_at,
reverse transitions, lock sessao encerrada, label/badge
pacote-aware). Reverse trava antecipada de pos-C13 pra ja
(user hit pra valer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dois bugs descobertos durante C11/C+D:
1) Faltou+multa SEM consumeSaldo nao amarrava billing_contract_id
no agenda_evento (so amarrava se consumeSaldo=true). Resultado:
sessao 27/05 do Andre faltou+multa-sem-consume ficou sem rastro
do contrato no DB. Reverse posterior nao detectaria saldoConsumed.
Fix: bloco 1b) universal — sempre amarra quando forward (realizado/
faltou/cancelado) + tem contract + eventoId. Cobre todos os
combos (multa-sem-consume, multa-com-consume, generatePackageCharge,
consumeSaldo solo).
2) Reverse decrementar saldo as vezes nao persistia. Suspeita: race
com ctx.billingContract.sessions_used stale do _loadStatusChangeContext
quando flows rapidos sequenciais (Realizada+gerar -> Agendada
imediato). Fix: refetch FRESH do billing_contracts.sessions_used
direto do DB ANTES de calcular newUsed. Mais robusto contra qualquer
race condition. Adicionado console.log pra futura debug.
Removida duplicidade do amarra-billing_contract_id no bloco
consumeSaldo (universal cobre).
Backfill Andre Green: 27/05 amarrado, saldo voltou pra 2/4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug descoberto durante teste C11: sessao 27/05 do Andre Green
(materializada via Falta+reverse, pertence ao pacote saldo)
mostrava "A cobrar R$ 40,00" no popover mesmo sem fatura ativa.
Implicava que dava pra gerar cobranca avulsa solta — conflito
com o flow do pacote (Usar consume saldo).
Fix em paymentLabel: quando state='none' e ev.contract existe,
label muda conforme estilo:
- saldo: "Aguardando uso do pacote"
- upfront: "Coberta pelo pacote (upfront)"
Avulsa sem pacote continua mostrando "A cobrar R$ X".
Simetria em MelissaAgenda.vue badge gate: nao mostra badge $ amber
em sessao state=none com pacote amarrado (hasPacoteTied). Sem
isso, sessao agendada de pacote saldo no calendar ficava com
badge "cobranca pendente" enganoso.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug em cascata descoberto durante C11/B com Andre Green:
- User clicou Falta + Descontar (consumeSaldo) -> sessions_used: 1->2
- billing_contract_id do agenda_evento ficou NULL (omissao no flow)
- User clicou Agendada (reverse) -> detector saldoConsumed em
_loadStatusChangeContext checa evRow.billing_contract_id, que esta
NULL -> saldoConsumed=false -> bloco "Devolver saldo" NAO aparece
no dialog -> saldo NAO devolvido
- Next Falta mostra "Descontar 2 para 3" em vez de "1 para 2"
Fix: bloco consumeSaldo agora tambem amarra billing_contract_id no
agenda_eventos. Replica o padrao que ja existe no generatePackageCharge
e no onUsarSessao. Sem isso, qualquer reverse pos-consumeSaldo nao
detecta o saldo consumido.
Backfill manual do Andre: sessions_used voltou pra 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User hit pra valer a pendencia documentada (reverter
realizado/faltou/cancelado pra agendado deixa records/saldo
orfaos). Decidido implementar trava AGORA em vez de pos-C13.
Quando user clica "Agendada" no popover/dialog em sessao que
tem artefatos pendentes (cobranca pending, multa, saldo consumido
em pacote saldo), abre o AgendaStatusChangeConfirmDialog com nova
variante "reverse":
1. Lista records pending vinculados (descricao + valor) com radio
[Cancelar (recomendado) | Manter ativa]
2. Warning textual pra records PAID (estorno e manual pelo
Financeiro — sem radio, so info)
3. Saldo consumido (pacote saldo): radio [Devolver 1 sessao | Manter]
No confirm:
- Cancela records pending escolhidos (status='cancelled' + notes
de auditoria)
- Decrementa sessions_used + reativa contract se estava completed
- Desamarra billing_contract_id do evento se devolveu saldo
- Status muda pra agendado (ja foi aplicado pelo _applyStatusUpdateOnly)
Se nao tem artefato algum (sessao agendado -> agendado, ou
realizado sem records): aplica direto sem dialog (existing
behavior via _needsConfirmDialog).
_loadStatusChangeContext agora carrega reverseArtifacts (status
anterior, records ativos, saldoConsumed) quando novoStatus=agendado.
Memoria project_agenda_reverse_transitions atualizada — pendencia
fechada antes da hora.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ROOT CAUSE DESCOBERTO durante C11/A com Andre Green:
billing_contracts NAO tem coluna updated_at. UPDATEs em
_applyStatusDecisions passavam updated_at -> Postgres retornava
"column updated_at does not exist" -> Promise.allSettled engolia
como {status: 'rejected'} silencioso -> toast warn generico que
user nao percebia. Resultado: sessions_used nunca incrementava.
Bug existia em DOIS lugares:
1. consumeSaldo block (faltou/cancelado pacote saldo) - afetaria
C11/B, C11/C, C11/D
2. generatePackageCharge block (realizado pacote saldo) - afetou
C11/A
Em ambos: removido updated_at do patch (.update({...})).
ADICIONAL: generatePackageCharge refatorado pra usar AWAITS
SEQUENCIAIS (igual onUsarSessao do MelissaLayout que sempre
funcionou):
- 4a) UPDATE agenda_eventos.billing_contract_id (faltava!)
- 4b) RPC create_financial_record_for_session
- 4c) UPDATE billing_contracts.sessions_used + status=completed
Cada step com try/catch + console.error + toast distinto. Sem mais
falhas escondidas em Promise.allSettled paralelo.
Backfill manual do estado do Andre Green: evento 6e70476f agora
amarrado ao contract 691118da com sessions_used=1.
Memoria nova: project_billing_contracts_no_updated_at.md pra evitar
o gotcha no futuro.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes (UX confusa): bloco "Gerar cobranca no pacote?" tinha so um
Select "Como cobrar?" com options mixadas:
- "Enviar link de pagamento (Asaas)"
- "Ja recebi - PIX"
- "Ja recebi - Dinheiro"
- etc
User selecionou "Ja recebi - PIX" pensando que era "cobrar via PIX"
durante teste C11/A com Andre Green. Resultado: fatura virou paid
sem o user ter recebido de verdade. Ambiguidade entre "como cobrar"
(header) e "ja recebi" (options).
Refactor: espelhar o padrao da avulsa (showRegistrarPagto):
1. Sub-question "A sessao ja foi paga?" radio Sim/Nao (default Nao)
2. Se Nao -> Select "Como vai cobrar?" [Apenas registrar pendente |
Enviar link de pagamento (Asaas)]
3. Se Sim -> Select "Como recebeu?" [PIX | Dinheiro | Deposito |
Maquininha] (sem prefixo "Ja recebi" — header ja deixa claro)
Defaults safer: markPaid=false em ambos contextos (avulsa e pacote)
pra evitar marcar paid sem querer. paymentMethod='pending' inicial.
Handler em useMelissaAgenda._applyStatusDecisions: pos-processamento
agora usa decision.markPaid explicito no caso pacote saldo:
- markPaid=true -> record vira paid + payment_method=X
- markPaid=false + paymentMethod='link' -> pending + payment_method='asaas'
- markPaid=false + paymentMethod='pending' -> pending sem metodo
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cenario 10 (status change avulsa) completo:
- A: Realizada sem markPaid (record pending preservado)
- A2: Realizada + markPaid maquininha (paid + paid_at + payment_method)
- B: Faltou + multa fixed R$ 30 (original cancelled + nova multa)
- C: Cancelado >2h (original cancelled, sem multa)
- C2: Cancelado tardio <2h, full charge (original cancelled + nova taxa)
Bugs descobertos + corrigidos durante a bateria: cobranca dupla na
multa (cancela original agora), _reloadRange not defined no escopo
de _buildHandlers, badge $ amber em sessao encerrada, paymentLabel
usando ev.price em vez de paymentAmount pra pending, popover
permitindo emissao de fatura em sessao cancelada.
3 pendencias pos-C13 mapeadas em memoria + addendum HTML do doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bugs descobertos durante testes C10/A2/B/C com user:
1) _reloadRange not defined: _buildHandlers nao destruturava
_reloadRange do deps (passava mas nao desempacotava). Toast
ReferenceError ao tentar reload pos-status change. Fix em
useMelissaAgenda.js:_buildHandlers.
2) Badge $ amber em sessao cancelada: MelissaAgenda.vue badge gate
ignorava status. Cancelado+state=none (records cancelled
filtrados) ainda recebia badge "cobranca pendente". Fix: gate
sessaoEncerrada (cancelado/faltou) -> sem badge nunca.
3) Botao "Gerar cobranca" em sessao encerrada: AgendaEventoFinanceiro
Panel mostrava botao mesmo em cancelado/faltou -> user podia
emitir fatura nova em sessao que nao aconteceu. Fix: v-if
!isSessaoEncerrada + label muda pra "Sessao cancelada · sem
cobranca ativa".
4) paymentLabel usava ev.price em vez de paymentAmount pra state
'pending': caso multa R$ 30 mostrava R$ 150 (ev.price original).
Fix: usar paymentAmount tambem em pending.
5) Lock total em sessao encerrada (cancelado/faltou):
- "Editar sessao" SOME do popover
- Realizada/Falta/Reagendar/Cancelar disabled com tooltip
- Apenas "Agendada" continua funcional (caminho explicito de
recuperacao). Single path de saida do estado encerrado.
Adicoes UX em AgendaStatusChangeConfirmDialog:
- Hint contextual sobre min_hours_notice explicando POR QUE multa
veio (des)marcada por padrao: "Cancelou 18.5h antes da sessao.
Regra: multa apenas quando cancelamento <2h -> sem multa por
padrao." Terapeuta ve a razao e pode inverter conscientemente.
Adicoes UX em MelissaEventoPanel:
- Botao "Agendada" (variante --info azul cyan) no grupo status
pra reset/recuperacao. CSS .evento-act--info hover + is-current.
Doc:
- Addendum C10 no topo de src/docs/agenda-compromisso-financeiro
-cenarios.html capturando todas as divergencias/melhorias vs
mockup original + 3 pendencias pos-C13 (reverse transitions,
popover snapshot, A2 markPaid stale).
Pendencias salvas em memoria pra puxar pos-C13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adicoes (durante teste C10/A2):
- Botao "Agendada" no popover (pi-calendar, variante --info azul
cyan) pra permitir reset de status realizado/faltou/cancelado
voltando pra agendado sem precisar abrir o AgendaEventDialog.
Wire-up: emit 'agendar' -> onAgendar -> updateEventoStatus.
- CSS .evento-act--info: hover + is-current com tom cyan
(#38bdf8 do domainColors da agenda). Highlight generico
rgba(255,255,255,0.12) era invisivel em light mode.
Bug fixes durante teste C10/B com Otto Rank:
- MelissaEventoPanel paymentLabel: usar paymentAmount tambem pra
state='pending' (antes so 'paid' usava; pending caia em ev.price
e mostrava R$ 150 original quando o pendente real era R$ 30 da
multa).
- useMelissaAgenda onUpdateSeriesEvent: chamar _reloadRange() apos
_applyStatusDecisions. Sem isso o paymentStateMap+amountMap nao
re-populavam apos status change com multa -> FullCalendar e
popover ficavam stale ate F5/troca de view.
Pendencia salva em memoria: travas em reverse transitions
(faltou->agendado deixa multa orfa). User hit pra valer com Otto
durante teste, R$ 30 limpo manualmente no DB. Implementar pos-C13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: ao mudar status pra faltou/cancelado com multa configurada
em financial_exceptions, _applyStatusDecisions INSERIA o novo
record da multa MAS deixava o pendingRecord original em pending.
Resultado: cobranca dupla (R$ 200 original + R$ 30 multa = R$ 230).
Fix em useMelissaAgenda.js:1450-1505:
- applyFine agora carrega data da sessao na description ("Multa
por falta - sessao dd/mm/aa") pro paciente identificar na fatura.
- Novo bloco 2b: cancela ctx.pendingRecord quando faltou/cancelado,
com nota de auditoria appendada em notes ("[YYYY-MM-DD] Cancelada
- substituida por multa de no-show" ou similar). Vale tanto pra
caso com multa quanto sem (status mudado sem aplicar multa).
Fix dormente em useAgendaFinanceiro.js:59 ('fixed' -> 'fixed_fee')
- charge_mode no schema eh 'fixed_fee' mas calcChargeAmount usava
'fixed' silenciosamente caia no fallback. Path nao exercitado na
Melissa (usa _applyStatusDecisions, nao handleStatusChange), mas
iria quebrar se algum dia fosse.
Pre-teste C10: financial_exceptions seedadas no DB para tenant
Bruno Terapeuta / owner Leonardo:
- patient_no_show: fixed_fee R$ 30
- patient_cancellation: full, min_hours_notice=2, default_consume_on_miss=true
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cenário 9 (Per-session — Michael Balint 12 × R$ 150)
- Testado e passou. 1 rule + 12 agenda_eventos materializadas + 12
financial_records pending. Sem billing_contract. Badge $ em todas as
12 sessões. Conforme esperado.
/melissa/financeiro-lancamentos: agrupado por paciente
- DataTable com rowGroupMode='subheader' + groupRowsBy='patient_id'
- Header de grupo com avatar + nome + badge "N lançamento(s)"
- expandableRowGroups + v-model:expandedRowGroups; watcher popula
todos os grupos da página atual como expandidos (sempre que
recordsGrouped muda — refletindo paginação/filtros)
- Sort outer por nome do paciente, preserva inner order
(pai → filhos de multas/taxas via mesmo agenda_evento_id)
Bubble-up @cobranca-atualizada → M.refetch
- Antes: ao marcar como pago no dialog, o card no FC ficava stale
até trocar de view. AgendaEventoFinanceiroPanel emitia
cobranca-atualizada mas só o loadOccFinancialRecord do dialog
escutava; o _paymentStateMap da agenda nao re-rodava.
- Fix: AgendaEventDialog ganhou _onCobrancaAtualizada que dispara
loadOccFinancialRecord() E emit('cobranca-atualizada') pra cima.
MelissaLayout escuta nos 2 dialogs e chama M.refetch() +
refetchEventosHoje(). Card passa pra borda verde na hora.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>