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>
DB
- migration 20260519000001: create_financial_record_for_session passa a
ignorar status='cancelled' na idempotência (era bug — cancelar e tentar
regerar travava silencioso retornando o cancelado)
Cenário 5 (convênio) — fixes pra save + visualização
- Convênio: amount lia 'price' (null) → agora detecta via insurance_plan_id
e usa insurance_value. payment_method forçado 'convenio' (era 'asaas')
- Popover: ev.price era null em convênio → normalize expõe insurance_value
e paymentLabel faz fallback. Linha mostra "A receber R$ X" corretamente
- /financeiro: branch novo pra payment_method='convenio' → pill violeta
com pi-id-card (antes ficava sem indicador, igual particular)
Cenário 6 (recorrente sem pacote, Maria Magali) — materialização
- chargeMode='none' não materializava a 1ª (todas viravam virtuais, sem
badge $). Agora materializa a 1ª no fluxo de criação recorrente
- Bug intermediário: usei 'paciente_id' (Portuguese) mas agendaRepository
dropa esse campo. Corrigido pra 'patient_id' (English DB column)
Atalho "Gerar fatura" no popover
- Pill amber pequeno ao lado de "A cobrar R$ X" no popover (paymentVariant
='none' + sessão materializada)
- Wire em MelissaLayout via emit gerar-cobranca + handler onGerarCobrancaQuick
(chama gerarCobrancaManual, fecha popover pra impedir double-click)
- Bulk-load do useMelissaAgenda e fetchRecord do AgendaEventoFinanceiroPanel
agora filtram status='cancelled' (resolve badge $ residual + botão sumido)
Header do popover: info de pacote/série
- "Sessão · Pacote · N sessões" ou "Sessão X de Y" abaixo do tipo
(computed seriesLabel lê do _raw da rule)
Título do dialog "Sessão do Pacote · Sessão"
- Quando commitment name é "Sessão" (default), drop pra evitar duplicação
- Outros nomes (Avaliação, etc) permanecem com forma completa
Excluir série inteira (popover)
- Novo botão "Excluir série" no popover quando evento pertence a recorrência
- Hard delete: financial_records pendentes → agenda_eventos materializados
→ recurrence_rules (CASCADE leva exceptions + rule_services)
- Bloqueia se algum record tem status='paid' (estornar primeiro)
cancel_session some da agenda
- useRecurrence.expandRules agora pula occurrence com exception type=
'cancel_session' (era visível com status cancelado; doc dizia "some
da agenda" mas código mantinha. Honra a promessa do diálogo)
- patient_missed / therapist_canceled / holiday_block permanecem visíveis
como histórico
UX outros
- "+ Novo convênio" toolbar em ConfiguracoesConveniosPage (botão faltava
— empty state mandava clicar em botão inexistente)
- InsurancePlanServiceQuickCreateDialog: cadastrar procedimento POR CIMA
do AgendaEventDialog sem sair da agenda. Auto-seleciona quando nada
estava selecionado antes
- Hint contextual abaixo do card Sessão/Honorários: convênio = "Nº guia
opcional"; gratuito = "sem cobrança". Particular sem hint
- recurrence_exceptions cancel agora usa upsert com onConflict
(idempotente, não quebra com unique violation em re-cancel)
- goToConveniosConfig removida (dead code após quick-create inline)
CSS
- .aed-row-50 perdeu margin-bottom (user request)
- .field-card.mb-4 ganhou margin-top: 1rem (scoped a composer wrappers)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
BLOQUEIO REAL Inativo + AVISO Arquivado futura:
- useAgendaEventComposer.canSave agora bloqueia edicao de sessao futura
quando !perms.canReschedule. Antes canReschedule era dead code
(definido em getPatientAgendaPermissions mas nunca consumido). Pra
Inativo: canReschedule=false => Save desabilitado de verdade (antes
o aviso "Remarcacao bloqueada" mentia, save acontecia mesmo).
- Aviso novo (severity=info) em AgendaEventDialog + V2 pra Arquivado +
futura + edit: "Sessao futura editavel; novos agendamentos e
recorrencias bloqueados". Cobria um gap onde nao havia aviso nenhum
pra esse cenario.
RESUMO FLUTUANTE acompanha o Dialog:
- ResizeObserver no .p-dialog.agenda-event-composer mede top + altura
e sincroniza com :style do aside via ref resumoStyle. Antes o aside
tinha top:5vh fixo — dialog baixo (Bloqueio/Atividade) centrava
vertical e o resumo ficava preso la em cima desalinhado.
CARD "Campos Extras (compromisso)":
- Bloco de selectedCommitmentFields extraido da fields-grid pra um
.field-card proprio com header pi-list + titulo + .aed-extras-body
(padding 0.85rem). Hierarquia visual clara: campos do compromisso
ficam isolados dos campos do form principal.
- Bind especial pra f.key==='notes': v-model="form.observacoes" em vez
de form.extra_fields.notes. Mantem compat com a coluna nativa
agenda_eventos.observacoes (consumida por relatorios/prontuario).
- Textareas hardcoded de Observacao removidas do form (fields-grid +
side-card direito) — agora vem como campo extra do commitment Sessao,
via migration 20260511000001 (commit anterior).
OUTROS:
- Card "Pagamento" renomeado pra "Sessao / Honorarios" (cobre os 3
tipos: gratuito, particular, convenio — terminologia mais alinhada
ao vocabulario clinico).
- composer-grid e composer-right ganharam gap:0 — os cards filhos
ja tem mb-4 proprio (Tailwind ~1rem), gap do flex duplicava.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PROBLEMA 1 — recorrencias virtuais nao apareciam em listas de sessao
============================================================
Sistema cria 1 row real em agenda_eventos por recorrencia (a primeira
ocorrencia) + 1 regra em recurrence_rules. As N-1 sessoes seguintes sao
geradas em runtime via useRecurrence.loadAndExpand. AgendaTerapeutaPage e
AgendaClinicaPage ja usavam loadAndExpand, mas composables compartilhados
("Hoje", widget, prontuario, ver todas) so liam agenda_eventos direto —
serie semanal de 4 sessoes aparecia como 1.
Fix em 3 composables cross-layout:
- usePatientSessions.load — range padrao -6mo a +12mo, filtra virtuais
por patient_id apos loadAndExpand. Cobre MelissaPaciente Tab Agenda +
PatientProntuario legacy.
- useMelissaEventos._fetchRange — merge real + virtual no range visivel.
Cobre widget "Hoje" (MelissaLayout), mini-cal, fallback standalone do
MelissaAgenda. Falha do expand cai silencioso pra so-reais.
- useMelissaTodasSessoesPaciente.fetch — mesma logica do paciente, range
-6mo a +12mo. Cobre "Ver todas as sessoes" do MelissaAgenda.
normalizeEvent agora aceita shape de virtual (paciente_nome/patient_name)
alem de joined query (patients.nome_completo). Expoe is_occurrence +
recurrence_id pra consumidores diferenciarem.
PROBLEMA 2 — UPDATE em id virtual quebra com "invalid input syntax for type uuid"
============================================================
Apos #1, ocorrencias virtuais aparecem na UI. Quando o user mudava status
(via botoes do MelissaEventoPanel, watcher do form.status no
AgendaEventDialog, ou botoes diretos no MelissaPaciente Tab Agenda), o
UPDATE caia direto no PostgreSQL com id "rec::ruleId::date" — sintaxe
invalida pra coluna UUID.
Materializacao em 4 caminhos:
- usePatientSessions.updateStatus(sessionOrId, status) — aceita row inteira
agora. Se virtual, busca row real por recurrence_id+date, ou cria nova
copiando campos da virtual (com status aplicado).
- useAgendaEventActions watcher do form.status — emit('updateSeriesEvent',
{ ..., row: form }) em vez de UPDATE direto. Parent materializa.
- MelissaLayout.updateEventoStatus — detecta virtual, delega pro
M.onUpdateSeriesEvent passando row: ev (sem isso, dialogEventRow ficaria
vazio porque user nao abriu o dialog antes — criava row orfa sem
patient_id).
- MelissaPaciente — @updateSeriesEvent do dialog local aponta pro
onSessaoDialogUpdateSeries (wrapper que delega pro composable que sabe
materializar). Antes apontava pro save normal.
useMelissaAgenda.onUpdateSeriesEvent atualizado:
- aceita row opcional do chamador (prioridade > dialogEventRow > vazio).
- guard: aborta com toast se rid (recurrence_id) for null, em vez de
criar row orfa.
- error check no .maybeSingle (antes ignorado — query falhando seguia pro
insert e duplicava sessoes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picker de paciente: button list -> DataTable (.aed-patient-dt) com
rowClass condicional pra blocked + Tags Arquivado/Inativo + ordenacao
Ativo > Inativo > Arquivado. Pareia com selectPaciente do composable
(commit anterior).
Time picker: header com header-dot colorido + titulo dinamico
"Nova {commitment.name}" + subtitulo "Inicio da sessao e duracao"
(espelha o header do dialog principal). Inicio e Termino lado a lado
(Termino readonly, derivado de fimDateTime). Cards "Horarios disponiveis"
(.aed-card) + chips de duracao rapida (.aed-pill, 30/50/60/90m) + Select
"Outra" pareando com AgendaEventDialogV2. Card de Termino destacado
embaixo da Duracao removido (info ja vai no input ao lado do Inicio).
Mini calendar (.mc-mini) estilo MelissaAgenda mini-cal — grid 6x7, sem
dots/feriados, sync com form.dia ao abrir.
Cadastro completo inline: importa PatientCadastroDialog dentro do dialog
em vez de redirecionar pra rota nova-aba (vazaria do layout Melissa).
Botao pi-id-card no patient-hero abre. Usa prop hideViewListButton
adicionada antes pra esconder "Salvar e ver pacientes".
Popovers de ajuda nos InputGroups do card Pagamento (servico/convenio
help refs separados pra nao colidir).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AgendaEventDialogV2.filteredPatients agora mostra TODOS os pacientes
(antes filtrava status='Ativo' silenciosamente), ordenados Ativo > Inativo
> Arquivado. Items nao-Ativo vem com Tag colorida + disabled + tooltip
explicativo — UX clara: o paciente aparece (user nao "perde" no search)
mas nao da pra agendar.
selectPaciente bloqueia non-Ativo (defesa em camadas: template ja marca
disabled, mas se alguem chamar a funcao programaticamente por cache stale
etc, a regra continua valendo). Copia status pro form pra canSave aplicar
getPatientAgendaPermissions corretamente.
3 specs novas em useAgendaEventPickerBilling.spec cobrem o bloqueio +
copia do status.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
services: useServices.save e ServiceQuickCreateDialog agora validam nome
unico por owner (ilike, case-insensitive; ignora self no update). Antes
era possivel criar dois servicos com nome igual via paths diferentes.
cadastro in-flow: ComponentCadastroRapido e PatientCadastroDialog ganham
prop hideViewListButton. Quando true (uso dentro de outro fluxo, ex:
cadastrar paciente direto no AgendaEventDialog), esconde "Salvar e ver
pacientes" — navegar pra lista abandonaria o evento em edicao.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sem long-press delays customizados, tap em slot vazio precisa de 1000ms
antes de disparar select — diverge totalmente do mouse (clique abre na
hora). Mesmo problema em eventDrop. Move pra utils/fcDefaults.js e
aplica nos 4 calendars (AgendaCalendar, AgendaClinicMosaic,
AgendaTerapeutaPage, MelissaAgenda no proximo commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User apontou que jornada de trabalho, frequencia e billing (particular/
convenio/gratuito) sumiam quando o dialog abria do prontuario. Causa:
meu watch original do commit 30d09eb so forcava step.value=2 sem
inicializar form.commitment_id. Sem commitment, o computed
isSessionEvent virava false e esses 3 blocos do template (que dependem
de isSessionEvent) ficavam ocultos:
- jornadaDialog: <Message v-if="jornadaDialog && isSessionEvent">
- frequencia: bloco v-if="!hasSerie" tem gates internos de billing/
patient que dependem de isSessionEvent
- billing radio (particular/convenio/gratuito): isSessionEvent
FIX: watch agora chama selectCommitment(sessao) — exatamente o que o
user faria clicando no card "Sessão" no step 1. Isso seta:
- form.commitment_id pro id do native_key='session'
- form.extra_fields = {} populado pelos fields do commitment
- step.value = 2
Adicionei props.commitmentOptions ao watch dep — necessario pq quando
o dialog abre antes do tenant load terminar, commitmentOptions chega
vazio inicialmente. Watch re-roda quando popula.
Idempotente: so chama selectCommitment se ainda nao tem commitment_id
ou se id atual nao bate com sessao.id (re-open com mesmo lockType
nao reinicializa redundantemente).
301 specs do agenda continuam passando. ESLint: 31 errors pre-existentes
(mesmos do commit anterior).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User escolheu caminho A: modificar AgendaEventDialog em vez de copiar.
Mudancas SAO ADITIVAS — comportamento atual dos 5 callsites legacy
(TherapistDashboard, PatientsListPage, MelissaAgenda,
MelissaAgendamentosRecebidos, MelissaLayout) preservado.
VALIDACAO: rodei os 7 spec files do agenda — 301 testes passaram.
Zero regressao.
ADICIONADO em src/features/agenda/components/AgendaEventDialog.vue
- Prop lockType (Boolean, default false): pula step 1 (escolha de tipo)
e vai direto pro form. Watch immediate em [lockType, modelValue]
forca step.value=2 quando lockType=true e dialog abre.
- Prop lockPatient (Boolean, default false): esconde botoes "trocar"/
"limpar" do paciente. Mostra icon de lock com tooltip "Paciente do
prontuario". Cobre o cenario "criar sessao pra paciente fixo" sem
precisar do isEdit que o patientLocked computed exige.
- Slot #headerLeft: substitui o conteudo esquerdo do header (default
era header-dot + headerTitle + previewRange). Permite callsites
customizar com icon+title+subtitle proprios sem mexer no resto do
header (X / actions).
- v-if no Step 1: "step === 1 && !lockType"
- v-if nos buttons trocar/limpar: "!patientLocked && !lockPatient"
- Lock icon: "patientLocked || lockPatient" + tooltip dinamico
MELISSAPACIENTE.VUE
- Reverte o inject-only do commit 88dff50.
- Mantem o inject(MELISSA_AGENDA_KEY) APENAS pra LER dados pesados
(commitmentOptions, workRules, allEvents, agendaSettings, feriados,
ownerId, tenantId) — evita re-fetch.
- State LOCAL pro dialog: sessaoDialogOpen, sessaoDialogEventRow,
sessaoDialogStartISO, sessaoDialogEndISO. Nao colide com o dialog
global do MelissaLayout que continua na Agenda.
- goAgendar(): inicializa eventRow com paciente_id fixo + tipo='sessao'
+ defaults razoaveis (proximo slot 15min + duracao da agenda),
abre o dialog local.
- Handlers onSessaoDialogSave / onSessaoDialogDelete delegam pros
handlers globais (M.onDialogSave/Delete) e ao final refetcham
sessions+recorrencias do paciente in-place.
- Render <AgendaEventDialog> com lock-type=true + lock-patient=true
+ slot #headerLeft custom (icon pi-calendar-plus em quadrado
primary 40x40 + "Nova sessão" + nome do paciente como subtitulo).
Resultado: prontuario tem o MESMO componente da Agenda (form completo
de sessao, frequencia com preview de ocorrencias + conflitos,
vinculacao de servicos/billing, edicao de serie, etc) mas pre-fixado
no contexto do paciente, com header proprio e single source of truth.
ESLint: 31 errors pre-existentes em ambos arquivos (variaveis declaradas
nao usadas — confirmado via git stash baseline). 0 errors da minha
mudanca.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprints B (05-03) e C (05-04) acumulados:
- NotificationDrawer/Item redesign (visual mais limpo, ações inline)
- Dock pins compose (useMelissaDockPins) + cache store global (melissaCacheStore)
- MelissaAgenda: timeline FullCalendar parity + cards resumo, histórico
card com useMelissaAgendaHistorico, MelissaEventoPanel ajustado
- useFeriados: cache opt-in pra evitar fetch redundante de feriados
- PatientProntuario: aba Visão Geral nova; PatientConversationsTab polish
- AgendaClinicMosaic / AgendaTerapeutaPage / useAgendaSettings: ajustes
de paridade com Melissa
- DocumentsListPage: pequenos ajustes
- DB migration 20260504000001: fix do trigger pra status 'excluido' nas
cancel_notifications
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
=== 8.2 Botão "Lembrar paciente" na agenda ===
Edge nova send-session-reminder-manual:
- Recebe {event_id}, autoriza (member ativo do tenant), resolve template
lembrete_sessao (custom → default global), envia via Evolution, registra
outbound em conversation_messages + log em session_reminder_logs com
reminder_type='manual'.
- Reusa lógica do cron reminders (sanitização, fmt datas, render template)
mas sem janela/dedup — terapeuta pode redisparar quantas vezes quiser
(log usa UPSERT; UNIQUE (event_id, reminder_type) sobrescreve).
Migration 20260423000008 adiciona 'manual' ao CHECK constraint de
session_reminder_logs.reminder_type.
UI: botão verde pi-whatsapp no footer do AgendaEventDialog (só em edit
de sessão com paciente vinculado). Confirm dialog + toast + erros
amigáveis (no_phone, invalid_phone, no_active_channel, template_not_found,
forbidden, send_failed).
=== 8.3 Status sessão dispara mensagem ===
Migration 20260423000009 cria trigger AFTER UPDATE OF status em
agenda_eventos: quando status muda pra cancelado/remarcado/confirmado,
dispara edge send-session-status-notification via pg_net (não bloqueia
o UPDATE). Settings app.settings.supabase_url/service_role_key reusadas.
Edge nova send-session-status-notification:
- Body {event_id, old_status, new_status}
- STATUS_TEMPLATE_MAP: cancelado→cancelamento_sessao, remarcado→
remarcacao_sessao, confirmado→confirmacao_sessao.
- Respeita opt-out (conversation_optouts), canal ativo, template
existente (tenant-specific → global default). Skip silencioso em
caso de falta de config.
- Insere outbound em conversation_messages (sem log unique — múltiplas
mudanças de status geram múltiplas mensagens por design).
=== 8.4 Intake abandonado vira lead no CRM ===
Migration 20260423000010:
- Adiciona 'in_progress' e 'abandoned_lead' ao CHECK de
patient_intake_requests.status. Colunas last_progress_at e
lead_thread_key.
- RPC convert_abandoned_intake_to_lead(intake_id): cria mensagem
placeholder inbound no CRM do tenant (thread_key anon:{phone}) +
conversation_notes com resumo dos dados coletados + marca status.
Edge save-intake-progress:
- POST {token, nome_completo?, telefone?, email_principal?, ...}
- Whitelist de campos (ALLOWED_FIELDS) pra proteger contra POST
malicioso tentar setar status/owner/etc.
- Busca por token, set status='in_progress' se era 'new', atualiza
campos enviados + last_progress_at.
Edge convert-abandoned-intakes (cron):
- Body opcional {idle_minutes} (default 30).
- Varre patient_intake_requests status='in_progress' + last_progress_at
mais antigo que cutoff. Filtra só os com nome_completo OU telefone
(contato mínimo pra valer lead). Chama RPC pra cada um.
Hook no form público CadastroPacienteExterno:
- Watch em nome_completo, telefone, email_principal, onde_nos_conheceu
dispara scheduleProgressSave() com debounce 1.5s.
- savePartialProgress só chama a edge se tem nome OU telefone.
- Silent fail — autosave não é crítico.
Cron do convert-abandoned-intakes NÃO ativado automaticamente (igual
heartbeat/SLA). Template comentado não está na migration — admin
descomenta SELECT cron.schedule manualmente quando quiser ligar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>