agenda: Fase 5 (status change/edit cobrada) + indicadores visuais + UX convenio

DB
- drop agenda_excecoes (substituida por financial_exceptions + lock-edit
  baseado em financial_records)
- financial_records.payment_link (Asaas + link compartilhavel)
- financial_exceptions.consume_on_miss (rotular nao-show consome ou nao)
- billing_contracts.charging_style (upfront/saldo/per_session)

Payment refactor
- paymentSettlement -> paymentMethod (string) + markPaidNow (bool).
  Handler aplica payment_method sempre; status='paid'+paid_at apenas
  quando markPaidNow=true && method != 'link'. Asaas (link) sempre
  liquida via webhook, nunca nasce paid.
- create_financial_record_for_session com pos-RPC patch pra payment_method
  e (opcional) status='paid' quando user marca "ja recebi".

Indicadores visuais (3 canais distintos por estado)
- Paid: barra esquerda emerald-500 4px na agenda (MelissaAgenda),
  pi-check-circle no popover/Resumo.
- Pending: badge \$ amber canto direito (mantido); linha amber no popover/
  Resumo "A receber R\$ X (cobranca pendente)".
- Neutro: sem badge nem barra (compromisso pessoal, bloqueio, ou
  ocorrencia virtual de pacote upfront/saldo).
- Bulk-load de paymentState em _reloadRange etapa 4 (1 query unica em
  financial_records mapeada por agenda_evento_id).
- AgendaEventDialog Resumo lateral ganha linha entre pi-clock e
  pi-map-marker via novo sessionPaymentRecord (sem guard de
  occurrenceMode, contrario ao occFinancialRecord que continua so pra
  Rail/Clinica). 5 estados: paid+paid_at, overdue+venceu, pending+vence,
  sem cobranca c/ valor, sem cobranca s/ valor.

UX de convenio
- InsurancePlanServiceQuickCreateDialog novo: cadastra procedimento
  POR CIMA do AgendaEventDialog sem sair da agenda. Auto-seleciona
  novo procedimento so quando nada estava selecionado antes.
- Caixa cinza "Cadastrar procedimento" sempre visivel quando convenio
  selecionado, com copy variavel (0 procedimentos: chamada urgente;
  1+: "se quiser adicionar mais").
- "+ Novo convenio" toolbar em ConfiguracoesConveniosPage (botao
  estava faltando, empty state mandava clicar em botao inexistente).
- Hint contextual abaixo do card Sessao/Honorarios: convenio = "N da
  guia eh opcional", gratuito = "sem cobranca", particular = sem hint.
  Label "N da Guia" tambem ganhou "(opcional)" no service-picker dialog.

Bug fixes
- pickDbFields whitelist faltando 'modalidade' (useMelissaAgenda.js:74)
  — sessoes avulsas eram salvas como presencial independente da
  escolha visual. Adicionado.
