From a8ab13b201b2c9a0e4db93f68573e9b49588f56d Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 8 May 2026 11:20:00 -0300 Subject: [PATCH] MelissaPaciente: dialog inline nova sessao + createSession mutation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Espelha o padrao do "Lancamento" mas pra agenda — botao "Agendar" agora navega pra aba Agenda e abre dialog de nova sessao. NOVO em src/features/patients/composables/usePatientSessions.js - createSession(patientId, payload) — INSERT agenda_eventos com status='agendado', resolve owner_id (auth.getUser) e tenant_id (lazy import tenantStore). Auto-reload via _lastPatientId. Validacao: inicio_em + fim_em obrigatorios. Retorna {ok, data?, error?}. NOVO em MelissaPaciente.vue - Refs novaSessaoOpen + novaSessaoForm (tipo/data/hora/duracao_min/ modalidade/titulo_custom/observacoes) - 3 catalogos: - SESSAO_TIPOS: Sessao/Primeira/Retorno/Avaliacao/Devolutiva - SESSAO_DURACOES: 30/40/45/50/55/60/90/120 min - SESSAO_MODALIDADES: Presencial/Online - goAgendar() agora alem de navegar pra aba Agenda, tambem inicializa o form (default amanha 09:00, sessao 50min presencial) e abre o dialog. - salvarSessao() handler com validacao (data + hora) e construcao de inicio_em/fim_em a partir de data + hora + duracao_min. Local time -> ISO via Date constructor. - 460px com form: Tipo + grid 2-col (data + hora) + grid 2-col (duracao + modalidade) + titulo opcional + observacoes Textarea. - CSS .mpa-novo-lanc__opt pra "(opcional)" em cinza. Validacoes: - Data e hora obrigatorios (warn toast) - Date constructor invalido -> warn toast Pra criar sessoes mais complexas (recorrencia, multi-paciente, conflitos de agenda), o user vai pra MelissaAgenda direto que tem o AgendaEventDialog completo. Aqui no prontuario eh o caminho rapido. ESLint: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../composables/usePatientSessions.js | 59 ++++++ src/layout/melissa/MelissaPaciente.vue | 194 +++++++++++++++++- 2 files changed, 250 insertions(+), 3 deletions(-) diff --git a/src/features/patients/composables/usePatientSessions.js b/src/features/patients/composables/usePatientSessions.js index 49e5846..e6271fa 100644 --- a/src/features/patients/composables/usePatientSessions.js +++ b/src/features/patients/composables/usePatientSessions.js @@ -84,6 +84,64 @@ export function usePatientSessions() { .slice(0, 6) ); + /** + * Cria uma nova sessao na agenda do paciente. + * + * payload: { + * inicio_em: ISO timestamp, + * fim_em: ISO timestamp, + * tipo: 'sessao' | 'primeira' | 'retorno' | etc, + * modalidade: 'presencial' | 'online', + * titulo?: string, + * titulo_custom?: string, + * observacoes?: string + * } + * Retorna {ok, data?, error?}. + */ + async function createSession(patientId, payload = {}) { + if (!patientId || busy.value) return { ok: false, error: 'busy' }; + if (!payload?.inicio_em || !payload?.fim_em) { + return { ok: false, error: 'Inicio/fim obrigatorios' }; + } + busy.value = true; + try { + const { data: userData } = await supabase.auth.getUser(); + const ownerId = userData?.user?.id; + let tenantId = null; + try { + const { useTenantStore } = await import('@/stores/tenantStore'); + tenantId = useTenantStore().activeTenantId || null; + } catch { /* sem tenant store — segue */ } + + const row = { + patient_id: patientId, + owner_id: ownerId, + tenant_id: tenantId, + inicio_em: payload.inicio_em, + fim_em: payload.fim_em, + status: 'agendado', + modalidade: payload.modalidade || 'presencial', + tipo: payload.tipo || 'sessao', + titulo: String(payload.titulo || '').trim() || null, + titulo_custom: String(payload.titulo_custom || '').trim() || null, + observacoes: String(payload.observacoes || '').trim() || null + }; + + const { data, error: err } = await supabase + .from('agenda_eventos') + .insert([row]) + .select() + .single(); + if (err) throw err; + if (_lastPatientId) await load(_lastPatientId); + return { ok: true, data }; + } catch (e) { + return { ok: false, error: e?.message || 'Erro ao agendar sessao' }; + } finally { + busy.value = false; + } + } + /** * Atualiza o status de uma sessao (mutation). Recarrega a lista do paciente * ao final pra refletir o novo estado nos computeds derivados. @@ -114,6 +172,7 @@ export function usePatientSessions() { busy, load, updateStatus, + createSession, proximaSessao, ultimaSessao, totalSessoes, diff --git a/src/layout/melissa/MelissaPaciente.vue b/src/layout/melissa/MelissaPaciente.vue index b8bfb79..ef95879 100644 --- a/src/layout/melissa/MelissaPaciente.vue +++ b/src/layout/melissa/MelissaPaciente.vue @@ -416,12 +416,100 @@ function addFinancial() { novoLancOpen.value = true; } -// Atalho: navega pra aba Agenda (mesma logica do "Lancamento" so que sem -// dialog — a aba Agenda ja mostra todas as sessoes com acoes de marcar -// realizada/falta/cancelar inline). +// Atalho: navega pra aba Agenda + abre dialog de nova sessao. +const novaSessaoOpen = ref(false); +const novaSessaoForm = ref({ + tipo: 'sessao', + data: '', + hora: '', + duracao_min: 50, + modalidade: 'presencial', + titulo_custom: '', + observacoes: '' +}); +const SESSAO_TIPOS = [ + { label: 'Sessão', value: 'sessao' }, + { label: 'Primeira consulta', value: 'primeira' }, + { label: 'Retorno', value: 'retorno' }, + { label: 'Avaliação', value: 'avaliacao' }, + { label: 'Devolutiva', value: 'devolutiva' } +]; +const SESSAO_DURACOES = [ + { label: '30 min', value: 30 }, + { label: '40 min', value: 40 }, + { label: '45 min', value: 45 }, + { label: '50 min', value: 50 }, + { label: '55 min', value: 55 }, + { label: '1 hora', value: 60 }, + { label: '1h 30', value: 90 }, + { label: '2 horas', value: 120 } +]; +const SESSAO_MODALIDADES = [ + { label: 'Presencial', value: 'presencial' }, + { label: 'Online (vídeo)', value: 'online' } +]; function goAgendar() { activeTab.value = 'agenda'; if (isMobile.value) drawerOpen.value = false; + if (!props.patientId) { + toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 }); + return; + } + // Default: amanha as 09:00, sessao 50min presencial. + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + novaSessaoForm.value = { + tipo: 'sessao', + data: tomorrow.toISOString().slice(0, 10), + hora: '09:00', + duracao_min: 50, + modalidade: 'presencial', + titulo_custom: '', + observacoes: '' + }; + novaSessaoOpen.value = true; +} +async function salvarSessao() { + const f = novaSessaoForm.value; + if (!f.data) { + toast.add({ severity: 'warn', summary: 'Data obrigatória', life: 2500 }); + return; + } + if (!f.hora) { + 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); + if (Number.isNaN(inicio.getTime())) { + 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); + + const result = await sessionsHook.createSession(props.patientId, { + inicio_em: inicio.toISOString(), + fim_em: fim.toISOString(), + tipo: f.tipo, + modalidade: f.modalidade, + titulo_custom: f.titulo_custom, + observacoes: f.observacoes + }); + if (result.ok) { + toast.add({ severity: 'success', summary: 'Sessão agendada', life: 2200 }); + novaSessaoOpen.value = false; + } else { + toast.add({ + severity: 'error', + summary: 'Falha ao agendar', + detail: result.error || 'Erro inesperado', + life: 4000 + }); + } } async function salvarLancamento() { const f = novoLancForm.value; @@ -2030,6 +2118,101 @@ onBeforeUnmount(() => { + + + +
+
+ + +
+
+ +