From 6ebbfae4f2515c7592e43efacd7f960f38d05e53 Mon Sep 17 00:00:00 2001 From: guisilvagomes Date: Mon, 13 Oct 2025 11:24:25 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Sistema=20completo=20de=20agendamento?= =?UTF-8?q?=20com=20disponibilidade=20de=20m=C3=A9dicos=20e=20reserva=20de?= =?UTF-8?q?=20consultas=20para=20pacientes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MEDICONNECT 2.zip | 3 - .../src/components/AgendamentoConsulta.tsx | 821 ++++++++++++++++++ .../src/components/DisponibilidadeMedico.tsx | 698 +++++++++++++++ .../src/pages/AcompanhamentoPaciente.tsx | 89 +- MEDICONNECT 2/src/pages/PainelMedico.tsx | 15 +- 5 files changed, 1560 insertions(+), 66 deletions(-) delete mode 100644 MEDICONNECT 2.zip create mode 100644 MEDICONNECT 2/src/components/AgendamentoConsulta.tsx create mode 100644 MEDICONNECT 2/src/components/DisponibilidadeMedico.tsx diff --git a/MEDICONNECT 2.zip b/MEDICONNECT 2.zip deleted file mode 100644 index ae842b600..000000000 --- a/MEDICONNECT 2.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:50fbfa0b9343001fbf0b803364ec8a0fe48bcc62537eb59a31abc74674d80d0a -size 807015226 diff --git a/MEDICONNECT 2/src/components/AgendamentoConsulta.tsx b/MEDICONNECT 2/src/components/AgendamentoConsulta.tsx new file mode 100644 index 000000000..be25867a1 --- /dev/null +++ b/MEDICONNECT 2/src/components/AgendamentoConsulta.tsx @@ -0,0 +1,821 @@ +import { useState, useEffect, useCallback } from "react"; +import { + format, + addMonths, + subMonths, + startOfMonth, + endOfMonth, + eachDayOfInterval, + isSameMonth, + isSameDay, + isToday, + parseISO, + isBefore, + startOfDay, +} from "date-fns"; +import { ptBR } from "date-fns/locale"; +import { + Search, + Star, + MapPin, + Video, + Clock, + CalendarDays, + ChevronLeft, + ChevronRight, + Stethoscope, + AlertCircle, + CheckCircle2, +} from "lucide-react"; +import { medicoService } from "../services/medicoService"; +import { availabilityService } from "../services/availabilityService"; +import { exceptionService } from "../services/exceptionService"; +import { consultaService } from "../services/consultaService"; + +interface Medico { + id: string; + nome: string; + especialidade: string; + crm: string; + foto?: string; + email?: string; + telefone?: string; + valorConsulta?: number; +} + +interface TimeSlot { + inicio: string; + fim: string; + ativo: boolean; +} + +interface DaySchedule { + ativo: boolean; + horarios: TimeSlot[]; +} + +interface Availability { + domingo: DaySchedule; + segunda: DaySchedule; + terca: DaySchedule; + quarta: DaySchedule; + quinta: DaySchedule; + sexta: DaySchedule; + sabado: DaySchedule; +} + +interface Exception { + id: string; + data: string; + motivo?: string; +} + +const dayOfWeekMap: { [key: number]: keyof Availability } = { + 0: "domingo", + 1: "segunda", + 2: "terca", + 3: "quarta", + 4: "quinta", + 5: "sexta", + 6: "sabado", +}; + +export default function AgendamentoConsulta() { + const [medicos, setMedicos] = useState([]); + const [filteredMedicos, setFilteredMedicos] = useState([]); + const [selectedMedico, setSelectedMedico] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedSpecialty, setSelectedSpecialty] = useState("all"); + const [loading, setLoading] = useState(true); + + // Calendar and scheduling states + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(undefined); + const [availability, setAvailability] = useState(null); + const [exceptions, setExceptions] = useState([]); + const [availableSlots, setAvailableSlots] = useState([]); + const [selectedTime, setSelectedTime] = useState(""); + const [appointmentType, setAppointmentType] = useState< + "presencial" | "online" + >("presencial"); + const [motivo, setMotivo] = useState(""); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [bookingSuccess, setBookingSuccess] = useState(false); + const [bookingError, setBookingError] = useState(""); + + // Load doctors on mount + useEffect(() => { + loadMedicos(); + }, []); + + const loadMedicos = async () => { + try { + setLoading(true); + const data = await medicoService.listarMedicos(); + setMedicos(data); + setFilteredMedicos(data); + } catch (error) { + console.error("Erro ao carregar médicos:", error); + } finally { + setLoading(false); + } + }; + + // Filter doctors based on search and specialty + useEffect(() => { + let filtered = medicos; + + if (searchTerm) { + filtered = filtered.filter( + (medico) => + medico.nome.toLowerCase().includes(searchTerm.toLowerCase()) || + medico.especialidade.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + if (selectedSpecialty !== "all") { + filtered = filtered.filter( + (medico) => medico.especialidade === selectedSpecialty + ); + } + + setFilteredMedicos(filtered); + }, [searchTerm, selectedSpecialty, medicos]); + + // Get unique specialties + const specialties = Array.from(new Set(medicos.map((m) => m.especialidade))); + + // Load availability and exceptions when doctor is selected + useEffect(() => { + if (selectedMedico) { + loadDoctorAvailability(); + loadDoctorExceptions(); + } + }, [selectedMedico]); + + const loadDoctorAvailability = async () => { + if (!selectedMedico) return; + try { + const data = await availabilityService.getAvailability(selectedMedico.id); + if (data && data.length > 0) { + const avail = data[0]; + setAvailability({ + domingo: avail.domingo || { ativo: false, horarios: [] }, + segunda: avail.segunda || { ativo: false, horarios: [] }, + terca: avail.terca || { ativo: false, horarios: [] }, + quarta: avail.quarta || { ativo: false, horarios: [] }, + quinta: avail.quinta || { ativo: false, horarios: [] }, + sexta: avail.sexta || { ativo: false, horarios: [] }, + sabado: avail.sabado || { ativo: false, horarios: [] }, + }); + } else { + setAvailability(null); + } + } catch (error) { + console.error("Erro ao carregar disponibilidade:", error); + setAvailability(null); + } + }; + + const loadDoctorExceptions = async () => { + if (!selectedMedico) return; + try { + const data = await exceptionService.listExceptions(selectedMedico.id); + setExceptions(data || []); + } catch (error) { + console.error("Erro ao carregar exceções:", error); + setExceptions([]); + } + }; + + // Calculate available slots when date is selected + useEffect(() => { + if (selectedDate && availability && selectedMedico) { + calculateAvailableSlots(); + } else { + setAvailableSlots([]); + } + }, [selectedDate, availability, exceptions]); + + const calculateAvailableSlots = () => { + if (!selectedDate || !availability) return; + + // Check if date is an exception (blocked) + const dateStr = format(selectedDate, "yyyy-MM-dd"); + const isBlocked = exceptions.some((exc) => exc.data === dateStr); + + if (isBlocked) { + setAvailableSlots([]); + return; + } + + // Get day of week schedule + const dayOfWeek = selectedDate.getDay(); + const dayKey = dayOfWeekMap[dayOfWeek]; + const daySchedule = availability[dayKey]; + + if (!daySchedule || !daySchedule.ativo) { + setAvailableSlots([]); + return; + } + + // Extract active time slots + const slots = daySchedule.horarios + .filter((slot) => slot.ativo) + .map((slot) => slot.inicio); + + setAvailableSlots(slots); + }; + + const isDateBlocked = (date: Date): boolean => { + const dateStr = format(date, "yyyy-MM-dd"); + return exceptions.some((exc) => exc.data === dateStr); + }; + + const isDateAvailable = (date: Date): boolean => { + if (!availability) return false; + + // Check if in the past + if (isBefore(date, startOfDay(new Date()))) return false; + + // Check if blocked + if (isDateBlocked(date)) return false; + + // Check if day has available schedule + const dayOfWeek = date.getDay(); + const dayKey = dayOfWeekMap[dayOfWeek]; + const daySchedule = availability[dayKey]; + + return ( + daySchedule?.ativo && daySchedule.horarios.some((slot) => slot.ativo) + ); + }; + + // Calendar generation + const generateCalendarDays = () => { + const start = startOfMonth(currentMonth); + const end = endOfMonth(currentMonth); + const days = eachDayOfInterval({ start, end }); + + // Add padding days from previous month + const startDay = start.getDay(); + const prevMonthDays = []; + for (let i = startDay - 1; i >= 0; i--) { + const day = new Date(start); + day.setDate(day.getDate() - (i + 1)); + prevMonthDays.push(day); + } + + return [...prevMonthDays, ...days]; + }; + + const handlePrevMonth = () => { + setCurrentMonth(subMonths(currentMonth, 1)); + }; + + const handleNextMonth = () => { + setCurrentMonth(addMonths(currentMonth, 1)); + }; + + const handleSelectDoctor = (medico: Medico) => { + setSelectedMedico(medico); + setSelectedDate(undefined); + setSelectedTime(""); + setMotivo(""); + setBookingSuccess(false); + setBookingError(""); + }; + + const handleBookAppointment = () => { + if (selectedMedico && selectedDate && selectedTime && motivo) { + setShowConfirmDialog(true); + } + }; + + const confirmAppointment = async () => { + if (!selectedMedico || !selectedDate || !selectedTime) return; + + try { + setBookingError(""); + + // Get current user from localStorage + const userStr = localStorage.getItem("user"); + if (!userStr) { + setBookingError("Usuário não autenticado"); + return; + } + + const user = JSON.parse(userStr); + + // Create date-time string + const dataHora = `${format( + selectedDate, + "yyyy-MM-dd" + )}T${selectedTime}:00`; + + // Book appointment via API + await consultaService.criarConsulta({ + medicoId: selectedMedico.id, + pacienteId: user.id, + dataHora, + tipoConsulta: appointmentType, + motivoConsulta: motivo, + status: "agendada", + }); + + setBookingSuccess(true); + setShowConfirmDialog(false); + + // Reset form after 3 seconds + setTimeout(() => { + setSelectedMedico(null); + setSelectedDate(undefined); + setSelectedTime(""); + setMotivo(""); + setBookingSuccess(false); + }, 3000); + } catch (error: any) { + console.error("Erro ao agendar consulta:", error); + setBookingError( + error.message || "Erro ao agendar consulta. Tente novamente." + ); + setShowConfirmDialog(false); + } + }; + + const calendarDays = generateCalendarDays(); + + return ( +
+ {/* Success Message */} + {bookingSuccess && ( +
+ +
+

+ Consulta agendada com sucesso! +

+

+ Você receberá uma confirmação por e-mail em breve. +

+
+
+ )} + + {/* Error Message */} + {bookingError && ( +
+ +

{bookingError}

+
+ )} + +
+

