From af8aee91887a860da95d2fe18ba4c8d549c16726 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 11 May 2026 16:56:49 -0300 Subject: [PATCH] wiki: documenta recorrencia da agenda + log da sessao 2026-05-11 Nova pagina [[recorrencia-agenda]] cobrindo: modelo "1 real + N-1 virtual" via useRecurrence, quem expande virtuais (composables corrigidos em 39cf017), pattern de materializacao ao mudar status (4 caminhos), view listAll de 2 anos no MelissaAgenda, visual de evento inativo, e query SQL pra detectar rows orfas. index.md ganhou link sob Concepts. Log entry da sessao 2026-05-11 10:50 cobrindo os 6 commits previos (8b0e633..39cf017): time picker, services nome unico, paciente arquivado/ inativo, AgendaEventDialog overhaul, view lista Melissa, expansao+ materializacao de recorrencia. Co-Authored-By: Claude Opus 4.7 (1M context) --- Obsidian/Brain/log.md | 58 +++++++++++++++ Obsidian/Brain/wiki/index.md | 2 + Obsidian/Brain/wiki/recorrencia-agenda.md | 91 +++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 Obsidian/Brain/wiki/recorrencia-agenda.md diff --git a/Obsidian/Brain/log.md b/Obsidian/Brain/log.md index 56ce2df..82c2337 100644 --- a/Obsidian/Brain/log.md +++ b/Obsidian/Brain/log.md @@ -543,3 +543,61 @@ placeholders com display:contents; hide rules por breakpoint. Card de preview usa mag-w--side e perde fundo/borda no floating (glass do painel ja faz papel). ESLint 0 errors. Working tree: src/auto-imports.d.ts (auto-gerado) + MelissaAgendador.vue. Nao commitado, nao testado em browser ainda. + +## [2026-05-11 10:50] session | Recorrencia: expandir + materializar + view lista +Touched: [[recorrencia-agenda]] +Detalhes: 6 commits criados e pushed (8b0e633..39cf017). + +TIME PICKER do AgendaEventDialog (commit 988a4e5): +- Header dinamico (header-dot + "Nova {commitment.name}" + subtitulo + "Inicio da sessao e duracao"). Inicio + Termino lado a lado (Termino + readonly via fimDateTime). Card destacado de Termino removido. +- Picker virou DataTable (.aed-patient-dt) + Tags Arquivado/Inativo + sort + Ativo>Inativo>Arquivado. +- Cadastro completo INLINE via PatientCadastroDialog (botao pi-id-card) + em vez de redirecionar pra rota nova — nao vaza do layout Melissa. + Usa prop hideViewListButton adicionada antes pra esconder "Salvar e + ver pacientes". +- Mini calendar (.mc-mini) no time picker; chips de duracao rapida + (30/50/60/90m); cards .aed-card; popovers de ajuda. + +EXPANSAO DE RECORRENCIA cross-layout (commit 39cf017): 3 composables +compartilhados ganharam loadAndExpand — antes so AgendaTerapeutaPage +e AgendaClinicaPage expandiam, deixando widgets do Melissa com 1 sessao +de uma serie de 4. usePatientSessions.load (range -6mo a +12mo, filtra +por patient_id), useMelissaEventos._fetchRange (range visivel), +useMelissaTodasSessoesPaciente.fetch. normalizeEvent aceita shape de +virtual (paciente_nome/patient_name) alem de joined query. + +MATERIALIZACAO em 4 caminhos: UPDATE em id virtual "rec::..." quebrava +com "invalid input syntax for type uuid". Corrigido em +usePatientSessions.updateStatus (aceita row inteira, materializa), +useAgendaEventActions watcher (emit updateSeriesEvent com row), +MelissaLayout.updateEventoStatus (detecta virtual, delega passando +row: ev — sem isso dialogEventRow ficava vazio e criava row orfa sem +patient_id), MelissaPaciente wire-up (@updateSeriesEvent aponta pro +handler certo agora), useMelissaAgenda.onUpdateSeriesEvent (aceita row +do chamador, guard contra rid null, error check no maybeSingle). + +VIEW LISTA MelissaAgenda (commit 279b4f7): listWeek -> custom listAll +(duration { years: 2 }, centrada via gotoDate(hoje - 1 ano)). Banner +showRecurrenceHint aparece em day/week/month com botao "Ver na lista". +Sticky day header (.fc-list-day) com z-index 3 + bg opaco — antes +.fc-event passava por cima conforme scroll. View toggle dos botoes +manuais -> PrimeVue SelectButton. + +VISUAL EVENTO INATIVO: classNames=['ma-evt--inactive-patient'] em +fcEvents quando paciente_status === Arquivado|Inativo (borda tracejada ++ opacidade 0.58 + italico em list view). useAgendaEventPickerBilling ++ AgendaEventDialogV2: picker mostra TODOS os pacientes ordenados +Ativo>Inativo>Arquivado, nao-Ativos com Tag colorida + disabled + +tooltip. selectPaciente bloqueia non-Ativo (defesa em camadas, 3 +specs novas). + +OUTROS: services nome unico por owner (case-insensitive); FC touch +defaults centralizados em src/features/agenda/utils/fcDefaults.js +aplicado em 4 calendars; props hideViewListButton em +ComponentCadastroRapido + PatientCadastroDialog pra uso in-flow. + +Database backup gerado: backups/2026-05-11/ (138 tabelas, 141 FKs). +Dashboard regenerado. diff --git a/Obsidian/Brain/wiki/index.md b/Obsidian/Brain/wiki/index.md index b9f6580..9edb325 100644 --- a/Obsidian/Brain/wiki/index.md +++ b/Obsidian/Brain/wiki/index.md @@ -14,6 +14,8 @@ _(people, places, organizations, products — pages that describe a thing)_ _(ideas, frameworks, patterns, principles — pages that describe a concept)_ +- [[recorrencia-agenda]] — modelo "1 real + N-1 virtual", materialização ao mudar status, view `listAll`, visual de paciente inativo + ## Sources _(summaries of specific sources you've ingested)_ diff --git a/Obsidian/Brain/wiki/recorrencia-agenda.md b/Obsidian/Brain/wiki/recorrencia-agenda.md new file mode 100644 index 0000000..2711d66 --- /dev/null +++ b/Obsidian/Brain/wiki/recorrencia-agenda.md @@ -0,0 +1,91 @@ +# Recorrência na Agenda + +Como o sistema modela e exibe sessões recorrentes. Decisões arquiteturais importantes que não são óbvias só lendo o código. + +## Modelo de dados — "1 real + N-1 virtual" + +Quando o user cria "4 sessões semanais", o sistema escreve **só 2 rows** no banco: + +1. **1 row em `agenda_eventos`** — a primeira ocorrência, materializada. Tem `recurrence_id` apontando pra regra abaixo. +2. **1 row em `recurrence_rules`** — a "semente": `start_date`, `type='weekly'`, `interval=1`, `max_occurrences=4` (ou `open_ended=true`). + +As **sessões 2, 3, 4 NÃO existem no banco**. São geradas em runtime por `useRecurrence.loadAndExpand` — chamado pelas páginas de agenda quando precisam exibir um range. ID virtual: `rec::ruleId::originalDateISO`. + +Trade-off da escolha: +- ✅ Cria recorrência infinita (open-ended) sem inflar o DB +- ✅ Mudança na regra (preço, modalidade, etc) reflete em todas as ocorrências automaticamente +- ❌ Toda exibição precisa chamar `loadAndExpand` no range visível +- ❌ Edição de uma ocorrência específica exige **materializar** primeiro (criar a row real com `recurrence_id` + `recurrence_date`) + +## Quem expande virtuais (e quem não) + +**Expande:** +- `AgendaTerapeutaPage` (Rail) — `loadAndExpand` no range mensal + na busca +- `AgendaClinicaPage` (Clínica) — mesma coisa, com tenant +- `useMelissaAgenda._reloadRange` (Melissa FullCalendar) — expande no range visível +- `usePatientSessions.load` — range -6mo a +12mo, filtra por `patient_id` +- `useMelissaEventos._fetchRange` — expande no range pedido. Cobre widget "Hoje", mini-cal, fallback +- `useMelissaTodasSessoesPaciente.fetch` — range -6mo a +12mo, filtra por `patient_id` + +**Antes de 2026-05-11** os 3 últimos NÃO expandiam — uma série semanal de 4 aparecia como 1. Comentário no código admitia "adicionar quando promover Melissa pra produção". Bug resolvido nesta data. + +## Cap do range — `MAX_RANGE_DAYS = 730` + +`useRecurrence.expandRules` loga warning quando o range visível ultrapassa 730 dias (2 anos). É só warning, não bloqueia. A `listAll` view custom do MelissaAgenda usa exatamente `duration: { years: 2 }` pra bater no cap. + +## Materialização — "ao mudar status numa virtual" + +UPDATE direto numa row com `id = "rec::..."` quebra com `invalid input syntax for type uuid`. Pra mudar status (cancelar, marcar realizada, etc) numa ocorrência virtual, é preciso: + +1. Buscar em `agenda_eventos` se já existe row materializada (`recurrence_id` + `recurrence_date`). +2. Se sim, UPDATE status nela. +3. Se não, **INSERT nova row** copiando campos da virtual + status novo. + +Pattern central: **`useMelissaAgenda.onUpdateSeriesEvent(...)`** (e gêmeas em `AgendaTerapeutaPage` / `AgendaClinicaPage`). Aceita opcional `row` do chamador — quando o user clica direto no evento sem abrir o dialog antes, `dialogEventRow` está vazio e a função precisaria buscar a regra de outro lugar. + +### Caminhos que mudam status (e como chegam à materialização) + +| Onde | Composable/Handler | Comportamento virtual | +|---|---|---| +| `MelissaEventoPanel` (painel lateral do calendário) | `MelissaLayout.updateEventoStatus` | Detecta `is_occurrence` → delega `M.onUpdateSeriesEvent({ row: ev, ... })` | +| `AgendaEventDialog` SelectButton form.status (Cancelado/Remarcar) | `useAgendaEventActions` watcher | `emit('updateSeriesEvent', { row, ... })` em vez de UPDATE direto | +| `AgendaEventDialog` pills da série | `useAgendaEventLifecycle.onPillStatusChange` | Já emitia desde sempre | +| `MelissaPaciente` Tab Agenda botões diretos | `usePatientSessions.updateStatus(ev, status)` | Aceita row inteira; se virtual, materializa internamente | + +**Guard importante** em `onUpdateSeriesEvent`: se `recurrence_id` resolver pra `null` (callerRow + dialogEventRow ambos sem ele), aborta com toast. Antes criava row órfã com `patient_id: null` aparecendo "Faltou sem nome" no calendário. + +## View `listAll` no MelissaAgenda + +View custom (não built-in do FC) com `duration: { years: 2 }`. `setView('lista')` faz `gotoDate(hoje - 1 ano)` pra centrar passado + presente + futuro. Substituiu `listWeek` que mostrava só 7 dias. + +Banner `showRecurrenceHint` aparece nas outras views (dia/semana/mês) quando há virtuais visíveis — botão "Ver na lista" troca pra `listAll`. Sem o banner, user não percebe que tem ocorrências fora do range. + +## Visual de evento inativo + +`normalizeEvent` (`useMelissaEventos.js` + `useMelissaAgenda.normalizeForMelissa`) carrega `paciente_status` do JOIN. MelissaAgenda aplica `classNames: ['ma-evt--inactive-patient']` quando `'Arquivado'|'Inativo'` — CSS dá borda tracejada + opacidade 0.58 + itálico em list view. Mantém a cor do commitment pra não perder contexto. + +Picker do AgendaEventDialog / V2: mostra TODOS os pacientes (Ativo > Inativo > Arquivado), nao-Ativos com Tag + disabled + tooltip. `selectPaciente` bloqueia non-Ativo como defesa em camadas. + +## Quando algo der errado + +Se aparecer "sessão fantasma sem nome" no calendário, provavelmente é row órfã criada por materialização sem `patient_id`/`recurrence_id`. Query pra detectar: + +```sql +SELECT id, inicio_em, status, patient_id, recurrence_id +FROM agenda_eventos +WHERE patient_id IS NULL + AND recurrence_id IS NULL + AND tipo = 'sessao' + AND created_at > NOW() - INTERVAL '1 day'; +``` + +Causa raiz já corrigida em 2026-05-11 (guard contra `rid` null em `onUpdateSeriesEvent`), mas o pattern de query é útil pra catch futuros. + +## Referências de código + +- `src/features/agenda/composables/useRecurrence.js` — `loadAndExpand`, `expandRules`, `mergeWithStoredSessions`, `buildOccurrence` +- `src/layout/melissa/composables/useMelissaAgenda.js:809` — `onUpdateSeriesEvent` +- `src/features/agenda/composables/useAgendaEventActions.js:65` — watcher do form.status +- `src/features/patients/composables/usePatientSessions.js:189` — `updateStatus` com materialização +- `src/layout/melissa/MelissaLayout.vue:655` — `updateEventoStatus` do `MelissaEventoPanel` +- `src/layout/melissa/MelissaAgenda.vue:244` — `VIEW_MAP.lista = 'listAll'`