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
@@ -87,10 +87,46 @@ export function useAgendaEventActions({
acceptSeverity: isCancelar ? 'danger' : 'warn',
accept: async () => {
try {
// Se o evento é ocorrência VIRTUAL de recorrência
// (id "rec::..." sem row real em agenda_eventos),
// delega pro parent — useMelissaAgenda.onUpdateSeriesEvent
// e AgendaTerapeutaPage.onUpdateSeriesEvent materializam
// a linha antes de aplicar status. Sem essa delegação,
// UPDATE direto em id virtual quebra com PostgreSQL
// "invalid input syntax for type uuid".
const formId = composer.form.value.id;
const isVirtual =
!!composer.form.value.is_occurrence ||
(typeof formId === 'string' && formId.startsWith('rec::'));
if (isVirtual) {
emit('updateSeriesEvent', {
id: null, // sem row real
status: newVal,
recurrence_date:
composer.form.value.recurrence_date ||
composer.form.value.original_date ||
String(composer.form.value.inicio_em || '').slice(0, 10),
inicio_em: composer.form.value.inicio_em,
fim_em: composer.form.value.fim_em,
is_virtual: true,
// Form completo do dialog — handler usa pra resolver
// recurrence_id/patient_id sem depender de dialogEventRow.
row: { ...composer.form.value }
});
toast.add({
severity: 'success',
summary: 'Status atualizado',
detail: `Sessão marcada como ${labelStatusSessao(newVal)}.`,
life: 3000
});
return;
}
const { data, error } = await supabase
.from('agenda_eventos')
.update({ status: newVal })
.eq('id', composer.form.value.id)
.eq('id', formId)
.select()
.single();
if (error) throw error;