Commit Graph

72 Commits

Author SHA1 Message Date
Leonardo 0aabea7753 templates/editor: corpo do editor ocupa 100% da altura disponivel
Antes: minHeight 450 em pixel fixo do Jodit limitava o corpo —
sobrava area vazia abaixo do editor.

Fix CSS-only (sem mexer no JoditEmailEditor compartilhado):

- .dte-main__editor: overflow hidden + flex column (era overflow-y
  auto). O scroll passa pra dentro do Jodit (workplace).
- .dte-editor-wrap: flex 1 + min-height 450 (preserva minimo).
- :deep(.jodit-container/workplace/wysiwyg) force flex + height
  100% + min-height 0/100% pra anular o height: 450px que o Jodit
  seta inline.

Resultado: editor sempre preenche toda area disponivel da COL 2,
expande/contrai com a janela, e o scroll do conteudo fica dentro
do proprio editor (jodit-wysiwyg).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:02:04 -03:00
Leonardo 80cce772db templates/editor: layout 3-col + tabs cabecalho/corpo/rodape
Refatora DocumentTemplateEditor em 3 colunas seguindo padrao
MelissaAgendaConfig:

- COL 1 (esquerda, 240-280px): form de metadados — nome, tipo,
  descricao (Textarea com autoResize), URL do logo
- COL 2 (centro, flex 1): sub-tabs Cabecalho/Corpo/Rodape, 1
  editor visivel por vez. Cada editor com minHeight: 450px (era
  120/350/120). Tab ativa destacada com border-bottom primary +
  background sutil.
