backup/agendamento #60
@ -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<Medico | null>(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<number>(30)
|
||||
const [confirmInsurance, setConfirmInsurance] = useState<string>('')
|
||||
const [confirmChiefComplaint, setConfirmChiefComplaint] = useState<string>('')
|
||||
const [confirmPatientNotes, setConfirmPatientNotes] = useState<string>('')
|
||||
|
||||
// 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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation dialog shown when a user selects a slot */}
|
||||
<Dialog open={confirmOpen} onOpenChange={(open) => { if (!open) { setConfirmOpen(false); setPendingAppointment(null); } }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirmar agendamento</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-2">
|
||||
{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 (
|
||||
<div className="space-y-2">
|
||||
<p>Profissional: <strong>{doctorName}</strong></p>
|
||||
<p>Data / Hora: <strong>{when}</strong></p>
|
||||
<p>Paciente: <strong>Você</strong></p>
|
||||
</div>
|
||||
)
|
||||
})()
|
||||
) : (
|
||||
<p>Carregando informações...</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button variant="ghost" onClick={() => { setConfirmOpen(false); setPendingAppointment(null); }}>Cancelar</Button>
|
||||
<Button onClick={confirmAndBook} disabled={confirmLoading}>{confirmLoading ? 'Agendando...' : 'Marcar consulta'}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Hero de filtros (mantido) */}
|
||||
<section className="rounded-3xl bg-primary p-6 text-primary-foreground shadow-lg">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
@ -644,7 +739,7 @@ export default function ResultadosClient() {
|
||||
{nearestSlotByDoctor[id] && (
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<span className="text-sm text-muted-foreground">Próximo horário:</span>
|
||||
<Button className="h-9 rounded-full bg-primary/10 text-primary" onClick={() => agendar(id, nearestSlotByDoctor[id]!.iso)}>
|
||||
<Button className="h-9 rounded-full bg-primary/10 text-primary" onClick={() => openConfirmDialog(id, nearestSlotByDoctor[id]!.iso)}>
|
||||
{nearestSlotByDoctor[id]!.label}
|
||||
</Button>
|
||||
</div>
|
||||
@ -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}
|
||||
</button>
|
||||
@ -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}
|
||||
</button>
|
||||
@ -873,7 +968,7 @@ export default function ResultadosClient() {
|
||||
) : (moreTimesSlots.length ? (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{moreTimesSlots.map(s => (
|
||||
<button key={s.iso} type="button" className="rounded-lg bg-primary/10 px-3 py-2 text-sm text-primary hover:bg-primary hover:text-primary-foreground" onClick={() => { if (moreTimesForDoctor) { agendar(moreTimesForDoctor, s.iso); setMoreTimesForDoctor(null); } }}>
|
||||
<button key={s.iso} type="button" className="rounded-lg bg-primary/10 px-3 py-2 text-sm text-primary hover:bg-primary hover:text-primary-foreground" onClick={() => { if (moreTimesForDoctor) { openConfirmDialog(moreTimesForDoctor, s.iso); setMoreTimesForDoctor(null); } }}>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@ -1196,6 +1196,60 @@ export async function criarAgendamento(input: AppointmentCreate): Promise<Appoin
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria um agendamento direto no endpoint REST sem realizar validações locais
|
||||
* como checagem de disponibilidade ou exceções. Use com cautela.
|
||||
*/
|
||||
export async function criarAgendamentoDireto(input: AppointmentCreate & { created_by?: string | null }): Promise<Appointment> {
|
||||
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<Appointment>(res);
|
||||
return created;
|
||||
}
|
||||
|
||||
// Payload for updating an appointment (PATCH /rest/v1/appointments/{id})
|
||||
export type AppointmentUpdate = Partial<{
|
||||
scheduled_at: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user