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
+31 -5
View File
@@ -652,16 +652,42 @@ function fecharEvento() {
// ── Actions do MelissaEventoPanel ──────────────────────────────
// updateStatus: muda status no DB e refetcha agenda. Pattern espelha
// AgendaTerapeutaPage (sem optimistic update por simplicidade no MVP).
//
// Quando `ev` é ocorrência VIRTUAL de recorrência (id `rec::...` sem row real),
// delega pro M.onUpdateSeriesEvent que materializa antes do UPDATE — sem isso
// PostgreSQL recusa o UPDATE com "invalid input syntax for type uuid".
async function updateEventoStatus(novoStatus, msgSucesso) {
const ev = eventoSelecionado.value;
if (!ev?.id || eventoBusy.value) return;
eventoBusy.value = true;
try {
const { error } = await supabase
.from('agenda_eventos')
.update({ status: novoStatus })
.eq('id', ev.id);
if (error) throw error;
const isVirtual =
!!ev.is_occurrence ||
(typeof ev.id === 'string' && ev.id.startsWith('rec::'));
if (isVirtual) {
await M.onUpdateSeriesEvent({
id: null,
status: novoStatus,
recurrence_date:
ev.recurrence_date ||
ev.original_date ||
String(ev.inicio_em || '').slice(0, 10),
inicio_em: ev.inicio_em,
fim_em: ev.fim_em,
is_virtual: true,
// Passa o ev completo — sem isso o handler depende de
// dialogEventRow.value (que está vazio quando o user clica
// direto no evento do FC sem abrir o dialog antes).
row: ev
});
} else {
const { error } = await supabase
.from('agenda_eventos')
.update({ status: novoStatus })
.eq('id', ev.id);
if (error) throw error;
}
toast.add({ severity: 'success', summary: msgSucesso, life: 2200 });
// Refetch:
// - M.refetch() alimenta a Agenda (FullCalendar + ocorrências virtuais)