Merge pull request #27 from m1guelmcf/ajustes-agendamentio

Ajustes agendamentio
This commit is contained in:
DaniloSts 2025-11-27 09:12:02 -03:00 committed by GitHub
commit 98f14efe00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -41,8 +41,8 @@ import {
export default function ScheduleForm() { export default function ScheduleForm() {
// Estado do usuário e role // Estado do usuário e role
const [role, setRole] = useState<string>("paciente"); const [role, setRole] = useState<string>("paciente")
const [userId, setUserId] = useState<string | null>(null); const [userId, setUserId] = useState<string | null>(null)
// Listas e seleções // Listas e seleções
const [patients, setPatients] = useState<any[]>([]); const [patients, setPatients] = useState<any[]>([]);
@ -61,227 +61,186 @@ export default function ScheduleForm() {
const [loadingSlots, setLoadingSlots] = useState(false); const [loadingSlots, setLoadingSlots] = useState(false);
// Outras configs // Outras configs
const [tipoConsulta] = useState("presencial"); const [tipoConsulta] = useState("presencial")
const [duracao] = useState("30"); const [duracao] = useState("30")
const [disponibilidades, setDisponibilidades] = useState<any[]>([]); const [disponibilidades, setDisponibilidades] = useState<any[]>([])
const [availabilityCounts, setAvailabilityCounts] = useState< const [availabilityCounts, setAvailabilityCounts] = useState<Record<string, number>>({})
Record<string, number> const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string } | null>(null)
>({}); const calendarRef = useRef<HTMLDivElement | null>(null)
const [tooltip, setTooltip] = useState<{
x: number;
y: number;
text: string;
} | null>(null);
const calendarRef = useRef<HTMLDivElement | null>(null);
// Funções auxiliares // Funções auxiliares
const getWeekdayNumber = (weekday: string) => const getWeekdayNumber = (weekday: string) =>
[ ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].indexOf(weekday.toLowerCase()) + 1
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
].indexOf(weekday.toLowerCase()) + 1;
const getBrazilDate = (date: Date) => const getBrazilDate = (date: Date) =>
new Date( new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0))
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)
);
// 🔹 Buscar dados do usuário e role // 🔹 Buscar dados do usuário e role
useEffect(() => { useEffect(() => {
(async () => { ;(async () => {
try { try {
const me = await usersService.getMe(); const me = await usersService.getMe()
const currentRole = me?.roles?.[0] || "paciente"; const currentRole = me?.roles?.[0] || "paciente"
setRole(currentRole); setRole(currentRole)
setUserId(me?.user?.id || null); setUserId(me?.user?.id || null)
if (["secretaria", "gestor", "admin"].includes(currentRole)) { if (["secretaria", "gestor", "admin"].includes(currentRole)) {
const pats = await patientsService.list(); const pats = await patientsService.list()
setPatients(pats || []); setPatients(pats || [])
} }
} catch (err) { } catch (err) {
console.error("Erro ao carregar usuário:", err); console.error("Erro ao carregar usuário:", err)
} }
})(); })()
}, []); }, [])
// 🔹 Buscar médicos // 🔹 Buscar médicos
const fetchDoctors = useCallback(async () => { const fetchDoctors = useCallback(async () => {
setLoadingDoctors(true); setLoadingDoctors(true)
try { try {
const data = await doctorsService.list(); const data = await doctorsService.list()
setDoctors(data || []); setDoctors(data || [])
} catch (err) { } catch (err) {
console.error("Erro ao buscar médicos:", err); console.error("Erro ao buscar médicos:", err)
toast({ toast({ title: "Erro", description: "Não foi possível carregar médicos." })
title: "Erro",
description: "Não foi possível carregar médicos.",
});
} finally { } finally {
setLoadingDoctors(false); setLoadingDoctors(false)
} }
}, []); }, [])
useEffect(() => { useEffect(() => {
fetchDoctors(); fetchDoctors()
}, [fetchDoctors]); }, [fetchDoctors])
// 🔹 Buscar disponibilidades // 🔹 Buscar disponibilidades
const loadDoctorDisponibilidades = useCallback(async (doctorId?: string) => { const loadDoctorDisponibilidades = useCallback(async (doctorId?: string) => {
if (!doctorId) return; if (!doctorId) return
try { try {
const disp = await AvailabilityService.listById(doctorId); const disp = await AvailabilityService.listById(doctorId)
setDisponibilidades(disp || []); setDisponibilidades(disp || [])
await computeAvailabilityCountsPreview(doctorId, disp || []); await computeAvailabilityCountsPreview(doctorId, disp || [])
} catch (err) { } catch (err) {
console.error("Erro ao buscar disponibilidades:", err); console.error("Erro ao buscar disponibilidades:", err)
setDisponibilidades([]); setDisponibilidades([])
} }
}, []); }, [])
const computeAvailabilityCountsPreview = async ( const computeAvailabilityCountsPreview = async (
doctorId: string, doctorId: string,
dispList: any[] dispList: any[]
) => { ) => {
try { try {
const today = new Date(); const today = new Date()
const start = format(today, "yyyy-MM-dd"); const start = format(today, "yyyy-MM-dd")
const endDate = addDays(today, 90); const endDate = addDays(today, 90)
const end = format(endDate, "yyyy-MM-dd"); const end = format(endDate, "yyyy-MM-dd")
const appointments = await appointmentsService.search_appointment( const appointments = await appointmentsService.search_appointment(
`doctor_id=eq.${doctorId}&scheduled_at=gte.${start}T00:00:00Z&scheduled_at=lt.${end}T23:59:59Z` `doctor_id=eq.${doctorId}&scheduled_at=gte.${start}T00:00:00Z&scheduled_at=lt.${end}T23:59:59Z`,
); )
const apptsByDate: Record<string, number> = {}; const apptsByDate: Record<string, number> = {}
(appointments || []).forEach((a: any) => { ;(appointments || []).forEach((a: any) => {
const d = String(a.scheduled_at).split("T")[0]; const d = String(a.scheduled_at).split("T")[0]
apptsByDate[d] = (apptsByDate[d] || 0) + 1; apptsByDate[d] = (apptsByDate[d] || 0) + 1
}); })
const counts: Record<string, number> = {}; const counts: Record<string, number> = {}
for (let i = 0; i <= 90; i++) { for (let i = 0; i <= 90; i++) {
const d = addDays(today, i); const d = addDays(today, i)
const key = format(d, "yyyy-MM-dd"); const key = format(d, "yyyy-MM-dd")
const dayOfWeek = d.getDay() === 0 ? 7 : d.getDay(); const dayOfWeek = d.getDay() === 0 ? 7 : d.getDay()
const dailyDisp = dispList.filter( const dailyDisp = dispList.filter((p) => getWeekdayNumber(p.weekday) === dayOfWeek)
(p) => getWeekdayNumber(p.weekday) === dayOfWeek
);
if (dailyDisp.length === 0) { if (dailyDisp.length === 0) {
counts[key] = 0; counts[key] = 0
continue; continue
} }
let possible = 0; let possible = 0
dailyDisp.forEach((p) => { dailyDisp.forEach((p) => {
const [sh, sm] = p.start_time.split(":").map(Number); const [sh, sm] = p.start_time.split(":").map(Number)
const [eh, em] = p.end_time.split(":").map(Number); const [eh, em] = p.end_time.split(":").map(Number)
const startMin = sh * 60 + sm; const startMin = sh * 60 + sm
const endMin = eh * 60 + em; const endMin = eh * 60 + em
const slot = p.slot_minutes || 30; const slot = p.slot_minutes || 30
if (endMin >= startMin) if (endMin >= startMin) possible += Math.floor((endMin - startMin) / slot) + 1
possible += Math.floor((endMin - startMin) / slot) + 1; })
}); const occupied = apptsByDate[key] || 0
const occupied = apptsByDate[key] || 0; counts[key] = Math.max(0, possible - occupied)
counts[key] = Math.max(0, possible - occupied);
} }
setAvailabilityCounts(counts); setAvailabilityCounts(counts)
} catch (err) { } catch (err) {
console.error("Erro ao calcular contagens:", err); console.error("Erro ao calcular contagens:", err)
setAvailabilityCounts({}); setAvailabilityCounts({})
}
} }
};
// 🔹 Quando médico muda // 🔹 Quando médico muda
useEffect(() => { useEffect(() => {
if (selectedDoctor) { if (selectedDoctor) {
loadDoctorDisponibilidades(selectedDoctor); loadDoctorDisponibilidades(selectedDoctor)
} else { } else {
setDisponibilidades([]); setDisponibilidades([])
setAvailabilityCounts({}); setAvailabilityCounts({})
} }
setSelectedDate(""); setSelectedDate("")
setSelectedTime(""); setSelectedTime("")
setAvailableTimes([]); setAvailableTimes([])
}, [selectedDoctor, loadDoctorDisponibilidades]); }, [selectedDoctor, loadDoctorDisponibilidades])
// 🔹 Buscar horários disponíveis // 🔹 Buscar horários disponíveis
const fetchAvailableSlots = useCallback( const fetchAvailableSlots = useCallback(async (doctorId: string, date: string) => {
async (doctorId: string, date: string) => { if (!doctorId || !date) return
if (!doctorId || !date) return; setLoadingSlots(true)
setLoadingSlots(true); setAvailableTimes([])
setAvailableTimes([]);
try { try {
const disponibilidades = await AvailabilityService.listById(doctorId); const disponibilidades = await AvailabilityService.listById(doctorId)
const consultas = await appointmentsService.search_appointment( const consultas = await appointmentsService.search_appointment(
`doctor_id=eq.${doctorId}&scheduled_at=gte.${date}T00:00:00Z&scheduled_at=lt.${date}T23:59:59Z` `doctor_id=eq.${doctorId}&scheduled_at=gte.${date}T00:00:00Z&scheduled_at=lt.${date}T23:59:59Z`,
); )
const diaJS = new Date(date).getDay(); const diaJS = new Date(date).getDay()
const diaAPI = diaJS === 0 ? 7 : diaJS; const diaAPI = diaJS === 0 ? 7 : diaJS
const disponibilidadeDia = disponibilidades.find( const disponibilidadeDia = disponibilidades.find((d: any) => getWeekdayNumber(d.weekday) === diaAPI)
(d: any) => getWeekdayNumber(d.weekday) === diaAPI
);
if (!disponibilidadeDia) { if (!disponibilidadeDia) {
toast({ toast({ title: "Nenhuma disponibilidade", description: "Nenhum horário para este dia." })
title: "Nenhuma disponibilidade", return setAvailableTimes([])
description: "Nenhum horário para este dia.",
});
return setAvailableTimes([]);
} }
const [startHour, startMin] = disponibilidadeDia.start_time const [startHour, startMin] = disponibilidadeDia.start_time.split(":").map(Number)
.split(":") const [endHour, endMin] = disponibilidadeDia.end_time.split(":").map(Number)
.map(Number); const slot = disponibilidadeDia.slot_minutes || 30
const [endHour, endMin] = disponibilidadeDia.end_time const horariosGerados: string[] = []
.split(":") let atual = new Date(date)
.map(Number); atual.setHours(startHour, startMin, 0, 0)
const slot = disponibilidadeDia.slot_minutes || 30; const end = new Date(date)
const horariosGerados: string[] = []; end.setHours(endHour, endMin, 0, 0)
let atual = new Date(date);
atual.setHours(startHour, startMin, 0, 0);
const end = new Date(date);
end.setHours(endHour, endMin, 0, 0);
while (atual <= end) { while (atual <= end) {
horariosGerados.push(atual.toTimeString().slice(0, 5)); horariosGerados.push(atual.toTimeString().slice(0, 5))
atual = new Date(atual.getTime() + slot * 60000); atual = new Date(atual.getTime() + slot * 60000)
} }
const ocupados = (consultas || []).map((c: any) => const ocupados = (consultas || []).map((c: any) => String(c.scheduled_at).split("T")[1]?.slice(0, 5))
String(c.scheduled_at).split("T")[1]?.slice(0, 5) const livres = horariosGerados.filter((h) => !ocupados.includes(h))
); setAvailableTimes(livres)
const livres = horariosGerados.filter((h) => !ocupados.includes(h));
setAvailableTimes(livres);
} catch (err) { } catch (err) {
console.error(err); console.error(err)
toast({ title: "Erro", description: "Falha ao carregar horários." }); toast({ title: "Erro", description: "Falha ao carregar horários." })
} finally { } finally {
setLoadingSlots(false); setLoadingSlots(false)
} }
}, }, [])
[]
);
useEffect(() => { useEffect(() => {
if (selectedDoctor && selectedDate) if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate)
fetchAvailableSlots(selectedDoctor, selectedDate); }, [selectedDoctor, selectedDate, fetchAvailableSlots])
}, [selectedDoctor, selectedDate, fetchAvailableSlots]);
// 🔹 Submeter agendamento // 🔹 Submeter agendamento
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault()
const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role); const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role)
const patientId = isSecretaryLike ? selectedPatient : userId; const patientId = isSecretaryLike ? selectedPatient : userId
if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) {
toast({ toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." })
title: "Campos obrigatórios", return
description: "Preencha todos os campos.",
});
return;
} }
try { try {
@ -292,7 +251,7 @@ export default function ScheduleForm() {
duration_minutes: Number(duracao), duration_minutes: Number(duracao),
notes, notes,
appointment_type: tipoConsulta, appointment_type: tipoConsulta,
}; }
await appointmentsService.create(body); await appointmentsService.create(body);
@ -309,44 +268,38 @@ export default function ScheduleForm() {
try { try {
if (isSecretaryLike) { if (isSecretaryLike) {
const patient = patients.find((p: any) => p.id === patientId); const patient = patients.find((p: any) => p.id === patientId)
const rawPhone = patient?.phone || patient?.phone_mobile || null; const rawPhone = patient?.phone || patient?.phone_mobile || null
if (rawPhone) phoneNumber = rawPhone; if (rawPhone) phoneNumber = rawPhone
} else { } else {
const me = await usersService.getMe(); const me = await usersService.getMe()
const rawPhone = const rawPhone =
me?.profile?.phone || me?.profile?.phone ||
(typeof me?.profile === "object" && "phone_mobile" in me.profile (typeof me?.profile === "object" && "phone_mobile" in me.profile
? (me.profile as any).phone_mobile ? (me.profile as any).phone_mobile
: null) || : null) ||
(typeof me === "object" && "user_metadata" in me (typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) ||
? (me as any).user_metadata?.phone null
: null) || if (rawPhone) phoneNumber = rawPhone
null;
if (rawPhone) phoneNumber = rawPhone;
} }
// 🔹 Normaliza para formato internacional (+55)
if (phoneNumber) { if (phoneNumber) {
phoneNumber = phoneNumber.replace(/\D/g, ""); phoneNumber = phoneNumber.replace(/\D/g, "")
if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`
phoneNumber = `+${phoneNumber}`; phoneNumber = `+${phoneNumber}`
} }
console.log("📞 Telefone usado:", phoneNumber); console.log("📞 Telefone usado:", phoneNumber)
} catch (err) { } catch (err) {
console.warn("⚠️ Não foi possível obter telefone do paciente:", err); console.warn("⚠️ Não foi possível obter telefone do paciente:", err)
} }
// 💬 envia o SMS de confirmação
// 💬 Envia o SMS de lembrete (sem mostrar nada ao paciente)
// 💬 Envia o SMS de lembrete (somente loga no console, não mostra no sistema)
try { try {
const smsRes = await smsService.sendSms({ const smsRes = await smsService.sendSms({
phone_number: phoneNumber, phone_number: phoneNumber,
message: `Lembrete: sua consulta é em ${dateFormatted} às ${selectedTime} na Clínica MediConnect.`, message: `Lembrete: sua consulta é em ${dateFormatted} às ${selectedTime} na Clínica MediConnect.`,
patient_id: patientId, patient_id: patientId,
}); })
if (smsRes?.success) { if (smsRes?.success) {
console.log("✅ SMS enviado com sucesso:", smsRes.message_sid); console.log("✅ SMS enviado com sucesso:", smsRes.message_sid);
@ -371,187 +324,100 @@ export default function ScheduleForm() {
// 🔹 Tooltip no calendário // 🔹 Tooltip no calendário
useEffect(() => { useEffect(() => {
const cont = calendarRef.current; const cont = calendarRef.current
if (!cont) return; if (!cont) return
const onMove = (ev: MouseEvent) => { const onMove = (ev: MouseEvent) => {
const target = ev.target as HTMLElement | null; const target = ev.target as HTMLElement | null
const btn = target?.closest("button"); const btn = target?.closest("button")
if (!btn) return setTooltip(null); if (!btn) return setTooltip(null)
const aria = btn.getAttribute("aria-label") || btn.textContent || ""; const aria = btn.getAttribute("aria-label") || btn.textContent || ""
const parsed = new Date(aria); const parsed = new Date(aria)
if (isNaN(parsed.getTime())) return setTooltip(null); if (isNaN(parsed.getTime())) return setTooltip(null)
const key = format(getBrazilDate(parsed), "yyyy-MM-dd"); const key = format(getBrazilDate(parsed), "yyyy-MM-dd")
const count = availabilityCounts[key] ?? 0; const count = availabilityCounts[key] ?? 0
setTooltip({ setTooltip({
x: ev.pageX + 10, x: ev.pageX + 10,
y: ev.pageY + 10, y: ev.pageY + 10,
text: `${count} horário${count !== 1 ? "s" : ""} disponíveis`, text: `${count} horário${count !== 1 ? "s" : ""} disponíveis`,
}); })
}; }
const onLeave = () => setTooltip(null); const onLeave = () => setTooltip(null)
cont.addEventListener("mousemove", onMove); cont.addEventListener("mousemove", onMove)
cont.addEventListener("mouseleave", onLeave); cont.addEventListener("mouseleave", onLeave)
return () => { return () => {
cont.removeEventListener("mousemove", onMove); cont.removeEventListener("mousemove", onMove)
cont.removeEventListener("mouseleave", onLeave); cont.removeEventListener("mouseleave", onLeave)
}; }
}, [availabilityCounts]); }, [availabilityCounts])
return ( return (
<div className="max-w-6xl mx-auto space-y-4 px-4"> <div className="py-3 px-2">
<h1 className="text-2xl font-semibold">Agendar Consulta</h1> <div className="max-w-7xl mx-auto space-y-3">
<div className="space-y-1">
<h1 className="text-3xl font-bold text-slate-900">Agendar Consulta</h1>
<p className="text-slate-600">Selecione o médico, data e horário para sua consulta</p>
</div>
<Card className="border rounded-xl shadow-sm"> <div className="grid lg:grid-cols-[1fr,380px] gap-4">
<CardHeader> {/* Coluna do Formulário */}
<CardTitle>Dados da Consulta</CardTitle> <form onSubmit={handleSubmit} className="space-y-3">
<Card className="border shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg">Informações Básicas</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{["secretaria", "gestor", "admin"].includes(role) && (
<div className="space-y-2">
<Label htmlFor="patient-select" className="text-sm font-medium">
Paciente
</Label>
<Select value={selectedPatient} onValueChange={setSelectedPatient}>
<SelectTrigger id="patient-select">
<SelectValue placeholder="Selecione o paciente" />
</SelectTrigger>
<SelectContent>
{patients.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.full_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<Label htmlFor="doctor-select" className="text-sm font-medium">
Médico
</Label>
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}>
<SelectTrigger id="doctor-select">
<SelectValue placeholder="Selecione o médico" />
</SelectTrigger>
<SelectContent>
{loadingDoctors ? (
<SelectItem value="loading" disabled>
Carregando...
</SelectItem>
) : (
doctors.map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.full_name} {d.specialty}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card className="border shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg">Selecione a Data</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form <div ref={calendarRef} className="flex justify-center">
onSubmit={handleSubmit}
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
<div className="space-y-4">
{" "}
{/* Ajuste: maior espaçamento vertical geral */}
{/* Se secretária/gestor/admin → COMBOBOX de Paciente */}
{["secretaria", "gestor", "admin"].includes(role) && (
<div className="flex flex-col gap-2">
{" "}
{/* Ajuste: gap entre Label e Input */}
<Label>Paciente</Label>
<Popover
open={openPatientCombobox}
onOpenChange={setOpenPatientCombobox}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openPatientCombobox}
className="w-full justify-between"
>
{selectedPatient
? patients.find((p) => p.id === selectedPatient)
?.full_name
: "Selecione o paciente..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0">
<Command>
<CommandInput placeholder="Buscar paciente..." />
<CommandList>
<CommandEmpty>
Nenhum paciente encontrado.
</CommandEmpty>
<CommandGroup>
{patients.map((patient) => (
<CommandItem
key={patient.id}
value={patient.full_name}
onSelect={() => {
setSelectedPatient(
patient.id === selectedPatient
? ""
: patient.id
);
setOpenPatientCombobox(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedPatient === patient.id
? "opacity-100"
: "opacity-0"
)}
/>
{patient.full_name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)}
{/* COMBOBOX de Médico (Nova funcionalidade) */}
<div className="flex flex-col gap-2">
{" "}
{/* Ajuste: gap entre Label e Input */}
<Label>Médico</Label>
<Popover
open={openDoctorCombobox}
onOpenChange={setOpenDoctorCombobox}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openDoctorCombobox}
className="w-full justify-between"
disabled={loadingDoctors}
>
{loadingDoctors
? "Carregando médicos..."
: selectedDoctor
? doctors.find((d) => d.id === selectedDoctor)
?.full_name
: "Selecione o médico..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0">
<Command>
<CommandInput placeholder="Buscar médico..." />
<CommandList>
<CommandEmpty>Nenhum médico encontrado.</CommandEmpty>
<CommandGroup>
{[...doctors]
.sort((a, b) =>
String(a.full_name).localeCompare(
String(b.full_name)
)
)
.map((doctor) => (
<CommandItem
key={doctor.id}
value={doctor.full_name}
onSelect={() => {
setSelectedDoctor(
doctor.id === selectedDoctor
? ""
: doctor.id
);
setOpenDoctorCombobox(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedDoctor === doctor.id
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{doctor.full_name}</span>
<span className="text-xs text-muted-foreground">
{doctor.specialty}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col gap-2">
<Label>Data</Label>
<div ref={calendarRef} className="rounded-lg border p-2">
<CalendarShadcn <CalendarShadcn
mode="single" mode="single"
disabled={!selectedDoctor} disabled={!selectedDoctor}
@ -561,64 +427,72 @@ export default function ScheduleForm() {
: undefined : undefined
} }
onSelect={(date) => { onSelect={(date) => {
if (!date) return; if (!date) return
const formatted = format( const formatted = format(new Date(date.getTime() + 12 * 60 * 60 * 1000), "yyyy-MM-dd")
new Date(date.getTime() + 12 * 60 * 60 * 1000), setSelectedDate(formatted)
"yyyy-MM-dd"
);
setSelectedDate(formatted);
}} }}
className="rounded-md"
/> />
</div> </div>
</div> </CardContent>
<div className="flex flex-col gap-2"> </Card>
<Label>Observações</Label>
<Card className="border shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-lg">Observações (Opcional)</CardTitle>
</CardHeader>
<CardContent>
<Textarea <Textarea
placeholder="Instruções para o médico..." placeholder="Adicione instruções ou informações importantes para o médico..."
value={notes} value={notes}
onChange={(e) => setNotes(e.target.value)} onChange={(e) => setNotes(e.target.value)}
className="mt-1" rows={4}
className="resize-none"
/> />
</div> </CardContent>
</Card>
</form>
<div className="space-y-3">
<Card className="border-2 border-blue-200 shadow-lg bg-gradient-to-br from-blue-50 to-white sticky top-6">
<CardHeader className="pb-3 border-b border-blue-100">
<CardTitle className="text-blue-900 flex items-center gap-2">
<User className="h-5 w-5" />
Resumo da Consulta
</CardTitle>
</CardHeader>
<CardContent className="pt-3 space-y-3">
<div className="space-y-1">
<p className="text-xs font-medium text-slate-600">Médico</p>
<p className="text-sm font-semibold text-slate-900">
{selectedDoctor ? doctors.find((d) => d.id === selectedDoctor)?.full_name : "Não selecionado"}
</p>
</div> </div>
<div className="space-y-4"> <div className="space-y-1">
{" "} <p className="text-xs font-medium text-slate-600">Data</p>
{/* Ajuste: Espaçamento no lado direito também */} <p className="text-sm font-semibold text-slate-900">
<Card className="shadow-md rounded-xl bg-blue-50 border border-blue-200"> {selectedDate ? format(new Date(selectedDate + "T12:00:00"), "dd/MM/yyyy") : "Não selecionada"}
<CardHeader> </p>
<CardTitle className="text-blue-700">Resumo</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-gray-900 text-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-blue-600" />
<div className="text-xs">
{selectedDoctor
? doctors.find((d) => d.id === selectedDoctor)
?.full_name
: "Médico"}
</div>
</div>
<div className="text-xs text-gray-600">
{tipoConsulta} {duracao} min
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Horário</Label> <Label htmlFor="time-select" className="text-xs font-medium text-slate-600">
Horário
</Label>
<Select <Select
value={selectedTime}
onValueChange={setSelectedTime} onValueChange={setSelectedTime}
disabled={loadingSlots || availableTimes.length === 0} disabled={loadingSlots || availableTimes.length === 0}
> >
<SelectTrigger> <SelectTrigger id="time-select" className="bg-white">
<SelectValue <SelectValue
placeholder={ placeholder={
loadingSlots loadingSlots
? "Carregando horários..." ? "Carregando..."
: availableTimes.length === 0 : availableTimes.length === 0
? "Nenhum horário disponível" ? "Nenhum horário"
: "Selecione o horário" : "Escolha o horário"
} }
/> />
</SelectTrigger> </SelectTrigger>
@ -632,41 +506,55 @@ export default function ScheduleForm() {
</Select> </Select>
</div> </div>
<div className="pt-3 border-t border-blue-100 space-y-2">
<div className="flex justify-between text-xs">
<span className="text-slate-600">Tipo:</span>
<span className="font-medium text-slate-900 capitalize">{tipoConsulta}</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-slate-600">Duração:</span>
<span className="font-medium text-slate-900">{duracao} minutos</span>
</div>
</div>
{notes && ( {notes && (
<div className="flex items-start gap-2 text-sm"> <div className="pt-3 border-t border-blue-100">
<StickyNote className="h-4 w-4" /> <div className="flex items-start gap-2">
<div className="italic text-gray-700">{notes}</div> <StickyNote className="h-4 w-4 text-blue-600 mt-0.5 flex-shrink-0" />
<p className="text-xs italic text-slate-700 line-clamp-3">{notes}</p>
</div>
</div> </div>
)} )}
</CardContent>
</Card> <div className="pt-2 space-y-2">
<div className="flex gap-2">
<Button <Button
type="submit" type="submit"
className="w-full md:w-auto px-4 py-1.5 text-sm bg-blue-600 text-white hover:bg-blue-700" onClick={handleSubmit}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium shadow-md"
disabled={!selectedDoctor || !selectedDate || !selectedTime} disabled={!selectedDoctor || !selectedDate || !selectedTime}
> >
Agendar Confirmar Agendamento
</Button> </Button>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setSelectedDoctor(""); setSelectedDoctor("")
setSelectedDate(""); setSelectedDate("")
setSelectedTime(""); setSelectedTime("")
setNotes(""); setNotes("")
setSelectedPatient(""); setSelectedPatient("")
}} }}
className="px-3" className="w-full"
> >
Limpar Limpar Formulário
</Button> </Button>
</div> </div>
</div>
</form>
</CardContent> </CardContent>
</Card> </Card>
</div>
</div>
</div>
{tooltip && ( {tooltip && (
<div <div
@ -677,14 +565,16 @@ export default function ScheduleForm() {
zIndex: 60, zIndex: 60,
background: "rgba(0,0,0,0.85)", background: "rgba(0,0,0,0.85)",
color: "white", color: "white",
padding: "6px 8px", padding: "6px 10px",
borderRadius: 6, borderRadius: 6,
fontSize: 12, fontSize: 12,
fontWeight: 500,
pointerEvents: "none",
}} }}
> >
{tooltip.text} {tooltip.text}
</div> </div>
)} )}
</div> </div>
); )
} }