import { useState, useEffect } from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { format, startOfMonth, endOfMonth, eachDayOfInterval, isBefore, startOfDay, addMonths, subMonths, getDay } from "date-fns"; import { ptBR } from "date-fns/locale"; import { availabilityService, appointmentService } from "../../services"; import type { DoctorAvailability, DoctorException } from "../../services"; interface CalendarPickerProps { doctorId: string; selectedDate?: string; onSelectDate: (date: string) => void; } interface DayStatus { date: Date; available: boolean; // Tem horários disponíveis hasAvailability: boolean; // Médico trabalha neste dia da semana hasBlockException: boolean; // Dia bloqueado por exceção isPast: boolean; // Data já passou } export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: CalendarPickerProps) { const [currentMonth, setCurrentMonth] = useState(new Date()); const [availabilities, setAvailabilities] = useState([]); const [exceptions, setExceptions] = useState([]); const [loading, setLoading] = useState(false); const [availableSlots, setAvailableSlots] = useState>({}); // Carregar disponibilidades e exceções do médico useEffect(() => { if (!doctorId) return; const loadData = async () => { setLoading(true); try { const [availData, exceptData] = await Promise.all([ availabilityService.list({ doctor_id: doctorId, active: true }), availabilityService.listExceptions({ doctor_id: doctorId }), ]); setAvailabilities(Array.isArray(availData) ? availData : []); setExceptions(Array.isArray(exceptData) ? exceptData : []); } catch (error) { console.error("Erro ao carregar dados do calendário:", error); } finally { setLoading(false); } }; loadData(); }, [doctorId, currentMonth]); // Calcular disponibilidade de slots localmente (sem chamar Edge Function) useEffect(() => { if (!doctorId || availabilities.length === 0) return; const checkAvailableSlots = async () => { const start = startOfMonth(currentMonth); const end = endOfMonth(currentMonth); const days = eachDayOfInterval({ start, end }); const slotsMap: Record = {}; // Verificar apenas dias futuros que têm configuração de disponibilidade const today = startOfDay(new Date()); const daysToCheck = days.filter((day) => { const dayOfWeek = getDay(day); // 0-6 const hasConfig = availabilities.some((a) => a.weekday === dayOfWeek); return !isBefore(day, today) && hasConfig; }); // Buscar todos os agendamentos do médico uma vez só let allAppointments: Array<{ scheduled_at: string; status: string }> = []; try { const appointments = await appointmentService.list({ doctor_id: doctorId }); allAppointments = Array.isArray(appointments) ? appointments : []; } catch (error) { console.error("[CalendarPicker] Erro ao buscar agendamentos:", error); } // Calcular slots para cada dia for (const day of daysToCheck) { try { const dateStr = format(day, "yyyy-MM-dd"); const dayOfWeek = getDay(day); // Filtra disponibilidades para o dia da semana const dayAvailability = availabilities.filter( (avail) => avail.weekday === dayOfWeek && avail.active ); if (dayAvailability.length === 0) { slotsMap[dateStr] = false; continue; } // Verifica se há exceção de bloqueio const hasBlockException = exceptions.some( (exc) => exc.date === dateStr && exc.kind === "bloqueio" ); if (hasBlockException) { slotsMap[dateStr] = false; continue; } // Gera todos os slots possíveis const allSlots: string[] = []; for (const avail of dayAvailability) { const startTime = avail.start_time; const endTime = avail.end_time; const slotMinutes = avail.slot_minutes || 30; const [startHour, startMin] = startTime.split(":").map(Number); const [endHour, endMin] = endTime.split(":").map(Number); let currentMinutes = startHour * 60 + startMin; const endMinutes = endHour * 60 + endMin; while (currentMinutes < endMinutes) { const hours = Math.floor(currentMinutes / 60); const minutes = currentMinutes % 60; const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; allSlots.push(timeStr); currentMinutes += slotMinutes; } } // Filtra agendamentos já ocupados para esta data const bookedSlots = allAppointments .filter((apt) => { if (!apt.scheduled_at) return false; const aptDate = new Date(apt.scheduled_at); return ( format(aptDate, "yyyy-MM-dd") === dateStr && apt.status !== "cancelled" && apt.status !== "no_show" ); }) .map((apt) => { const aptDate = new Date(apt.scheduled_at); return format(aptDate, "HH:mm"); }); // Verifica se há pelo menos um slot disponível const availableSlots = allSlots.filter((slot) => !bookedSlots.includes(slot)); slotsMap[dateStr] = availableSlots.length > 0; } catch (error) { console.error(`[CalendarPicker] Erro ao verificar slots para ${format(day, "yyyy-MM-dd")}:`, error); slotsMap[format(day, "yyyy-MM-dd")] = false; } } setAvailableSlots(slotsMap); }; checkAvailableSlots(); }, [doctorId, currentMonth, availabilities, exceptions]); const getDayStatus = (date: Date): DayStatus => { const today = startOfDay(new Date()); const isPast = isBefore(date, today); const dayOfWeek = getDay(date); // 0-6 (domingo-sábado) const dateStr = format(date, "yyyy-MM-dd"); // Verifica se há exceção de bloqueio para este dia const hasBlockException = exceptions.some( (exc) => exc.date === dateStr && exc.kind === "bloqueio" ); // Verifica se médico trabalha neste dia da semana const hasAvailability = availabilities.some((a) => a.weekday === dayOfWeek); // Verifica se há slots disponíveis (baseado na verificação assíncrona) const available = availableSlots[dateStr] === true; return { date, available, hasAvailability, hasBlockException, isPast, }; }; const getDayClasses = (status: DayStatus, isSelected: boolean): string => { const base = "w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors"; if (isSelected) { return `${base} bg-blue-600 text-white ring-2 ring-blue-400`; } if (status.isPast) { return `${base} bg-gray-100 text-gray-400 cursor-not-allowed`; } if (status.hasBlockException) { return `${base} bg-red-100 text-red-700 cursor-not-allowed`; } if (status.available) { return `${base} bg-blue-100 text-blue-700 hover:bg-blue-200 cursor-pointer`; } if (status.hasAvailability) { return `${base} bg-gray-50 text-gray-600 hover:bg-gray-100 cursor-pointer`; } return `${base} bg-white text-gray-400 cursor-not-allowed`; }; const handlePrevMonth = () => { setCurrentMonth(subMonths(currentMonth, 1)); }; const handleNextMonth = () => { setCurrentMonth(addMonths(currentMonth, 1)); }; const handleDayClick = (date: Date, status: DayStatus) => { if (status.isPast || status.hasBlockException) return; if (!status.hasAvailability && !status.available) return; const dateStr = format(date, "yyyy-MM-dd"); onSelectDate(dateStr); }; const renderCalendar = () => { const start = startOfMonth(currentMonth); const end = endOfMonth(currentMonth); const days = eachDayOfInterval({ start, end }); // Preencher dias do início (para alinhar o primeiro dia da semana) const startDayOfWeek = getDay(start); const emptyDays = Array(startDayOfWeek).fill(null); const allDays = [...emptyDays, ...days]; return (
{/* Cabeçalho dos dias da semana */} {["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
{day}
))} {/* Dias do mês */} {allDays.map((day, index) => { if (!day) { return
; } const status = getDayStatus(day); const isSelected = selectedDate === format(day, "yyyy-MM-dd"); const classes = getDayClasses(status, isSelected); return (
); })}
); }; return (
{/* Navegação do mês */}

{format(currentMonth, "MMMM yyyy", { locale: ptBR })}

{loading ? (
) : ( <> {renderCalendar()} {/* Legenda */}
Disponível
Bloqueado
Data passada
Sem horários
)}
); }