- COL 3 (direita, 220-260px): variaveis agrupadas por categoria,
  hint dinamico mostrando qual sub-tab esta ativa ("Clique para
  inserir no Cabecalho/Corpo/Rodape"). Botoes com {{ }} braces
  em monospace + cor primary.

Scroll interno:
- .dte-page flex column, gap 12, min-height 0, padding 12
- Cada coluna eh card (border + radius) com header sticky + body
  scrollable interno (overflow-y: auto, scrollbar-width: thin)
- Variaveis com max-height proprio + scroll interno

Mobile (<1024px):
- 3-col vira 1-col stacked
- Container do .dte-cols ganha overflow-y auto (scroll da pagina
  inteira em vez de scroll interno em cada coluna)
- Variaveis ganha max-height 320px pra nao ocupar a tela toda

Preview (toggle no topo):
- Documento A4-like centralizado (max-width 794px ≈ 96dpi)
- Padding 48/56px, shadow sutil
- Mobile: padding reduzido pra max width disponivel

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:59:21 -03:00
Leonardo 6807b447cb agenda Fase D: adapter Clinica usa agendaBilling.service
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>
2026-05-21 09:48:18 -03:00
Leonardo 034c2c0f3d agenda Fase C: adapter Rail usa agendaBilling.service
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>
2026-05-21 09:46:35 -03:00
Leonardo 049dd91b9b agenda Fase B2: extrai mutations pro agendaBilling.service
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>
2026-05-21 09:37:09 -03:00
Leonardo e7e3d1beb1 agenda Fase B1: agendaBilling.service (read-only + helpers puros)
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>
2026-05-21 09:30:06 -03:00
Leonardo ee117eafe6 agenda Fase A: extrai utils puros pra features/agenda/utils
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>
2026-05-21 09:21:12 -03:00
Leonardo 2dae4a11ae roadmap #11: recently viewed (ultimos 5 pacientes acessados)
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>
2026-05-21 05:17:51 -03:00
Leonardo 6ae651a8ae roadmap #14: recibo profissional PDF — gerador + quick path da agenda
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>
2026-05-21 05:05:17 -03:00
Leonardo 19caa42f3b compliance CFP #7: dialog gera share_link junto com signature
DocumentSignatureDialog (terapeuta-side) ja existia com fluxo de
add signatarios. Estendido pra:
  - Checkbox "Gerar link publico para assinatura" (default ON)
  - Select de validade (24h/3d/7d/30d, default 7d)
  - Apos submit: alem de createSignatureRequests chama createShareLink
    e exibe o URL gerado num bloco emerald com botao Copy
  - Dialog fica aberto se gerou link (terapeuta copia/envia); fecha
    se nao gerou

Fluxo end-to-end agora funcional: terapeuta clica "Solicitar
assinatura" no DocumentsListPage > preenche signatarios > submit
gera signature requests + share_link > copia URL > envia via WA/
email > paciente abre /shared/document/:token > assina via fluxo
publico (RPC sign_document_by_token capturando IP/UA server-side).

Fecha ROADMAP #1.2 #6/#7 — Compliance basico BR completo (#5/#6/#7/
#8/#9 todos verdes, #6 com TCLE + Telehealth + TCLE menores + termo
sigilo + LGPD + autorizacao gravacao).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:52:01 -03:00
Leonardo 4e42881d5e compliance CFP #7: portal + fluxo de assinatura no SharedDocumentPage
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>
2026-05-21 04:49:27 -03:00
Leonardo 934c620295 compliance CFP #7: RPCs de assinatura + service ext + composable
Backend foundation pra assinatura eletronica (ROADMAP #1.2 #7).

Migration 20260521000006 cria 3 RPCs:
  - sign_document_by_signature_id (paciente logado, SECURITY INVOKER)
  - sign_document_by_token        (terceiro via share link, SECURITY DEFINER)
  - get_signable_document_by_token (preview pre-assinatura)

IP + user-agent capturados SERVER-SIDE via inet_client_addr() e
current_setting('request.headers'). Hash SHA-256 vem do cliente
pra integridade. Token via share link incrementa usos no UPDATE.

DocumentSignatures.service estendido com 3 wrappers RPC: signByPortal,
signByToken, getSignableDocumentByToken. useDocumentSignatures composable
novo (Tipo A blueprint) expoe state reativo + acoes: fetchForDocument,
requestSignatures, sign, refuse, signWithToken, loadByToken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:39:05 -03:00
Leonardo de3898878a asaas: Tier 1 Fase A foundation — migrations + service + edge function stubs
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>
2026-05-21 04:20:52 -03:00
Leonardo ee2967a075 M6: notices/conversations foundation — selects + repositories
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>
2026-05-21 04:20:42 -03:00
Leonardo 0956e4facc M5: tenantship + admin members + accept_invite RPC
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>
2026-05-21 04:20:33 -03:00
Leonardo fbfb95648e M4: financeiro foundation — services + composables paralelo
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>
2026-05-21 04:20:23 -03:00
Leonardo 388e9a4186 M3: prontuario foundation — repositories + composables clinical_notes
Modulo 3 da Fase 1. 6 arquivos novos em features/patients/prontuario/:
services (_tenantGuards, clinicalNotesSelects, clinicalNotesRepository,
clinicalNoteTemplatesRepository) + composables (useClinicalNotes,
useClinicalNoteTemplates). Ativa quando migrations 0.5.B (clinical_notes
tables/rls/versioning + documents link) forem aplicadas no banco.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:20:15 -03:00
Leonardo 1c2a2b6e19 M2: patients — selects + repository + 8 composables refatorados
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>
2026-05-21 04:20:08 -03:00
Leonardo 27467bbb68 M1: features/medicos + features/insurance + ComponentCadastroRapido refactor
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>
2026-05-21 04:19:57 -03:00
Leonardo f94a4ae97f padronizacao: foundation Fase 0+0.5 — blueprints + auditoria + clinical_notes
Pre-MVP: 3 blueprints canonicos (repository, composable, quick-create
overlay), AUDIT_BASELINE com 51 divergencias em 6 modulos, estrategia
PADRONIZACAO de 4 fases, DESIGN_BILLING_ORCHESTRATOR. Schema clinical
notes pronto pra Fase B (4 migrations + seed templates). AgendaEvent
Dialog.vue.bak deletado (lixo de refator anterior).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:19:45 -03:00
Leonardo 00c4168393 agenda: C12 prep — detectar paid pre-existente em pacote saldo realizada
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>
2026-05-20 12:28:19 -03:00
Leonardo 5684297243 agenda: reverse transition trava (Agendada apos artefatos)
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>
2026-05-20 11:40:19 -03:00
Leonardo 079509e001 agenda: dialog pacote saldo realizada — 2 sub-questions claras
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>
2026-05-20 11:11:28 -03:00
Leonardo 753182cfad agenda: C10 pos-test fixes + lock sessao encerrada + addendum doc
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>
2026-05-20 09:59:05 -03:00
Leonardo ec0a24f5c8 agenda: C9 OK + rowGroup por paciente em /financeiro + bubble cobranca-atualizada
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>
2026-05-19 23:55:06 -03:00
Leonardo fad1f4ebd4 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>
2026-05-19 23:27:20 -03:00
Leonardo 1feb7112ff agenda: C7 OK + Fase 6 lock-edit ativada em Melissa + cross-week payment propagation
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>
2026-05-19 20:54:23 -03:00
Leonardo c23d0a574f agenda: C5+C6 testes OK + atalho Gerar fatura + RPC idempotência fix
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>
2026-05-19 16:23:42 -03:00
Leonardo e95ed9b585 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>
2026-05-19 08:31:18 -03:00
Leonardo 41c44272a3 agenda: dialog UX (bloqueio paciente arquivado/inativo + resumo sticky + card extras + observacao via commitment)
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>
2026-05-11 16:57:16 -03:00
Leonardo 39cf0178e6 agenda: expandir e materializar ocorrencias de recorrencia (cross-layout)
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>
2026-05-11 10:46:58 -03:00
Leonardo 988a4e5892 agenda: AgendaEventDialog overhaul — picker DataTable + time picker + cadastro inline
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>
2026-05-11 10:46:15 -03:00
Leonardo 8f4e6679eb agenda: pacientes arquivados/inativos visiveis e bloqueados no picker
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>
2026-05-11 10:45:57 -03:00
Leonardo 8e3c09d1b1 agenda: services nome unico por owner + cadastro in-flow
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>
2026-05-11 10:44:27 -03:00
Leonardo 8b0e633aac agenda: centralize FullCalendar touch defaults
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>
2026-05-11 10:44:16 -03:00
Leonardo 73788c7031 AgendaEventDialog: lockType auto-seleciona commitment "Sessão" (fix jornada/billing/freq sumidos)
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>
2026-05-08 19:33:08 -03:00
Leonardo 30d09eb2ac AgendaEventDialog: props lockType + lockPatient + slot #headerLeft (additivos)
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>
2026-05-08 19:27:32 -03:00
Leonardo 9e76e4e6ea MelissaPaciente: bloco "Recorrencias do paciente" na Tab Agenda
User aprovou a ideia. Adiciona contexto "este paciente tem sessao toda
segunda 14h" direto no prontuario, evitando duplicacao de regras e
deixando claro o estado da serie.

NOVO src/features/patients/composables/usePatientRecurrences.js (~110L)
- load(patientId): SELECT recurrence_rules WHERE patient_id (DESC start_date)
- cancel(ruleId) / reactivate(ruleId): UPDATE status + auto-reload
- Computeds derivados: ativas, canceladas, totalAtivas, totalCanceladas
- busy flag pra disable de buttons

EXTENSAO src/features/patients/utils/patientFormatters.js
- WEEKDAY_LABEL + WEEKDAY_LABEL_SHORT (arrays 0=Domingo..6=Sabado)
- fmtRecurrenceLabel(rule): "Toda segunda às 14:00", "Quinzenal · Terça
  às 09:00", "Qua, Sex às 16:00" (custom_weekdays), "Mensal às 14:00",
  "Anual" — cobre todos os types do useRecurrence.
- fmtRecurrenceFim(rule): "Sem data de fim" / "Até DD/MM/YYYY" /
  "N sessões no total"

MELISSAPACIENTE.VUE
- Composable + handlers (onCancelRecurrence, onReactivateRecurrence) com
  toast feedback.
- recorrenciasShowCanc ref + recorrenciasVisiveis computed (toggle "ver
  canceladas").
- loadAll inclui recorrenciasHook.load.
- salvarSessao no caminho recorrente recarrega sessions+recorrencias em
  Promise.all (regra recem-criada aparece na lista imediatamente).
- 5o KPI na Tab Agenda: "Recorrencias" com count ativas + cap dinamica
  (cor #a855f7 quando > 0, cinza quando 0).
- Bloco <section class="mpa-panel"> entre KPIs e filter chips listando
  rules ativas (default) ou todas (toggle "Ver canceladas" no header,
  so aparece quando ha canceladas):
  - Icon roxo .mpa-recur-item__icon
  - Top: label + Tag status (verde Ativa / amarelo Cancelada)
  - Meta: duracao + modalidade + fim + "desde DATE"
  - Obs (quando preenchido): block textual
  - Actions: pi-ban (cancelar) ou pi-undo (reativar) com tooltip
- border-left adaptativa (#a855f7 ativo / cinza cancelado) + opacity 0.7
  pros cancelados.
- Mobile: stack icon+main em 2-col 2-row; actions full-width abaixo.

CSS: ~120L novos. Padrao Melissa: status pills, icon roxo distintivo
(diferente das sessoes que usam cinza), border-left por status.

ESLint: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:37:45 -03:00
Leonardo a8ab13b201 MelissaPaciente: dialog inline nova sessao + createSession mutation
Espelha o padrao do "Lancamento" mas pra agenda — botao "Agendar" agora
navega pra aba Agenda e abre dialog de nova sessao.

NOVO em src/features/patients/composables/usePatientSessions.js
- createSession(patientId, payload) — INSERT agenda_eventos com
  status='agendado', resolve owner_id (auth.getUser) e tenant_id (lazy
  import tenantStore). Auto-reload via _lastPatientId.
  Validacao: inicio_em + fim_em obrigatorios.
  Retorna {ok, data?, error?}.

NOVO em MelissaPaciente.vue
- Refs novaSessaoOpen + novaSessaoForm (tipo/data/hora/duracao_min/
  modalidade/titulo_custom/observacoes)
- 3 catalogos:
  - SESSAO_TIPOS: Sessao/Primeira/Retorno/Avaliacao/Devolutiva
  - SESSAO_DURACOES: 30/40/45/50/55/60/90/120 min
  - SESSAO_MODALIDADES: Presencial/Online
- goAgendar() agora alem de navegar pra aba Agenda, tambem inicializa
  o form (default amanha 09:00, sessao 50min presencial) e abre o dialog.
- salvarSessao() handler com validacao (data + hora) e construcao de
  inicio_em/fim_em a partir de data + hora + duracao_min. Local time
  -> ISO via Date constructor.
- <Dialog> 460px com form: Tipo + grid 2-col (data + hora) + grid 2-col
  (duracao + modalidade) + titulo opcional + observacoes Textarea.
- CSS .mpa-novo-lanc__opt pra "(opcional)" em cinza.

Validacoes:
- Data e hora obrigatorios (warn toast)
- Date constructor invalido -> warn toast

Pra criar sessoes mais complexas (recorrencia, multi-paciente, conflitos
de agenda), o user vai pra MelissaAgenda direto que tem o
AgendaEventDialog completo. Aqui no prontuario eh o caminho rapido.

ESLint: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:20:00 -03:00
Leonardo 64005a5b07 MelissaPaciente: fix openWhatsapp + dialog inline novo lancamento financeiro
DOIS BUGS DE COMPORTAMENTO:

1. openWhatsapp nao abria o drawer
   conversationDrawerStore.openForPatient(patientId) espera STRING id,
   nao objeto. Eu passava { id, name, phone, avatar_url } — store
   ignorava e drawer nunca abria.
   FIX: passar String(props.patientId) (mesmo pattern que MelissaPacientes).
   BONUS: a store seta this.error sem dar throw quando paciente nao tem
   telefone cadastrado. Detectamos com `if (err && !isOpen)` e mostramos
   toast warn com a mensagem da store ("Paciente sem telefone cadastrado").
   Funcao virou async pra aguardar o openForPatient.

2. addFinancial era placeholder "Em breve"
   User correto: o sistema ja tem suporte (composables/useFinancialRecords
   tem createManualRecord). Implementado dialog inline simples no
   prontuario.

NOVO em src/features/patients/composables/usePatientFinancial.js
- createRecord(patientId, payload) — INSERT financial_records com
  type='receita', resolve owner_id (auth.getUser) e tenant_id (lazy
  import tenantStore pra evitar circular). Auto-reload via _lastPatientId.
  Retorna {ok, data?, error?}.

NOVO em MelissaPaciente.vue
- Refs novoLancOpen + novoLancForm (description/amount/due_date/payment_method)
- PAYMENT_METHODS array (Pix/Cartao/Dinheiro/Transferencia/Boleto/Convenio)
- addFinancial() agora abre o dialog (era toast "em breve")
- salvarLancamento() handler com validacao (valor > 0, due_date obrigatorio)
- <Dialog> v-model:visible 420px com form: descricao + grid 2-col
  (valor InputNumber BRL + vencimento date input) + select forma
- CSS .mpa-novo-lanc + responsive (1-col em <540px)

ESLint: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:12:22 -03:00
Leonardo 167e864b8a MelissaPaciente Fase 7: Tabs Documentos + Conversas (KPIs + embed componentes existentes)
Duas tabs entregues numa sessao — sao mais leves porque reusam
DocumentsListPage e PatientConversationsTab existentes (testados em
producao no PatientProntuario legacy) com KPIs Melissa por cima.

EXTENSAO src/features/patients/utils/patientFormatters.js
- fmtSize(bytes): legivel B/KB/MB/GB
- DOC_TYPE_LABEL map: atestado/receita/laudo/encaminhamento/termo/etc
- chConvLabel(c): whatsapp -> WhatsApp / sms -> SMS / email -> E-mail

EXTENSAO src/features/patients/composables/usePatientDocuments.js
- topType computed: { tipo, count, label } do tipo mais comum
- pendentes computed: count status_revisao === 'pendente'
- sizeTotalFormatted computed: fmtSize(totalBytes)

EXTENSAO src/features/patients/composables/usePatientMessages.js
- primeiraMensagem computed (mais antiga)
- canais computed: Set de m.channel unicos

MELISSAPACIENTE.VUE — Tab Documentos
- 4 KPIs adaptativos (so renderizam com dados):
  Total + sizeTotalFormatted / Mais comum / Ultimo / Revisao pendente
- DocumentsListPage embedded no card Melissa (mpa-embed wrapper).
  Reusa upload/preview/listagem testados.

MELISSAPACIENTE.VUE — Tab Conversas
- 4 KPIs: Mensagens com canais / Recebidas % / Enviadas % / Ultima
- CTA "Abrir conversa no drawer" estilo WhatsApp pill verde #25d366
  que emite open-whatsapp pro parent (Fase 8 integra com
  conversationDrawerStore.openForPatient)
- PatientConversationsTab embedded — thread completa com filter/media

CSS: ~50L novos (mpa-conv-cta + mpa-embed wrapper).

Removido kpiDocumentos nao usado (substituido por documentsHook.total
direto).

ESLint: 0 errors da minha mudanca.

PROXIMA: Fase 8 wire-up final (Dialog -> router.push em MelissaPacientes/
MelissaAgenda; decisao sobre TherapistDashboard + PatientsListPage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:06:29 -03:00
Leonardo e7c0f6c4f5 MelissaPaciente Fase 6: Tab Financeiro completa + mark paid (mutation que legacy nao tem)
EXTENSAO src/features/patients/utils/patientFormatters.js
- recordStatus(r): pago / vencido (paid_at NULL && due_date < hoje) / pendente
- RECORD_STATUS_LABEL map
- fmtPaymentMethod(v): PIX/Cartao/Dinheiro/Boleto/Transferencia/Convenio
  cobrindo variantes pt-br + camelCase

EXTENSAO src/features/patients/composables/usePatientFinancial.js
- ref `busy` + `_lastPatientId` interno
- recordsOrdenados computed: DESC por due_date com fallback created_at
- markPaid(recordId): UPDATE financial_records SET paid_at=NOW() +
  auto-reload via _lastPatientId. Retorna {ok, error?}
- markUnpaid(recordId): reverte (paid_at=NULL) + auto-reload

MELISSAPACIENTE.VUE — script
- Imports: recordStatus, RECORD_STATUS_LABEL, fmtPaymentMethod
- markRecordPaid(r): chama financialHook.markPaid + toast success/error
- revertRecordPaid(r): chama markUnpaid + toast

MELISSAPACIENTE.VUE — Tab Financeiro reescrita (substitui placeholder Fase 1)
- Loading state
- Empty state com CTA "Novo lancamento" (mpa-quick-btn--cta)
- 3 KPIs: Pago / Pendente com proxVenc / Em atraso (cor adaptativa
  vermelho quando > 0, cinza quando 0)
- Header "Lancamentos" com badge count + botao "+ Novo" no canto
- Tabela 6-col responsiva:
  - Vencimento (date mono + relative)
  - Descricao
  - Forma (PIX/Cartao/etc)
  - Valor (mono right-aligned)
  - Status pill colorida (verde pago / vermelho vencido / azul pendente)
  - Action button (pi-check verde marca pago / pi-undo amarelo reverte)
- border-left adaptativa por status
- Mobile: tabela colapsa em cards 2-col 4-row

DIFERENCA DO LEGACY: o PatientProntuario.vue exibe a tabela mas NAO
permite marcar pago/reverter direto dela. MelissaPaciente adiciona essa
acao inline (mutation auto-reload).

CSS: ~190L novos. Padrao Melissa: status pills com color-mix, JetBrains
Mono pra valores, header cell uppercase letter-spacing.

ESLint: 0 errors da minha mudanca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:57:42 -03:00
Leonardo 8a8d2e05bd MelissaPaciente Fase 5: Tab Agenda completa (KPIs + filtros + grupos por mes + acoes)
EXTENSAO src/features/patients/utils/patientFormatters.js: +2 helpers
- fmtHourShort (HH:MM 24h pt-br) — usado na coluna data dos cards
- fmtDayShort (DOW abreviado pt-br sem ponto) — usado na coluna data

EXTENSAO src/features/patients/composables/usePatientSessions.js
- Novo ref `busy` pra disable de buttons durante mutation
- _lastPatientId guardado internamente pra auto-reload
- Nova funcao `updateStatus(sessionId, novoStatus)` que faz
  supabase.from('agenda_eventos').update({status}) + auto-reload da
  lista de sessoes. Retorna {ok, error?}.

MELISSAPACIENTE.VUE — script
- agendaFilter ref ('all' default) + AGENDA_FILTERS array com 6 opcoes
  (Todas, Proximas, Passadas, Realizadas, Faltas, Canceladas)
- agendaSessoesFiltradas computed: filtra por future/past/status (regex)
- agendaAgrupadas computed: agrupa por "Mes de YYYY" DESC
- updateSessionStatus(ev, status, msg): chama sessionsHook.updateStatus +
  toast de sucesso/erro
- Removido `void toast` (toast usado de verdade agora)

MELISSAPACIENTE.VUE — Tab Agenda reescrita (substitui placeholder Fase 1)
- 4 KPI cards no padrao Visao Geral (numerados 01-04):
  Total / Realizadas (% do total) / Faltas (cor adaptativa) / Proxima
- 6 filter chips redondas (cor primary quando active)
- Empty state contextual (sem sessoes vs filtro vazio)
- Grupos por mes com header (label + badge count)
- Cards 3-col: data column (DOW + dia + hora) | main (status tag + chips
  modalidade/duracao + relative + titulo + note 2-line clamp) | actions
  (3 buttons: ok/warn/danger com tooltip + cor adaptativa no hover)
- Mobile: stack date+main em 2 cols; actions full-width abaixo

CSS: ~150L novos. Padrao visual Melissa: data column estilo calendario,
actions hover muda cor por intent (verde realiz / amarelo falta / vermelho
cancel), border-left por status.

ESLint: 0 errors da minha mudanca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:53:59 -03:00
Leonardo 4fc0e3a02b MelissaPaciente Fase 3: Tab Perfil completa (6 sections stacked + anchors)
EXTENSAO: src/features/patients/utils/patientFormatters.js
- +5 formatters: pickField (compartilhado), onlyDigits, fmtCPF (000.000.000-00),
  fmtRG (passthrough), fmtPhoneMobile ((XX) 9XXXX-XXXX), fmtGender
  (Masculino/Feminino/Nao-binario/Outro), fmtMarital (Solteiro/Casado/
  Divorciado/Viuvo/Uniao estavel).

MELISSAPACIENTE.VUE — script
- 30+ field computeds usando pickField (cobre snake_case + camelCase):
  birthValue, telefone/Alternativo, email/Alternativo, genero, estadoCivil,
  naturalidade, ondeNosConheceu, encaminhadoPor, observacoes, notasInternas
  + 8 campos de endereco + 5 dados adicionais + 4 responsavel.
- groupNames/groupLabel/groupCountLabel pra bloco Origem.
- scrollToProfileSection(key): liga sidebar sub-nav -> scrollIntoView do
  anchor #mpa-perfil-XXX. Em mobile fecha o drawer.

MELISSAPACIENTE.VUE — Tab Perfil reescrita
Diferente do PatientProntuario legacy que usa PrimeVue Accordion (1 painel
aberto por vez), o Melissa nativo mostra os 6 cards stacked com scroll
suave do sidebar sub-nav. Mais legivel em desktop, mais rapido de escanear.

- 1. Informacoes Pessoais: 2-col com Dados de cadastro (nome/data nasc
  com idade inline/genero/estado civil/CPF/RG/naturalidade) + Contato +
  Origem (grupos/tags chips/onde nos conheceu/encaminhado por). tel: e
  mailto: links onde ha valor. Observacoes full-width quando preenchido.
- 2. Endereco: grid 2-col com 8 fields.
- 3. Dados Adicionais: grid 2-col com escolaridade/profissao/parente/grau/
  tel parente.
- 4. Responsavel: 1-col com nome/CPF/tel + observacao block textual.
- 5. Anotacoes Internas: card com hint lock + textblock min-height.
- 6. Sessoes: lista compacta scrollable (max-height 360px) com titulo/
  data/duracao/modalidade chips + tag status.

CSS: ~250L novos pros componentes (mpa-fields/field-row/field-grid-2/
field-block/sess/sess-list). Pattern visual Melissa: cards com label
uppercase, separadores horizontais sutis, links primary, monospace pra
CPF/RG/CEP.

ESLint: 0 errors da minha mudanca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:43:03 -03:00
Leonardo ab7526b8d7 MelissaPaciente Fase 2: Tab Visao Geral completa (4 KPIs + timeline + msgs + notas)
Reescreveu o placeholder da aba Visao Geral por uma versao 1:1 do
PatientProntuario.vue legado, com estilo Melissa nativo e dados
alimentados pelos composables criados na Fase 1.

NOVO: src/features/patients/utils/patientFormatters.js (~165L)
- Helpers compartilhaveis extraidos do PatientProntuario:
  parseDateLoose, fmtDateBR, fmtDateTimeBR, fmtCurrency, fmtRelative
  (pt-br: "agora"/"ha 5 min"/"em 2 dias"/"ha 3 sem"), sessionDuration,
  calcAge.
- STATUS_LABEL e STATUS_SEVERITY pra mapear status de sessao (cobre
  variantes: realizado/realizada, falta/faltou, cancelado/cancelada).
- tagStyle com contraste auto (luminance WCAG-ish: bg colorido +
  texto preto/branco baseado em luminance < 0.45).
- Sera reutilizado pelas Fases 3-7 e na Fase 8 substitui as funcoes
  duplicadas do PatientProntuario.

EXTENSAO de composables (Fase 1):
- usePatientSessions: novo computed `ultimasAtendidas` (top 6 sessoes
  com status realiz/falt/cancel/remarc pra Timeline). totalRealizadas/
  Faltas/Canceladas refinados pra usar regex (cobre variantes pt-br).
- usePatientFinancial: novo computed `statusFinanceiro` que retorna
  { emDia: bool, proxVenc: record, totalPendente, totalPago, vencidos }
  pra alimentar KPI 02 com info detalhada de status financeiro.

MELISSAPACIENTE.VUE — Visao Geral reescrita:
- 4 KPI cards ricos (substituem os simples da Fase 1):
  - 01 Sessoes: realizadas / total + faltas + canceladas
  - 02 Pagamento: status (Em dia/atraso) + prox venc + cor adaptativa
    (vermelho atrasado / primary ok)
  - 03 Proxima sessao: relative + datetime + modalidade
  - 04 Mensagens: ultima relative + direction + count
- Grid 2-col abaixo (1.4fr / 1fr em >=900px):
  - Timeline coluna esquerda: dots coloridos por status, tags severity,
    chips modalidade + duracao, nota observacoes inline.
  - Coluna direita: Mensagens recentes (4) com border-left in/out +
    meta direction/relative + body 3-line clamp; Notas e observacoes
    em card papel com label uppercase e icone lock.
- Removeu kpiEmAberto/Atrasado nao usados (statusFinanceiro encapsula).

CSS: ~280L novos pros componentes (KPIs ricos, panel base, empty rich,
timeline, mensagens, notas). Mantem o pattern visual Melissa.

ESLint: 0 errors da minha mudanca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:31:36 -03:00
Leonardo df61cc4d99 MelissaPaciente Fase 1: foundation (5 composables + skeleton 7 tabs + slug paciente)
Inicio do port do PatientProntuario.vue (3593L Dialog) pra Melissa nativo.
Plano em 8 fases — esta entrega cobre apenas a Fase 1 (foundation).
PatientProntuario continua intocado nos 4 callsites (TherapistDashboard,
MelissaAgenda, MelissaPacientes, PatientsListPage); migration acontece
nas fases 2-8.

5 COMPOSABLES NOVOS em src/features/patients/composables/
- usePatientDetail.js (108L): patients + groups + tags
- usePatientSessions.js (83L): agenda_eventos + computeds proxima/ultima/totais
- usePatientFinancial.js (82L): financial_records + computeds totalRecebido/Aberto/Atrasado
- usePatientMessages.js (64L): conversation_messages + computeds recentes/totalIn/Out
- usePatientDocuments.js (70L): documents + computeds total/Bytes/tiposCount

Cada composable encapsula a query original do PatientProntuario.vue +
adiciona computeds derivados. Reutilizaveis em outros lugares no futuro
(dashboards, relatorios, etc).

MELISSAPACIENTE.VUE NOVO (1190L) em src/layout/melissa/
- Prefixo CSS .mpa-*. Chrome glass + drawer mobile + right: max(...) >=1024px
  (mesmo padrao MelissaAgendador/Negocio).
- Header: avatar + nome + ageLabel + pronomes + Tag status/convenio +
  risco-elevado pill + actions (Conversar / Editar / Close).
- Subheader condicional: banner risco elevado.
- Body 2-col: sidebar 320px (esquerda, drawer no mobile) + main flex 1.
- Sidebar com 4 cards: Acoes Rapidas / Navegacao 7 tabs / Sub-nav Perfil /
  Vinculos (chips grupos+tags).
- Main: 7 tabs (Visao Geral / Perfil / Prontuario / Agenda / Financeiro /
  Documentos / Conversas). Visao Geral ja mostra 4 KPIs reais via composables.
  Outras 6 abas com placeholders "Em desenvolvimento — Fase X".

MELISSALAYOUT.VUE
- Import MelissaPaciente.
- SECOES.paciente entry novo.
- 'paciente' adicionado ao MELISSA_NON_CONFIG_SLUGS.
- Render condicional com :patient-id="String(route.query.id || '')"
  — navegacao via /melissa/paciente?id=xxx.

ESLint: 0 errors da mudanca. 2 errors pre-existentes em MelissaLayout
(duplicate key 'financeiro' L242, empty block L1130) — nao toquei essas
linhas. PatientProntuario tem outros pre-existentes nao tocados.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:23:48 -03:00
Leonardo ee084c2918 Pacientes restore unificado + MelissaLinkExterno nativa
Trabalho de continuidade pós-blueprint:

A) Botao "Restaurar" visivel direto na linha da PatientsListPage
   (layout Rail) quando paciente.status === 'Arquivado' — atalho
   pra usuarios que filtram por arquivados sem precisar abrir o
   menu de "..." (que ja tinha "Reativar" via PatientActionMenu).
   Icone pi-undo + label "Restaurar" + tooltip + click chama
   reactivatePatient do usePatientLifecycle. Aplicado tanto no
   DataTable desktop quanto nos cards mobile.

B) Consolidacao: removido restorePatient do patientsRepository
   (era duplicado com reactivatePatient do usePatientLifecycle).
   MelissaPacientes agora consome reactivatePatient direto, fonte
   unica de verdade pra toda transicao de status pra 'Ativo'.

C) MelissaLinkExterno (nova pagina nativa Melissa). Substitui o
   embed via MelissaEmbed que duplicava 3 headers (layout + embed
   + hero sticky da pagina interna). Lógica preservada (RPC
   issue_patient_invite + rotate_patient_invite_token_v2 +
   copy/openLink), so o chrome muda pra casar com o blueprint
   Melissa: 1 header com status pill (Link ativo/Gerando) +
   botao "Gerar novo link" + Recarregar + Voltar; subheader
   explicativo; body 2-col (esquerda card "Seu link publico" com
   InputGroup + 2 CTAs grandes + card "Mensagem pronta"; direita
   cards "Como funciona" + "Boas praticas"); mobile vira 1-col.

   PatientsExternalLinkPage continua intacta — segue funcionando
   no layout Rail. Wire-up no MelissaLayout: import +
   render block + 'link-externo' literal em NON_CONFIG_SLUGS;
   removido de MELISSA_EMBED_KEYS. Entry removido do EMBED_MAP
   no MelissaEmbed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:38:18 -03:00
Leonardo 269b531158 Melissa: blueprint tabular + Cadastros/Agendamentos/Pacientes + restore
Sprint E (05-05). Blueprint tabular oficial pras paginas Melissa de
listagem (DataTable + sidebar com stats e filtros coloridos, view
toggle list/grade, subheader explicativo, mobile pencil+popover).

Novo arquivo:
- blueprints/melissa-table-page-blueprint.md (~530L, 18 secoes) —
  referencia canonica MelissaCadastrosRecebidos

Paginas refatoradas/criadas:
- MelissaCadastrosRecebidos: refator pra blueprint (DataTable + frozen
  action + view toggle + subheader)
- MelissaAgendamentosRecebidos (NOVO): substitui o embed via
  MelissaEmbed; 4 status coloridos (Pendente/Autorizado/Convertido/
  Recusado), 3 acoes condicionais (Recusar/Autorizar/Converter em
  sessao), wired com AgendaEventDialog
- MelissaPacientes: refator parcial (subheader, sombras, status pills
  coloridas, email/phone colunas proprias, mobile pencil+popover, fix
  scroll mobile com min-height:0 na .mp-list, view toggle persistido,
  tags/grupos color fix g.cor->g.color, restore de arquivados)
- MelissaEmbed: agendamentos-recebidos removido do EMBED_MAP
- MelissaLayout: wire-up MelissaAgendamentosRecebidos nativo
- composables/useMelissaPacientes + useMelissaPacientesAside ajustes

Restore de pacientes arquivados:
- patientsRepository: novo restorePatient(id, { tenantId })
- PatientsCadastroPage statusOpts: +Arquivado (fecha gap de
  inconsistencia ao editar paciente arquivado)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:13:53 -03:00
Leonardo 6d9b36d592 A66 WIP: AgendaEventDialog quebrado em 5 composables + 265 specs + V2 esqueleto
Sub-sessao 1 entregue (composables):
- agendaEventHelpers (262L) — utilitarios puros (date, format, parse)
- useAgendaEventComposer (485L) — montagem do form + validacao
- useAgendaEventActions (387L) — save/delete/cancel/move actions
- useAgendaEventPickerBilling (378L) — pickers (terapeuta, servico,
  convenio) + calculo de billing
- useAgendaEventLifecycle (474L) — open/close/dirty state + autosave
- 5 specs em __tests__/ (75+76+28+43+43 = 265 testes), 495/495 passing

AgendaEventDialog: 3522 -> 2632 linhas (-25%) consumindo os composables.
Backup byte-identico em AgendaEventDialog.vue.bak pra rollback.

Sub-sessao 2 entregue (esqueleto, NAO TESTADO):
- AgendaEventDialogV2 (~1100L, 3 zonas: PACIENTE/QUANDO/O QUE)
- Preview em /preview/agenda-dialog-v2 com 5 cenarios
- Rota em routes.misc.js
- User testou e nao gostou do design — aguarda feedback especifico
  pra iteracao na sub-sessao 3 (migracao nos 9 consumers).

Dialogs auxiliares novos pro AgendaEventDialog:
- InsurancePlanQuickCreateDialog (criar convenio inline)
- ServiceQuickCreateDialog (criar tipo de sessao inline)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:13:22 -03:00
Leonardo 957e912a7f Melissa polish + Prontuario Visao Geral + agenda historico
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>
2026-05-06 09:11:55 -03:00