"use client"; import { usersService } from "services/usersApi.mjs"; import { doctorsService } from "services/doctorsApi.mjs"; import { appointmentsService } from "services/appointmentsApi.mjs"; import { AvailabilityService } from "services/availabilityApi.mjs"; import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; import { format, addDays } from "date-fns"; import { useState, useEffect, useCallback, useRef } from "react"; import { Calendar, User, StickyNote } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/hooks/use-toast"; import Sidebar from "@/components/Sidebar"; interface Doctor { id: string; full_name: string; specialty: string; } interface Disponibilidade { id?: string; doctor_id?: string; weekday: string; start_time: string; end_time: string; slot_minutes?: number; } export default function ScheduleAppointment() { const [selectedDoctor, setSelectedDoctor] = useState(""); const [selectedDate, setSelectedDate] = useState(""); const [selectedTime, setSelectedTime] = useState(""); const [doctors, setDoctors] = useState([]); const [availableTimes, setAvailableTimes] = useState([]); const [loadingSlots, setLoadingSlots] = useState(false); const [loadingDoctors, setLoadingDoctors] = useState(true); const [tipoConsulta, setTipoConsulta] = useState("presencial"); const [duracao, setDuracao] = useState("30"); const [notes, setNotes] = useState(""); const [disponibilidades, setDisponibilidades] = useState([]); const [availableWeekdays, setAvailableWeekdays] = useState([]); // 1..7 const [availabilityCounts, setAvailabilityCounts] = useState>({}); // "yyyy-MM-dd" -> count const calendarRef = useRef(null); const tooltipRef = useRef(null); const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string } | null>(null); // --- Helpers --- const getWeekdayNumber = (weekday: string) => ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] .indexOf(weekday.toLowerCase()) + 1; // monday=1 ... sunday=7 const getBrazilDate = (date: Date) => new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)); // --- Fetch doctors --- const fetchDoctors = useCallback(async () => { setLoadingDoctors(true); try { const data: Doctor[] = await doctorsService.list(); setDoctors(data || []); } catch (e) { console.error("Erro ao buscar médicos:", e); toast({ title: "Erro", description: "Não foi possível carregar médicos." }); } finally { setLoadingDoctors(false); } }, []); // --- Load disponibilidades details for selected doctor and compute weekdays --- const loadDoctorDisponibilidades = useCallback(async (doctorId?: string) => { if (!doctorId) { setDisponibilidades([]); setAvailableWeekdays([]); setAvailabilityCounts({}); return; } try { const disp: Disponibilidade[] = await AvailabilityService.listById(doctorId); setDisponibilidades(disp || []); const nums = (disp || []).map((d) => getWeekdayNumber(d.weekday)).filter(Boolean); setAvailableWeekdays(Array.from(new Set(nums))); // compute counts preview for next 90 days await computeAvailabilityCountsPreview(doctorId, disp || []); } catch (e) { console.error("Erro disponibilidades:", e); setDisponibilidades([]); setAvailableWeekdays([]); setAvailabilityCounts({}); } }, []); // --- Compute availability counts for next 90 days (efficient) --- const computeAvailabilityCountsPreview = async (doctorId: string, dispList: Disponibilidade[]) => { try { const today = new Date(); const start = format(today, "yyyy-MM-dd"); const endDate = addDays(today, 90); const end = format(endDate, "yyyy-MM-dd"); // fetch appointments for this doctor for the whole window (one call) const appointments = await appointmentsService.search_appointment( `doctor_id=eq.${doctorId}&scheduled_at=gte.${start}T00:00:00Z&scheduled_at=lt.${end}T23:59:59Z` ); // group appointments by date const apptsByDate: Record = {}; (appointments || []).forEach((a: any) => { const d = String(a.scheduled_at).split("T")[0]; apptsByDate[d] = (apptsByDate[d] || 0) + 1; }); const counts: Record = {}; for (let i = 0; i <= 90; i++) { const d = addDays(today, i); const key = format(d, "yyyy-MM-dd"); const dayOfWeek = d.getDay() === 0 ? 7 : d.getDay(); // 1..7 // find all disponibilidades matching this weekday const dailyDisp = dispList.filter((p) => getWeekdayNumber(p.weekday) === dayOfWeek); if (dailyDisp.length === 0) { counts[key] = 0; continue; } // compute total possible slots for the day summing multiple intervals let possible = 0; dailyDisp.forEach((p) => { const [sh, sm] = p.start_time.split(":").map(Number); const [eh, em] = p.end_time.split(":").map(Number); const startMin = sh * 60 + sm; const endMin = eh * 60 + em; const slot = p.slot_minutes || 30; // inclusive handling: if start==end -> 1 slot? normally not, we do Math.floor((end - start)/slot) + 1 if end >= start if (endMin >= startMin) { possible += Math.floor((endMin - startMin) / slot) + 1; } }); const occupied = apptsByDate[key] || 0; const free = Math.max(0, possible - occupied); counts[key] = free; } setAvailabilityCounts(counts); } catch (e) { console.error("Erro ao calcular contagens de disponibilidade:", e); setAvailabilityCounts({}); } }; // --- When doctor changes --- useEffect(() => { fetchDoctors(); }, [fetchDoctors]); useEffect(() => { if (selectedDoctor) { loadDoctorDisponibilidades(selectedDoctor); } else { setDisponibilidades([]); setAvailableWeekdays([]); setAvailabilityCounts({}); } setSelectedDate(""); setSelectedTime(""); setAvailableTimes([]); }, [selectedDoctor, loadDoctorDisponibilidades]); // --- Fetch available times for date --- (same logic, but shows toast if none) const fetchAvailableSlots = useCallback( async (doctorId: string, date: string) => { if (!doctorId || !date) return; setLoadingSlots(true); setAvailableTimes([]); try { const disponibilidades: Disponibilidade[] = await AvailabilityService.listById(doctorId); const consultas = await appointmentsService.search_appointment( `doctor_id=eq.${doctorId}&scheduled_at=gte.${date}T00:00:00Z&scheduled_at=lt.${date}T23:59:59Z` ); const diaJS = new Date(date).getDay(); // 0..6 const diaAPI = diaJS === 0 ? 7 : diaJS; const disponibilidadeDia = disponibilidades.find( (d) => getWeekdayNumber(d.weekday) === diaAPI ); if (!disponibilidadeDia) { setAvailableTimes([]); toast({ title: "Nenhuma disponibilidade", description: "Nenhuma disponibilidade cadastrada para este dia." }); setLoadingSlots(false); return; } const [startHour, startMin] = disponibilidadeDia.start_time.split(":").map(Number); const [endHour, endMin] = disponibilidadeDia.end_time.split(":").map(Number); const slot = disponibilidadeDia.slot_minutes || 30; const horariosGerados: string[] = []; 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) { horariosGerados.push(atual.toTimeString().slice(0, 5)); atual = new Date(atual.getTime() + slot * 60000); } const ocupados = (consultas || []).map((c: any) => String(c.scheduled_at).split("T")[1]?.slice(0, 5) ); const livres = horariosGerados.filter((h) => !ocupados.includes(h)); if (livres.length === 0) { toast({ title: "Sem horários livres", description: "Todos os horários estão ocupados neste dia." }); } setAvailableTimes(livres); } catch (err) { console.error(err); setAvailableTimes([]); toast({ title: "Erro", description: "Falha ao carregar horários." }); } finally { setLoadingSlots(false); } }, [] ); // run fetchAvailableSlots when date changes useEffect(() => { if (selectedDoctor && selectedDate) { fetchAvailableSlots(selectedDoctor, selectedDate); } else { setAvailableTimes([]); } setSelectedTime(""); }, [selectedDoctor, selectedDate, fetchAvailableSlots]); // --- Submit --- const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedDoctor || !selectedDate || !selectedTime) { toast({ title: "Preencha os campos", description: "Selecione médico, data e horário." }); return; } try { const doctor = doctors.find((d) => d.id === selectedDoctor); const paciente = await usersService.getMe(); const body = { doctor_id: doctor?.id, patient_id: paciente.user.id, scheduled_at: `${selectedDate}T${selectedTime}:00`, // saving as local-ish string (you chose UTC elsewhere) duration_minutes: Number(duracao), notes, appointment_type: tipoConsulta, }; await appointmentsService.create(body); toast({ title: "Agendado", description: "Consulta agendada com sucesso." }); // reset setSelectedDoctor(""); setSelectedDate(""); setSelectedTime(""); setAvailableTimes([]); setNotes(""); // refresh counts if (selectedDoctor) computeAvailabilityCountsPreview(selectedDoctor, disponibilidades); } catch (err) { console.error(err); toast({ title: "Erro", description: "Falha ao agendar consulta." }); } }; // --- Calendar tooltip via event delegation --- useEffect(() => { const cont = calendarRef.current; if (!cont) return; const onMove = (ev: MouseEvent) => { const target = ev.target as HTMLElement | null; if (!target) return; // find closest button that likely is a day cell const btn = target.closest("button"); if (!btn) { setTooltip(null); return; } // many calendar implementations put the date in aria-label, e.g. "November 13, 2025" const aria = btn.getAttribute("aria-label") || btn.textContent || ""; // try to parse date from aria-label: new Date(aria) works for many locales const parsed = new Date(aria); if (isNaN(parsed.getTime())) { // sometimes aria-label is like "13" (just day) - try data-day attribute const dataDay = btn.getAttribute("data-day"); if (dataDay) { // try parse yyyy-mm-dd const pd = new Date(dataDay); if (!isNaN(pd.getTime())) { const key = format(pd, "yyyy-MM-dd"); const count = availabilityCounts[key] ?? 0; setTooltip({ x: ev.pageX + 10, y: ev.pageY + 10, text: `${count} horário${count !== 1 ? "s" : ""} disponíveis`, }); return; } } setTooltip(null); return; } // parsed is valid - convert to yyyy-MM-dd const key = format(getBrazilDate(parsed), "yyyy-MM-dd"); const count = availabilityCounts[key] ?? 0; setTooltip({ x: ev.pageX + 10, y: ev.pageY + 10, text: `${count} horário${count !== 1 ? "s" : ""} disponíveis`, }); }; const onLeave = () => setTooltip(null); cont.addEventListener("mousemove", onMove); cont.addEventListener("mouseleave", onLeave); return () => { cont.removeEventListener("mousemove", onMove); cont.removeEventListener("mouseleave", onLeave); }; }, [availabilityCounts]); return (

Agendar Consulta

Dados da Consulta
{/* LEFT */}
{ if (!date) return; const fixedDate = new Date(date.getTime() + 12 * 60 * 60 * 1000); const formatted = format(fixedDate, "yyyy-MM-dd"); setSelectedDate(formatted); }} className="rounded-md border shadow-sm p-2" modifiers={{ selected: selectedDate ? new Date(selectedDate + 'T12:00:00') : undefined }} modifiersClassNames={{ selected: "bg-blue-600 text-white hover:bg-blue-700 rounded-md", }} />