diff --git a/src/features/agenda/components/AgendaEventDialog.vue b/src/features/agenda/components/AgendaEventDialog.vue index d937f6b..324da18 100644 --- a/src/features/agenda/components/AgendaEventDialog.vue +++ b/src/features/agenda/components/AgendaEventDialog.vue @@ -28,6 +28,7 @@ import { useConfirm } from 'primevue/useconfirm'; import { useToast } from 'primevue/usetoast'; import { supabase } from '@/lib/supabase/client'; import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue'; +import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue'; import AgendaEventoFinanceiroPanel from '@/components/agenda/AgendaEventoFinanceiroPanel.vue'; import ServiceQuickCreateDialog from './ServiceQuickCreateDialog.vue'; import InsurancePlanQuickCreateDialog from './InsurancePlanQuickCreateDialog.vue'; @@ -522,24 +523,49 @@ function onPatientCreatedRapido(p) { cadRapidoOpen.value = false; } +// "Cadastro completo" inline — substitui o open-em-nova-aba pra +// (a) nao vazar do layout Melissa pra /therapist/patients/cadastro +// (b) preservar o estado do form de evento sem precisar de nova aba. +// Reutiliza o PatientCadastroDialog (mesmo usado em MelissaPacientes). +const cadCompletoOpen = ref(false); +// Popovers de ajuda dos InputGroups do card Pagamento. Refs separados +// pra cada (servico/convenio) pra evitar conflito quando o user abre +// um e clica direto no outro sem fechar. +const servicoHelpRef = ref(null); +const convenioHelpRef = ref(null); +function onPatientCreatedCompleto(p) { + if (!p) return; + const nome = p.nome_completo || p.nome || p.name || ''; + form.value.paciente_id = p.id; + form.value.paciente_nome = nome; + form.value.paciente_avatar = p.avatar_url || null; + cadCompletoOpen.value = false; +} + // ── paciente picker ──────────────────────────────────────── // pacientePickerOpen, pacienteSearch, pacientesLoading, pacientesError, patients, // selectCommitment, goBack, openPacientePicker, clearPatientsCache, loadPatients, // selectPaciente, clearPaciente → useAgendaEventPickerBilling (1C-ii-a) // filteredPatients depende de patients/pacienteSearch (do composable) — fica no .vue: +// Mostra TODOS os pacientes (inclusive Inativo/Arquivado) — UX intencional pra +// o user achar quem procura. Os nao-Ativos vem com badge + disabled (template), +// e selectPaciente bloqueia o clique se status !== 'Ativo'. Ordenacao: +// Ativos primeiro, depois Inativos, depois Arquivados. +const _statusRank = { Ativo: 0, Inativo: 1, Arquivado: 2 }; const filteredPatients = computed(() => { const q = String(pacienteSearch.value || '') .trim() .toLowerCase(); - // Somente pacientes Ativos podem ser selecionados para novos agendamentos - const list = (patients.value || []).filter((p) => p.status === 'Ativo'); - if (!q) return list; - return list.filter((p) => { - const nome = String(p.nome || '').toLowerCase(); - const email = String(p.email || '').toLowerCase(); - const tel = String(p.telefone || '').toLowerCase(); - return nome.includes(q) || email.includes(q) || tel.includes(q); - }); + const list = patients.value || []; + const matched = !q + ? [...list] + : list.filter((p) => { + const nome = String(p.nome || '').toLowerCase(); + const email = String(p.email || '').toLowerCase(); + const tel = String(p.telefone || '').toLowerCase(); + return nome.includes(q) || email.includes(q) || tel.includes(q); + }); + return matched.sort((a, b) => (_statusRank[a.status] ?? 3) - (_statusRank[b.status] ?? 3)); }); // 4 watchers (modelValue init, tenant/scope, solicitação pendente, online @@ -561,6 +587,90 @@ function goToConveniosConfig() { } // ── duração / slots ──────────────────────────────────────── +// Chips de duração rápida no Time Picker (mesmo set do AgendaEventDialogV2 +// pra paridade visual entre os dois dialogs). +const duracaoQuickChips = [30, 50, 60, 90]; + +// ── Mini calendar do Time Picker (estilo MelissaAgenda mini-cal) ───── +// Versao enxuta: sem dots de eventos, sem feriados, sem dias fechados. +// So renderiza grid 6x7 com hoje (cor primary) + selecionado (fundo +// primary) + outros meses (cinza). Sincroniza com form.dia ao abrir. +const miniRefDate = ref(new Date()); + +watch( + () => timePickerOpen.value, + (open) => { + if (!open) return; + const d = form.value.dia ? new Date(form.value.dia) : new Date(); + if (!Number.isNaN(d.getTime())) miniRefDate.value = d; + } +); + +const miniMesAno = computed(() => + miniRefDate.value + .toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }) + .replace(/(^|\s)\S/g, (l) => l.toUpperCase()) +); + +const miniDias = computed(() => { + const refD = miniRefDate.value; + const ano = refD.getFullYear(); + const mes = refD.getMonth(); + const ultimoDia = new Date(ano, mes + 1, 0).getDate(); + const inicio = new Date(ano, mes, 1).getDay(); // 0=dom + + const hoje = new Date(); + hoje.setHours(0, 0, 0, 0); + const sel = form.value.dia ? new Date(form.value.dia) : null; + if (sel) sel.setHours(0, 0, 0, 0); + + const sameDay = (a, b) => a && b && a.getTime() === b.getTime(); + const arr = []; + + // Padding antes (mês anterior, em cinza) + for (let i = inicio - 1; i >= 0; i--) { + const d = new Date(ano, mes, -i); + d.setHours(0, 0, 0, 0); + arr.push({ dia: d.getDate(), outro: true, isHoje: false, isSel: sameDay(d, sel), date: d }); + } + // Dias do mês corrente + for (let d = 1; d <= ultimoDia; d++) { + const data = new Date(ano, mes, d); + data.setHours(0, 0, 0, 0); + arr.push({ dia: d, outro: false, isHoje: sameDay(data, hoje), isSel: sameDay(data, sel), date: data }); + } + // Padding depois até 42 (6 semanas completas) + while (arr.length < 42) { + const last = arr[arr.length - 1].date; + const next = new Date(last); + next.setDate(last.getDate() + 1); + next.setHours(0, 0, 0, 0); + arr.push({ dia: next.getDate(), outro: true, isHoje: false, isSel: sameDay(next, sel), date: next }); + } + return arr; +}); + +function miniPrev() { + const d = new Date(miniRefDate.value); + d.setMonth(d.getMonth() - 1); + miniRefDate.value = d; +} +function miniNext() { + const d = new Date(miniRefDate.value); + d.setMonth(d.getMonth() + 1); + miniRefDate.value = d; +} +function miniToday() { + const t = new Date(); + miniRefDate.value = t; + form.value.dia = new Date(t.getFullYear(), t.getMonth(), t.getDate()); +} +function selecionarDiaMini(d) { + if (!d?.date) return; + form.value.dia = new Date(d.date); + miniRefDate.value = new Date(d.date); +} + // grouped duration options: default preset first, then com pausa, then sem pausa const duracaoOptions = computed(() => { const defaultDur = props.agendaSettings?.session_duration_min || 50; @@ -754,10 +864,34 @@ const googleCalendarUrl = computed(() => { // labelStatusSessao / statusSeverity / statusExtraClass movidos pra // agendaEventHelpers.js (A66/1A) + +// ── Dialog width: compacto no step 1, full no step 2 ─────────── +// Step 1 (escolha de tipo) e uma lista de rows finas — 400px da +// foco. Step 2 (formulario) precisa de espaco pro grid 2-col. +// Quando lockType=true, abre direto no step 2 (sem step 1). +const dialogStyle = computed(() => { + const isStep1 = step.value === 1 && !props.lockType; + return { + width: isStep1 ? '420px' : '600px', + maxWidth: '96vw' + }; +}); + +// Some o card flutuante (Resumo) quando QUALQUER sub-dialog/picker +// abre. Sem isso, o card (z-index alto) ficaria por cima do +// pacientePicker/timePicker/cadRapido/quickCreate, atrapalhando. +const anyChildDialogOpen = computed(() => + pacientePickerOpen.value || + timePickerOpen.value || + cadRapidoOpen.value || + cadCompletoOpen.value || + serviceQuickDlgOpen.value || + insuranceQuickDlgOpen.value +);