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;