// UI/UX refresh: melhorias visuais e de acessibilidade sem alterar a lógica import React, { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { appointmentService, patientService } from "../../services/index"; import type { Appointment } from "../../services/appointments/types"; import { ChevronLeft, ChevronRight, X } from "lucide-react"; interface Props { doctorId: string; } interface CalendarDay { date: Date; dateStr: string; isCurrentMonth: boolean; isToday: boolean; appointments: Appointment[]; } const WEEKDAYS = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"]; const MONTHS = [ "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro", ]; const DoctorCalendar: React.FC = ({ doctorId }) => { const [currentDate, setCurrentDate] = useState(new Date()); const [appointments, setAppointments] = useState([]); const [loading, setLoading] = useState(false); const [selectedDay, setSelectedDay] = useState(null); const [patientsById, setPatientsById] = useState>({}); useEffect(() => { if (doctorId) { loadAppointments(); loadPatients(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [doctorId, currentDate]); async function loadAppointments() { setLoading(true); try { const appointments = await appointmentService.list(); // Filtrar apenas do médico selecionado const filtered = appointments.filter( (apt: Appointment) => apt.doctor_id === doctorId ); setAppointments(filtered); } catch (error) { console.error("Erro ao carregar agendamentos:", error); toast.error("Erro ao carregar agendamentos"); } finally { setLoading(false); } } async function loadPatients() { // Carrega pacientes para mapear nome pelo id (render amigável) try { const patients = await patientService.list(); const map: Record = {}; for (const p of patients) { if (p?.id) { map[p.id] = p.full_name || p.email || p.cpf || p.id; } } setPatientsById(map); } catch { // silencioso; não bloqueia calendário } } function getPatientName(id?: string) { if (!id) return ""; return patientsById[id] || id; } function getCalendarDays(): CalendarDay[] { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); // Primeiro dia do mês const firstDay = new Date(year, month, 1); // Último dia do mês const lastDay = new Date(year, month + 1, 0); // Dia da semana do primeiro dia (0 = domingo) const startingDayOfWeek = firstDay.getDay(); const days: CalendarDay[] = []; const today = new Date(); today.setHours(0, 0, 0, 0); // Adicionar dias do mês anterior const prevMonthLastDay = new Date(year, month, 0); for (let i = startingDayOfWeek - 1; i >= 0; i--) { const date = new Date(year, month - 1, prevMonthLastDay.getDate() - i); const dateStr = formatDateISO(date); days.push({ date, dateStr, isCurrentMonth: false, isToday: false, appointments: getAppointmentsForDate(dateStr), }); } // Adicionar dias do mês atual for (let day = 1; day <= lastDay.getDate(); day++) { const date = new Date(year, month, day); const dateStr = formatDateISO(date); const isToday = date.getTime() === today.getTime(); days.push({ date, dateStr, isCurrentMonth: true, isToday, appointments: getAppointmentsForDate(dateStr), }); } // Adicionar dias do próximo mês para completar a grade const remainingDays = 42 - days.length; // 6 semanas x 7 dias for (let day = 1; day <= remainingDays; day++) { const date = new Date(year, month + 1, day); const dateStr = formatDateISO(date); days.push({ date, dateStr, isCurrentMonth: false, isToday: false, appointments: getAppointmentsForDate(dateStr), }); } return days; } function formatDateISO(date: Date): string { return date.toISOString().split("T")[0]; } function getAppointmentsForDate(dateStr: string): Appointment[] { return appointments.filter((apt) => { if (!apt.scheduled_at) return false; const aptDate = apt.scheduled_at.split("T")[0]; return aptDate === dateStr; }); } function previousMonth() { setCurrentDate( new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1) ); } function nextMonth() { setCurrentDate( new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1) ); } function goToToday() { setCurrentDate(new Date()); } function getStatusColor(status?: string): string { switch (status) { case "confirmed": return "bg-blue-500"; case "completed": return "bg-green-500"; case "cancelled": return "bg-red-500"; case "no_show": return "bg-gray-500"; case "checked_in": return "bg-purple-500"; case "in_progress": return "bg-yellow-500"; default: return "bg-orange-500"; // requested } } function getStatusLabel(status?: string): string { const labels: Record = { requested: "Solicitado", confirmed: "Confirmado", checked_in: "Check-in", in_progress: "Em andamento", completed: "Concluído", cancelled: "Cancelado", no_show: "Faltou", }; return labels[status || "requested"] || status || "Solicitado"; } const calendarDays = getCalendarDays(); return (
{/* Cabeçalho modernizado: melhor contraste, foco e navegação */}

Calendário de Consultas

{MONTHS[currentDate.getMonth()]} {currentDate.getFullYear()}
{loading ? (
) : ( <> {/* Cabeçalhos dos dias da semana */}
{WEEKDAYS.map((day) => (
{day}
))}
{/* Grid do calendário com células interativas acessíveis */}
{calendarDays.map((day, index) => (
0 ? "cursor-pointer hover:bg-blue-50" : "" }`} onClick={() => day.appointments.length > 0 && setSelectedDay(day) } > {/* Número do dia com destaque para hoje */}
{day.date.getDate()}
{/* Chips de horários com cores por status */}
{day.appointments.slice(0, 3).map((apt, idx) => (
{apt.scheduled_at?.slice(11, 16)}
))} {day.appointments.length > 3 && (
+{day.appointments.length - 3} mais
)}
))}
)} {/* Modal de detalhes do dia - melhorado com acessibilidade e botão de fechar */} {selectedDay && (
setSelectedDay(null)} role="dialog" aria-modal="true" aria-label="Consultas do dia selecionado" >
e.stopPropagation()} >

Consultas de{" "} {selectedDay.date.toLocaleDateString("pt-BR", { weekday: "long", day: "numeric", month: "long", year: "numeric", })}

{selectedDay.appointments.length === 0 ? (

Nenhuma consulta agendada para este dia.

) : ( selectedDay.appointments.map((apt) => (
{apt.scheduled_at?.slice(11, 16)} {getStatusLabel(apt.status)}
Paciente:{" "} {getPatientName(apt.patient_id)}
{apt.appointment_type && (
Tipo:{" "} {apt.appointment_type === "presencial" ? "Presencial" : "Telemedicina"}
)} {apt.chief_complaint && (
Queixa:{" "} {apt.chief_complaint}
)}
)) )}
)}
); }; export default DoctorCalendar;