- goToConveniosConfig removida — fazia router.push("/therapist/
  configuracoes/convenios") mas /configuracoes/* eh rota raiz, nao
  filha. Substituida pelo quick-create inline (#1).
- bloqueioCobrindo + dialogBlockOverlap passados via deps em
  _buildHandlers (refs do useMelissaAgenda nao sao acessiveis no
  escopo de _buildHandlers).

Fase 5 (status change + edit cobrada)
- AgendaStatusChangeConfirmDialog: confirm dialog quando user muda
  status pra realizada/faltou/cancelado, com opcoes de markPaid ou
  gerar cobranca conforme o caso.
- useAgendaBloqueios novo composable: extrai logica de bloqueios
  cinza (background events) do MelissaAgenda.

Doc viva
- src/docs/agenda-compromisso-financeiro-cenarios.html: 13 cenarios
  de teste manual. C1-C4 ja validados. Cada teste validado vira parte
  da doc final pra area de ajuda (pos-Fase 9).

Wiki/handoff
- agenda-compromisso-fluxo e agenda-billing-pesquisa-mercado (decisoes
  arquiteturais sobre billing).
- HANDOFF.md atualizado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-19 08:31:18 -03:00
parent 41c44272a3
commit e95ed9b585
41 changed files with 8715 additions and 852 deletions
+1 -1
View File
@@ -106,7 +106,7 @@
"contact_email_types", "contact_emails"
],
"Agenda / Agendamento": [
"agenda_eventos", "agenda_bloqueios", "agenda_configuracoes", "agenda_excecoes",
"agenda_eventos", "agenda_bloqueios", "agenda_configuracoes",
"agenda_online_slots", "agenda_regras_semanais",
"agenda_slots_bloqueados_semanais", "agenda_slots_regras",
"agendador_configuracoes", "agendador_solicitacoes"
+1 -2
View File
@@ -43,13 +43,12 @@ Mapa completo do banco de dados PostgreSQL 17, extraído de `schema.sql` (2026-0
| `module_features` | Features por módulo |
| `tenant_modules` | Módulos ativos por tenant |
### Agenda (11 tabelas)
### Agenda (10 tabelas)
| Tabela | Descrição |
|--------|-----------|
| `agenda_bloqueios` | Bloqueios de horário |
| `agenda_configuracoes` | Configurações da agenda por tenant_member |
| `agenda_eventos` | Eventos da agenda (sessões, bloqueios) |
| `agenda_excecoes` | Exceções na agenda (horários extras, bloqueios pontuais) |
| `agenda_online_slots` | Slots de agendamento online |
| `agenda_regras_semanais` | Regras semanais de disponibilidade |
| `agenda_slots_bloqueados_semanais` | Slots bloqueados na semana |
@@ -0,0 +1,32 @@
-- ============================================================================
-- Drop agenda_excecoes (tabela órfã) + tipos relacionados
-- ----------------------------------------------------------------------------
-- A tabela `public.agenda_excecoes` foi criada num design anterior pra
-- representar "exceções no horário de trabalho" (almoço extra, atendimento
-- fora do padrão, etc) mas nunca foi integrada à UI. Auditoria em
-- 2026-05-13 confirmou 0 referências em src/. As funcionalidades equivalentes
-- vivem em:
-- - public.agenda_bloqueios — bloqueios (período, dia, horário, feriado)
-- - public.agenda_configuracoes.pausas_semanais (jsonb) — pausas semanais
-- - public.feriados — feriados nacionais/municipais
--
-- Esta migration:
-- 1) Dropa o trigger tg_agenda_excecoes_updated_at
-- 2) Dropa a tabela public.agenda_excecoes (CASCADE pra cair policies)
-- 3) Dropa os enums tipo_excecao_agenda e status_excecao_agenda
-- (verificados: usados APENAS por agenda_excecoes)
-- ============================================================================
BEGIN;
-- 1. Trigger (idempotente)
DROP TRIGGER IF EXISTS tg_agenda_excecoes_updated_at ON public.agenda_excecoes;
-- 2. Tabela (CASCADE leva policies junto)
DROP TABLE IF EXISTS public.agenda_excecoes CASCADE;
-- 3. Enums órfãos
DROP TYPE IF EXISTS public.tipo_excecao_agenda;
DROP TYPE IF EXISTS public.status_excecao_agenda;
COMMIT;
@@ -0,0 +1,24 @@
-- ============================================================================
-- Adiciona coluna payment_link em financial_records
-- ----------------------------------------------------------------------------
-- Quando a cobrança for paga via gateway externo (Asaas, Stripe, Mercado Pago)
-- e o terapeuta escolher "Enviar link de pagamento" no AgendaEventDialog, o
-- link de cobrança gerado pelo gateway é salvo aqui. UI da lista do Financeiro
-- usa esse campo pra exibir ícone clicável (external-link).
--
-- Campo nullable: registros sem integração de gateway (PIX manual, dinheiro,
-- depósito, cartão maquininha) ficam com payment_link = NULL.
--
-- Preparação pra Fase 7 (Pagamento como entidade separada) — quando a
-- integração Asaas estiver completa, o webhook vai preencher esse campo.
-- ============================================================================
BEGIN;
ALTER TABLE public.financial_records
ADD COLUMN IF NOT EXISTS payment_link text;
COMMENT ON COLUMN public.financial_records.payment_link IS
'URL externa de cobrança (Asaas/Stripe/etc) quando payment_method indica gateway. Null em pagamentos manuais.';
COMMIT;
@@ -0,0 +1,22 @@
-- ============================================================================
-- Adiciona coluna default_consume_on_miss em financial_exceptions
-- ----------------------------------------------------------------------------
-- Define o comportamento padrão pro saldo de pacote quando o status muda
-- pra "faltou" ou "cancelado":
-- true → desconta 1 sessão do pacote (sessions_used += 1) por padrão
-- false → não consome saldo (sessão fica disponível pra remarcar)
--
-- O dialog de confirmação que aparece ao mudar status sugere essa decisão
-- mas o terapeuta pode override caso a caso. Padrão começa false (mais
-- benevolente ao paciente).
-- ============================================================================
BEGIN;
ALTER TABLE public.financial_exceptions
ADD COLUMN IF NOT EXISTS default_consume_on_miss boolean DEFAULT false NOT NULL;
COMMENT ON COLUMN public.financial_exceptions.default_consume_on_miss IS
'Default pro toggle "Descontar do saldo" no dialog de status change. false = não consome (paciente pode remarcar); true = consome (sessão perdida).';
COMMIT;
@@ -0,0 +1,31 @@
-- ============================================================================
-- Adiciona coluna charging_style em billing_contracts
-- ----------------------------------------------------------------------------
-- Identifica como o pacote foi cobrado na criação:
-- 'upfront' → 1 financial_record total criado na hora; sessões só
-- consomem saldo, não geram nova cobrança
-- 'saldo' → sem financial_record na criação; cada sessão realizada
-- gera 1 cobrança individual e incrementa sessions_used
-- 'per_session'→ N financial_records já criados na materialização da série
-- (chargeMode='per_session' do AgendaEventDialog)
--
-- Sem esse campo, o handler de status change não saberia distinguir entre
-- "já tudo pago, só atualizar status" vs "criar cobrança nova".
-- ============================================================================
BEGIN;
ALTER TABLE public.billing_contracts
ADD COLUMN IF NOT EXISTS charging_style text DEFAULT 'saldo';
-- Constraint pra restringir aos 3 valores válidos
ALTER TABLE public.billing_contracts
DROP CONSTRAINT IF EXISTS billing_contracts_charging_style_chk;
ALTER TABLE public.billing_contracts
ADD CONSTRAINT billing_contracts_charging_style_chk
CHECK (charging_style = ANY (ARRAY['upfront'::text, 'saldo'::text, 'per_session'::text]));
COMMENT ON COLUMN public.billing_contracts.charging_style IS
'Estilo de cobrança: upfront (1 record total no início), saldo (cobra por sessão realizada), per_session (N records já criados).';
COMMIT;