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>
This commit is contained in:
Leonardo
2026-05-11 10:46:58 -03:00
parent 279b4f78e8
commit 39cf0178e6
6 changed files with 293 additions and 30 deletions
+20 -2
View File
@@ -321,8 +321,11 @@ async function revertRecordPaid(record) {
}
// Handler de mutacao de status (Realizada / Falta / Cancelar)
// Passa a row inteira pro composable porque pode ser ocorrência virtual de
// recorrência (id `rec::ruleId::date`) — nesse caso o composable materializa
// uma linha real antes de aplicar o status (UPDATE em id virtual quebra).
async function updateSessionStatus(ev, novoStatus, msg) {
const result = await sessionsHook.updateStatus(ev.id, novoStatus);
const result = await sessionsHook.updateStatus(ev, novoStatus);
if (result.ok) {
toast.add({ severity: 'success', summary: msg, life: 2200 });
} else {
@@ -539,6 +542,21 @@ async function onSessaoDialogSave(payload) {
]);
}
}
// Mudança de status numa ocorrência (cancelado/remarcado/etc) — delega pro
// handler do composable que SABE materializar ocorrência virtual antes de
// aplicar o status. Sem isso o UPDATE em id virtual quebra ("invalid input
// syntax for type uuid"). Espelha o wire-up de MelissaLayout/AgendaTerapeuta.
async function onSessaoDialogUpdateSeries(payload) {
if (typeof melissaAgenda?.onUpdateSeriesEvent === 'function') {
await melissaAgenda.onUpdateSeriesEvent(payload);
}
if (props.patientId) {
await Promise.all([
sessionsHook.load(props.patientId),
recorrenciasHook.load(props.patientId)
]);
}
}
async function onSessaoDialogDelete(payload) {
if (typeof melissaAgenda?.onDialogDelete === 'function') {
await melissaAgenda.onDialogDelete(payload);
@@ -2296,7 +2314,7 @@ onBeforeUnmount(() => {
:lock-patient="true"
@save="onSessaoDialogSave"
@delete="onSessaoDialogDelete"
@updateSeriesEvent="onSessaoDialogSave"
@updateSeriesEvent="onSessaoDialogUpdateSeries"
@editSeriesOccurrence="onSessaoDialogSave"
>
<template #headerLeft>