From f1d6fbad736234920993651b0ec5e2437c681675 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 8 May 2026 11:32:36 -0300 Subject: [PATCH] MelissaPaciente: dialog nova sessao integra useRecurrence (recorrencia semanal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User apontou que ja existe sistema de recorrencia pronto (useRecurrence.js + tabela recurrence_rules + MelissaRecorrencias). Integrei no dialog de nova sessao. NOVO no dialog: - Checkbox "Repetir semanalmente" + texto explicativo (cria serie no mesmo dia da semana e horario) - Quando ativado, mostra 3 opcoes radio: - "Sem data de fim" (open-ended — continua ate cancelar) - "Apos N sessoes" (max_occurrences) - "Ate " (end_date) - Cada opcao com input inline disabled quando nao selecionada - Label do botao salvar muda dinamicamente: "Agendar sessao" -> "Criar recorrencia" LOGICA salvarSessao() ramificada: - Se repetir = false: caminho original (createSession + INSERT em agenda_eventos) - Se repetir = true: caminho NOVO via useRecurrence.createRule: - type: 'weekly', interval: 1 - weekdays: [inicio.getDay()] (calculado do dia da semana selecionado) - start_date: f.data - end_date / max_occurrences conforme fim_tipo - start_time: f.hora - duration_min, modalidade, titulo_custom, observacoes, status: 'ativo' - Insere row em recurrence_rules; ocorrencias sao geradas dinamicamente pelo expandRules() do composable. Sessoes confirmadas/realizadas viram rows reais sob demanda. Validacoes adicionais: - fim_tipo='data' exige fim_data preenchido (toast warn) - fim_tipo='count' exige fim_count >= 1 (toast warn) Reload das sessoes ao final pra refletir caso start_date seja hoje (occurrence ja entra na timeline). Toast de sucesso aponta pra "Recorrencias" como destino pra gerenciar a serie. ESLint: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/layout/melissa/MelissaPaciente.vue | 184 ++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 7 deletions(-) diff --git a/src/layout/melissa/MelissaPaciente.vue b/src/layout/melissa/MelissaPaciente.vue index ef95879..c9aac84 100644 --- a/src/layout/melissa/MelissaPaciente.vue +++ b/src/layout/melissa/MelissaPaciente.vue @@ -22,6 +22,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue' import { useRouter } from 'vue-router'; import { useToast } from 'primevue/usetoast'; import { useConversationDrawerStore } from '@/stores/conversationDrawerStore'; +import { supabase } from '@/lib/supabase/client'; import DocumentsListPage from '@/features/documents/DocumentsListPage.vue'; import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue'; import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue'; @@ -30,6 +31,7 @@ import { usePatientSessions } from '@/features/patients/composables/usePatientSe import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial'; import { usePatientMessages } from '@/features/patients/composables/usePatientMessages'; import { usePatientDocuments } from '@/features/patients/composables/usePatientDocuments'; +import { useRecurrence } from '@/features/agenda/composables/useRecurrence'; import { pickField, calcAge, @@ -70,6 +72,7 @@ const sessionsHook = usePatientSessions(); const financialHook = usePatientFinancial(); const messagesHook = usePatientMessages(); const documentsHook = usePatientDocuments(); +const recurrenceHook = useRecurrence(); // ── Breakpoints + drawer ─────────────────────────────────── const drawerOpen = ref(false); @@ -425,7 +428,12 @@ const novaSessaoForm = ref({ duracao_min: 50, modalidade: 'presencial', titulo_custom: '', - observacoes: '' + observacoes: '', + // Recorrencia (integra com useRecurrence — schema recurrence_rules) + repetir: false, + fim_tipo: 'open', // open | data | count + fim_data: '', + fim_count: 12 }); const SESSAO_TIPOS = [ { label: 'Sessão', value: 'sessao' }, @@ -465,7 +473,11 @@ function goAgendar() { duracao_min: 50, modalidade: 'presencial', titulo_custom: '', - observacoes: '' + observacoes: '', + repetir: false, + fim_tipo: 'open', + fim_data: '', + fim_count: 12 }; novaSessaoOpen.value = true; } @@ -479,9 +491,6 @@ async function salvarSessao() { toast.add({ severity: 'warn', summary: 'Hora obrigatória', life: 2500 }); return; } - // Monta inicio_em + fim_em a partir de data + hora + duracao_min. - // Local time -> ISO. Sem timezone explicit, usa o do browser (consistente - // com o pattern do AgendaEventDialog e MelissaAgenda). const [y, m, d] = f.data.split('-').map(Number); const [hh, mm] = f.hora.split(':').map(Number); const inicio = new Date(y, (m || 1) - 1, d || 1, hh || 0, mm || 0, 0); @@ -489,8 +498,61 @@ async function salvarSessao() { toast.add({ severity: 'warn', summary: 'Data/hora inválida', life: 2500 }); return; } - const fim = new Date(inicio.getTime() + (Number(f.duracao_min) || 50) * 60 * 1000); + // Caminho RECORRENTE: cria regra em recurrence_rules via useRecurrence. + // Ocorrencias sao geradas dinamicamente — nao precisa popular agenda_eventos + // futuros (sessoes confirmadas/realizadas viram rows reais sob demanda). + if (f.repetir) { + if (f.fim_tipo === 'data' && !f.fim_data) { + toast.add({ severity: 'warn', summary: 'Informe a data de fim', life: 2500 }); + return; + } + if (f.fim_tipo === 'count' && (!f.fim_count || Number(f.fim_count) < 1)) { + toast.add({ severity: 'warn', summary: 'Informe o número de ocorrências', life: 2500 }); + return; + } + try { + const { data: userData } = await supabase.auth.getUser(); + const ownerId = userData?.user?.id; + const rule = { + patient_id: props.patientId, + owner_id: ownerId, + type: 'weekly', + interval: 1, + weekdays: [inicio.getDay()], + start_date: f.data, + end_date: f.fim_tipo === 'data' ? f.fim_data : null, + max_occurrences: f.fim_tipo === 'count' ? Number(f.fim_count) : null, + start_time: f.hora, + duration_min: Number(f.duracao_min) || 50, + modalidade: f.modalidade, + titulo_custom: String(f.titulo_custom || '').trim() || null, + observacoes: String(f.observacoes || '').trim() || null, + status: 'ativo' + }; + await recurrenceHook.createRule(rule); + toast.add({ + severity: 'success', + summary: 'Recorrência criada', + detail: 'A série semanal está ativa. Veja em "Recorrências".', + life: 3000 + }); + novaSessaoOpen.value = false; + // Recarrega sessoes do paciente (caso start_date seja hoje). + await sessionsHook.load(props.patientId); + } catch (e) { + toast.add({ + severity: 'error', + summary: 'Falha ao criar recorrência', + detail: e?.message || 'Erro inesperado', + life: 4000 + }); + } + return; + } + + // Caminho SESSAO UNICA: insert direto em agenda_eventos. + const fim = new Date(inicio.getTime() + (Number(f.duracao_min) || 50) * 60 * 1000); const result = await sessionsHook.createSession(props.patientId, { inicio_em: inicio.toISOString(), fim_em: fim.toISOString(), @@ -2197,6 +2259,49 @@ onBeforeUnmount(() => { class="w-full" /> + + +
+ +
+ + + +
+
@@ -3242,6 +3347,71 @@ onBeforeUnmount(() => { opacity: 0.75; } +/* Bloco de recorrencia (toggle + opcoes radio) dentro do dialog Nova Sessao */ +.mpa-recur { + margin-top: 4px; + padding: 12px; + border-radius: 10px; + background: var(--m-bg-medium); + border: 1px solid var(--m-border); + display: flex; + flex-direction: column; + gap: 12px; +} +.mpa-recur__toggle { + display: flex; + align-items: flex-start; + gap: 10px; + cursor: pointer; +} +.mpa-recur__toggle-text { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + min-width: 0; +} +.mpa-recur__toggle-text strong { + font-size: 0.85rem; + color: var(--m-text); + font-weight: 700; +} +.mpa-recur__toggle-text span { + font-size: 0.74rem; + color: var(--m-text-muted); + line-height: 1.4; +} +.mpa-recur__opts { + display: flex; + flex-direction: column; + gap: 8px; + padding-left: 4px; + padding-top: 4px; + border-top: 1px dashed var(--m-border); + padding-top: 10px; +} +.mpa-recur__opt { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 0.82rem; + color: var(--m-text); +} +.mpa-recur__opt > span { + display: inline-flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} +.mpa-recur__inline { + width: 70px !important; + text-align: center; +} +.mpa-recur__inline--date { + width: 150px !important; +} + /* ═══════ Tab Conversas (Fase 7) — CTA pra drawer ═══════ */ .mpa-conv-cta { display: flex;