diff --git a/susconecta/app/resultados/ResultadosClient.tsx b/susconecta/app/resultados/ResultadosClient.tsx index eb6b398..72f4064 100644 --- a/susconecta/app/resultados/ResultadosClient.tsx +++ b/susconecta/app/resultados/ResultadosClient.tsx @@ -1,6 +1,8 @@ "use client" import React, { useEffect, useMemo, useState } from 'react' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' import { useSearchParams, useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' @@ -28,6 +30,7 @@ import { buscarMedicos, getAvailableSlots, criarAgendamento, + criarAgendamentoDireto, getUserInfo, buscarPacientes, listarDisponibilidades, @@ -81,6 +84,16 @@ export default function ResultadosClient() { const [medicoSelecionado, setMedicoSelecionado] = useState(null) const [abaDetalhe, setAbaDetalhe] = useState('experiencia') + // Confirmation dialog for booking: hold pending selection until user confirms + const [confirmOpen, setConfirmOpen] = useState(false) + const [pendingAppointment, setPendingAppointment] = useState<{ doctorId: string; iso: string } | null>(null) + const [confirmLoading, setConfirmLoading] = useState(false) + // Fields editable in the confirmation dialog to be sent to the create endpoint + const [confirmDuration, setConfirmDuration] = useState(30) + const [confirmInsurance, setConfirmInsurance] = useState('') + const [confirmChiefComplaint, setConfirmChiefComplaint] = useState('') + const [confirmPatientNotes, setConfirmPatientNotes] = useState('') + // Toast simples const [toast, setToast] = useState<{ type: 'success' | 'error', msg: string } | null>(null) const showToast = (type: 'success' | 'error', msg: string) => { @@ -193,7 +206,7 @@ export default function ResultadosClient() { } } - // 4) Agendar ao clicar em um horário + // 4) Agendar ao clicar em um horário (performs the actual create call) async function agendar(doctorId: string, iso: string) { if (!patientId) { showToast('error', 'Paciente não identificado. Faça login novamente.') @@ -220,6 +233,55 @@ export default function ResultadosClient() { } } + // Open confirmation dialog for a selected slot instead of immediately booking + function openConfirmDialog(doctorId: string, iso: string) { + setPendingAppointment({ doctorId, iso }) + setConfirmOpen(true) + } + + // Called when the user confirms the booking in the dialog + async function confirmAndBook() { + if (!pendingAppointment) return + const { doctorId, iso } = pendingAppointment + if (!patientId) { + showToast('error', 'Paciente não identificado. Faça login novamente.') + return + } + // Debug: indicate the handler was invoked + console.debug('[ResultadosClient] confirmAndBook invoked', { doctorId, iso, patientId, confirmDuration, confirmInsurance }) + showToast('success', 'Iniciando agendamento...') + setConfirmLoading(true) + try { + // Use direct POST to ensure creation even if availability checks would block + await criarAgendamentoDireto({ + patient_id: String(patientId), + doctor_id: String(doctorId), + scheduled_at: String(iso), + duration_minutes: Number(confirmDuration) || 30, + appointment_type: (tipoConsulta === 'local' ? 'presencial' : 'telemedicina'), + chief_complaint: confirmChiefComplaint || null, + patient_notes: confirmPatientNotes || null, + insurance_provider: confirmInsurance || null, + }) + showToast('success', 'Consulta agendada com sucesso!') + // remover horário da lista local + setAgendaByDoctor((prev) => { + const days = prev[doctorId] + if (!days) return prev + const updated = days.map(d => ({ ...d, horarios: d.horarios.filter(h => h.iso !== iso) })) + return { ...prev, [doctorId]: updated } + }) + setConfirmOpen(false) + setPendingAppointment(null) + // Navigate to agenda after a short delay so user sees the toast + setTimeout(() => router.push('/agenda'), 500) + } catch (e: any) { + showToast('error', e?.message || 'Falha ao agendar') + } finally { + setConfirmLoading(false) + } + } + // Fetch slots for an arbitrary date using the same logic as CalendarRegistrationForm async function fetchSlotsForDate(doctorId: string, dateOnly: string) { if (!doctorId || !dateOnly) return [] @@ -439,6 +501,39 @@ export default function ResultadosClient() { )} + {/* Confirmation dialog shown when a user selects a slot */} + { if (!open) { setConfirmOpen(false); setPendingAppointment(null); } }}> + + + Confirmar agendamento + +
+ {pendingAppointment ? ( + (() => { + const doc = medicos.find(m => String(m.id) === String(pendingAppointment.doctorId)) + const doctorName = doc ? (doc.full_name || (doc as any).name || 'Profissional') : 'Profissional' + const when = (() => { + try { return new Date(pendingAppointment.iso).toLocaleString('pt-BR', { dateStyle: 'long', timeStyle: 'short' }) } catch { return pendingAppointment.iso } + })() + return ( +
+

Profissional: {doctorName}

+

Data / Hora: {when}

+

Paciente: Você

+
+ ) + })() + ) : ( +

Carregando informações...

+ )} +
+
+ + +
+
+
+ {/* Hero de filtros (mantido) */}
@@ -644,7 +739,7 @@ export default function ResultadosClient() { {nearestSlotByDoctor[id] && (
Próximo horário: -
@@ -706,7 +801,7 @@ export default function ResultadosClient() { key={h.iso} type="button" className="rounded-lg bg-primary/10 px-2 py-1 text-xs font-medium text-primary transition hover:bg-primary hover:text-primary-foreground" - onClick={() => agendar(id, h.iso)} + onClick={() => openConfirmDialog(id, h.iso)} > {h.label} @@ -827,7 +922,7 @@ export default function ResultadosClient() { key={h.iso} type="button" className="rounded-lg bg-primary/10 px-2 py-1 text-xs font-medium text-primary transition hover:bg-primary hover:text-primary-foreground" - onClick={() => agendar(String(medicoSelecionado.id), h.iso)} + onClick={() => openConfirmDialog(String(medicoSelecionado.id), h.iso)} > {h.label} @@ -873,7 +968,7 @@ export default function ResultadosClient() { ) : (moreTimesSlots.length ? (
{moreTimesSlots.map(s => ( - ))} diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index d779169..cbe6754 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1196,6 +1196,60 @@ export async function criarAgendamento(input: AppointmentCreate): Promise { + if (!input || !input.patient_id || !input.doctor_id || !input.scheduled_at) { + throw new Error('Parâmetros inválidos para criar agendamento. patient_id, doctor_id e scheduled_at são obrigatórios.'); + } + + // Determine created_by: prefer explicit, then localStorage, then user-info + let createdBy: string | null = input.created_by ?? null; + if (!createdBy && typeof window !== 'undefined') { + try { + const raw = localStorage.getItem(AUTH_STORAGE_KEYS.USER); + if (raw) { + const parsed = JSON.parse(raw); + createdBy = parsed?.id ?? parsed?.user?.id ?? null; + } + } catch (e) { + // ignore + } + } + if (!createdBy) { + try { + const info = await getUserInfo().catch(() => null); + createdBy = info?.user?.id ?? null; + } catch (e) { + // ignore + } + } + + const payload: any = { + patient_id: input.patient_id, + doctor_id: input.doctor_id, + scheduled_at: new Date(input.scheduled_at).toISOString(), + duration_minutes: input.duration_minutes ?? 30, + appointment_type: input.appointment_type ?? 'presencial', + chief_complaint: input.chief_complaint ?? null, + patient_notes: input.patient_notes ?? null, + insurance_provider: input.insurance_provider ?? null, + }; + if (createdBy) payload.created_by = createdBy; + + const url = `${REST}/appointments`; + const res = await fetch(url, { + method: 'POST', + headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'), + body: JSON.stringify(payload), + }); + + const created = await parse(res); + return created; +} + // Payload for updating an appointment (PATCH /rest/v1/appointments/{id}) export type AppointmentUpdate = Partial<{ scheduled_at: string;