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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)_
|
||||
|
||||
@@ -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'`
|
||||
Reference in New Issue
Block a user