diff --git a/app/patient/schedule/page.tsx b/app/patient/schedule/page.tsx index 34496f9..68c8fda 100644 --- a/app/patient/schedule/page.tsx +++ b/app/patient/schedule/page.tsx @@ -4,19 +4,19 @@ 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 } from "react"; -import { Calendar, Clock, User } from "lucide-react"; +import { useState, useEffect, useCallback, useRef } from "react"; +import { Calendar, Clock, User, StickyNote } from "lucide-react"; import PatientLayout from "@/components/patient-layout"; import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, @@ -26,8 +26,8 @@ import { SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; +import { toast } from "@/hooks/use-toast"; -const API_URL = " https://yuanqfswhberkoevtmfr.supabase.co/"; interface Doctor { id: string; @@ -36,6 +36,8 @@ interface Doctor { } interface Disponibilidade { + id?: string; + doctor_id?: string; weekday: string; start_time: string; end_time: string; @@ -50,11 +52,26 @@ export default function ScheduleAppointment() { 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 { @@ -62,11 +79,115 @@ export default function ScheduleAppointment() { 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; @@ -74,36 +195,28 @@ export default function ScheduleAppointment() { setAvailableTimes([]); try { - const disponibilidades: Disponibilidade[] = - await AvailabilityService.listById(doctorId); + const disponibilidades: Disponibilidade[] = await AvailabilityService.listById(doctorId); + const consultas = await appointmentsService.search_appointment( - `doctor_id=eq.${doctorId}&scheduled_at=gte.${date}&scheduled_at=lt.${date}T23:59:59` + `doctor_id=eq.${doctorId}&scheduled_at=gte.${date}T00:00:00Z&scheduled_at=lt.${date}T23:59:59Z` ); - const diaJS = new Date(date).getDay(); - // Ajuste: Sunday = 0 -> API pode esperar 1-7 + const diaJS = new Date(date).getDay(); // 0..6 const diaAPI = diaJS === 0 ? 7 : diaJS; - console.log("Disponibilidades recebidas: ", disponibilidades); - console.log("Consultas do dia: ", consultas); - const disponibilidadeDia = disponibilidades.find( - (d: Disponibilidade) => Number(diaAPI) === getWeekdayNumber(d.weekday) + (d) => getWeekdayNumber(d.weekday) === diaAPI ); if (!disponibilidadeDia) { - console.log("Nenhuma disponibilidade para este dia"); 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 [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[] = []; @@ -113,20 +226,26 @@ export default function ScheduleAppointment() { const end = new Date(date); end.setHours(endHour, endMin, 0, 0); - while (atual < end) { + while (atual <= end) { horariosGerados.push(atual.toTimeString().slice(0, 5)); - atual = new Date(atual.getTime() + slot * 60 * 1000); + atual = new Date(atual.getTime() + slot * 60000); } - const ocupados = consultas.map((c: any) => - c.scheduled_at.split("T")[1].slice(0, 5) + 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); } @@ -134,32 +253,7 @@ export default function ScheduleAppointment() { [] ); - const getWeekdayNumber = (weekday: string) => { - // Converte weekday API para número: 1=Monday ... 7=Sunday - switch (weekday.toLowerCase()) { - case "monday": - return 1; - case "tuesday": - return 2; - case "wednesday": - return 3; - case "thursday": - return 4; - case "friday": - return 5; - case "saturday": - return 6; - case "sunday": - return 7; - default: - return 0; - } - }; - - useEffect(() => { - fetchDoctors(); - }, [fetchDoctors]); - + // run fetchAvailableSlots when date changes useEffect(() => { if (selectedDoctor && selectedDate) { fetchAvailableSlots(selectedDoctor, selectedDate); @@ -169,193 +263,292 @@ export default function ScheduleAppointment() { setSelectedTime(""); }, [selectedDoctor, selectedDate, fetchAvailableSlots]); + // --- Submit --- const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedDoctor || !selectedDate || !selectedTime) { - alert("Selecione médico, data e horário."); + toast({ title: "Preencha os campos", description: "Selecione médico, data e horário." }); return; } - const doctor = doctors.find((d) => d.id === selectedDoctor); - const scheduledISO = `${selectedDate}T${selectedTime}:00Z`; - - const paciente = await usersService.getMe(); - const body = { - doctor_id: doctor?.id, - patient_id: paciente.user.id, - scheduled_at: scheduledISO, - duration_minutes: Number(duracao), - created_by: paciente.user.id, - }; - try { - const res = await fetch(`${API_URL}/appointments`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); + const doctor = doctors.find((d) => d.id === selectedDoctor); + const paciente = await usersService.getMe(); - if (!res.ok) throw new Error("Erro ao agendar consulta"); + 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, + }; - alert("Consulta agendada com sucesso!"); + 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); - alert("Falha ao agendar consulta"); + 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

+
+

Agendar Consulta

- + Dados da Consulta - Escolha o médico, data e horário + -
- {/* Médico */} -
- - -
- - {/* Data */} -
- - setSelectedDate(e.target.value)} - min={new Date().toISOString().split("T")[0]} - disabled={!selectedDoctor} - /> -
- - {/* Horário */} -
- - -
- - {/* Tipo e duração */} -
+ + {/* LEFT */} +
- - - + + {selectedDoctor && doctors.find(d => d.id === selectedDoctor)?.full_name} + - Presencial - Online + {loadingDoctors ? ( + Carregando... + ) : ( + doctors.map((d) => ( + + {d.full_name} — {d.specialty} + + )) + )}
+
- - setDuracao(e.target.value)} - min={10} - max={120} + +
+ { + 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", + }} + /> + + + +
+ +
+ +
+ +