+ Agendar Consulta +

+

+ Escolha um médico e horário disponível +

+
+ + {/* Search and Filters */} +
+

+ Buscar Médicos +

+
+
+ +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" + /> +
+
+
+ + +
+
+
+ + {/* Doctors List */} + {loading ? ( +
+
+

+ Carregando médicos... +

+
+ ) : filteredMedicos.length === 0 ? ( +
+ +

+ Nenhum médico encontrado +

+
+ ) : ( +
+ {filteredMedicos.map((medico) => ( +
+
+
+ {medico.nome + .split(" ") + .map((n) => n[0]) + .join("") + .substring(0, 2)} +
+
+
+

+ {medico.nome} +

+

+ {medico.especialidade} +

+

+ CRM: {medico.crm} +

+
+
+ + {medico.valorConsulta + ? `R$ ${medico.valorConsulta.toFixed(2)}` + : "Consultar valor"} + + +
+
+
+
+ ))} +
+ )} + + {/* Appointment Details */} + {selectedMedico && ( +
+
+

+ Detalhes do Agendamento +

+

+ Consulta com {selectedMedico.nome} -{" "} + {selectedMedico.especialidade} +

+
+ + {/* Appointment Type */} +
+ + +
+ +
+ {/* Calendar */} +
+
+ +
+ {/* Month/Year Navigation */} +
+ + + {format(currentMonth, "MMMM yyyy", { locale: ptBR })} + + +
+ + {/* Calendar Grid */} +
+ {/* Days of week header */} +
+ {["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map( + (day) => ( +
+ {day} +
+ ) + )} +
+ + {/* Calendar days */} +
+ {calendarDays.map((day, index) => { + const isCurrentMonth = isSameMonth(day, currentMonth); + const isSelected = + selectedDate && isSameDay(day, selectedDate); + const isTodayDate = isToday(day); + const isAvailable = + isCurrentMonth && isDateAvailable(day); + const isBlocked = isCurrentMonth && isDateBlocked(day); + const isPast = isBefore(day, startOfDay(new Date())); + + return ( + + ); + })} +
+
+ + {/* Legend */} +
+

🟢 Datas disponíveis

+

🔴 Datas bloqueadas

+
+
+
+
+ + {/* Time Slots and Details */} +
+
+ + {selectedDate ? ( +

+ {format(selectedDate, "EEEE, d 'de' MMMM 'de' yyyy", { + locale: ptBR, + })} +

+ ) : ( +

+ Selecione uma data +

+ )} +
+ + {selectedDate && availableSlots.length > 0 ? ( +
+ {availableSlots.map((slot) => ( + + ))} +
+ ) : selectedDate ? ( +
+

+ Nenhum horário disponível para esta data +

+
+ ) : ( +
+

+ Selecione uma data para ver os horários +

+
+ )} + + {/* Reason */} +
+ +