diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index a498ccc..08a4e16 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -41,8 +41,8 @@ import { export default function ScheduleForm() { // Estado do usuário e role - const [role, setRole] = useState("paciente"); - const [userId, setUserId] = useState(null); + const [role, setRole] = useState("paciente") + const [userId, setUserId] = useState(null) // Listas e seleções const [patients, setPatients] = useState([]); @@ -61,227 +61,186 @@ export default function ScheduleForm() { const [loadingSlots, setLoadingSlots] = useState(false); // Outras configs - const [tipoConsulta] = useState("presencial"); - const [duracao] = useState("30"); - const [disponibilidades, setDisponibilidades] = useState([]); - const [availabilityCounts, setAvailabilityCounts] = useState< - Record - >({}); - const [tooltip, setTooltip] = useState<{ - x: number; - y: number; - text: string; - } | null>(null); - const calendarRef = useRef(null); + const [tipoConsulta] = useState("presencial") + const [duracao] = useState("30") + const [disponibilidades, setDisponibilidades] = useState([]) + const [availabilityCounts, setAvailabilityCounts] = useState>({}) + const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string } | null>(null) + const calendarRef = useRef(null) // Funções auxiliares const getWeekdayNumber = (weekday: string) => - [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday", - ].indexOf(weekday.toLowerCase()) + 1; + ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].indexOf(weekday.toLowerCase()) + 1 const getBrazilDate = (date: Date) => - new Date( - Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0) - ); + new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)) // 🔹 Buscar dados do usuário e role useEffect(() => { - (async () => { + ;(async () => { try { - const me = await usersService.getMe(); - const currentRole = me?.roles?.[0] || "paciente"; - setRole(currentRole); - setUserId(me?.user?.id || null); + const me = await usersService.getMe() + const currentRole = me?.roles?.[0] || "paciente" + setRole(currentRole) + setUserId(me?.user?.id || null) if (["secretaria", "gestor", "admin"].includes(currentRole)) { - const pats = await patientsService.list(); - setPatients(pats || []); + const pats = await patientsService.list() + setPatients(pats || []) } } catch (err) { - console.error("Erro ao carregar usuário:", err); + console.error("Erro ao carregar usuário:", err) } - })(); - }, []); + })() + }, []) // 🔹 Buscar médicos const fetchDoctors = useCallback(async () => { - setLoadingDoctors(true); + setLoadingDoctors(true) try { - const data = await doctorsService.list(); - setDoctors(data || []); + const data = await doctorsService.list() + setDoctors(data || []) } catch (err) { - console.error("Erro ao buscar médicos:", err); - toast({ - title: "Erro", - description: "Não foi possível carregar médicos.", - }); + console.error("Erro ao buscar médicos:", err) + toast({ title: "Erro", description: "Não foi possível carregar médicos." }) } finally { - setLoadingDoctors(false); + setLoadingDoctors(false) } - }, []); + }, []) useEffect(() => { - fetchDoctors(); - }, [fetchDoctors]); + fetchDoctors() + }, [fetchDoctors]) // 🔹 Buscar disponibilidades const loadDoctorDisponibilidades = useCallback(async (doctorId?: string) => { - if (!doctorId) return; + if (!doctorId) return try { - const disp = await AvailabilityService.listById(doctorId); - setDisponibilidades(disp || []); - await computeAvailabilityCountsPreview(doctorId, disp || []); + const disp = await AvailabilityService.listById(doctorId) + setDisponibilidades(disp || []) + await computeAvailabilityCountsPreview(doctorId, disp || []) } catch (err) { - console.error("Erro ao buscar disponibilidades:", err); - setDisponibilidades([]); + console.error("Erro ao buscar disponibilidades:", err) + setDisponibilidades([]) } - }, []); + }, []) const computeAvailabilityCountsPreview = async ( doctorId: string, dispList: any[] ) => { try { - const today = new Date(); - const start = format(today, "yyyy-MM-dd"); - const endDate = addDays(today, 90); - const end = format(endDate, "yyyy-MM-dd"); + const today = new Date() + const start = format(today, "yyyy-MM-dd") + const endDate = addDays(today, 90) + const end = format(endDate, "yyyy-MM-dd") const appointments = await appointmentsService.search_appointment( - `doctor_id=eq.${doctorId}&scheduled_at=gte.${start}T00:00:00Z&scheduled_at=lt.${end}T23:59:59Z` - ); + `doctor_id=eq.${doctorId}&scheduled_at=gte.${start}T00:00:00Z&scheduled_at=lt.${end}T23:59:59Z`, + ) - const apptsByDate: Record = {}; - (appointments || []).forEach((a: any) => { - const d = String(a.scheduled_at).split("T")[0]; - apptsByDate[d] = (apptsByDate[d] || 0) + 1; - }); + 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 = {}; + 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(); - const dailyDisp = dispList.filter( - (p) => getWeekdayNumber(p.weekday) === dayOfWeek - ); + const d = addDays(today, i) + const key = format(d, "yyyy-MM-dd") + const dayOfWeek = d.getDay() === 0 ? 7 : d.getDay() + const dailyDisp = dispList.filter((p) => getWeekdayNumber(p.weekday) === dayOfWeek) if (dailyDisp.length === 0) { - counts[key] = 0; - continue; + counts[key] = 0 + continue } - let possible = 0; + 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; - if (endMin >= startMin) - possible += Math.floor((endMin - startMin) / slot) + 1; - }); - const occupied = apptsByDate[key] || 0; - counts[key] = Math.max(0, possible - occupied); + 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 + if (endMin >= startMin) possible += Math.floor((endMin - startMin) / slot) + 1 + }) + const occupied = apptsByDate[key] || 0 + counts[key] = Math.max(0, possible - occupied) } - setAvailabilityCounts(counts); + setAvailabilityCounts(counts) } catch (err) { - console.error("Erro ao calcular contagens:", err); - setAvailabilityCounts({}); + console.error("Erro ao calcular contagens:", err) + setAvailabilityCounts({}) } - }; + } // 🔹 Quando médico muda useEffect(() => { if (selectedDoctor) { - loadDoctorDisponibilidades(selectedDoctor); + loadDoctorDisponibilidades(selectedDoctor) } else { - setDisponibilidades([]); - setAvailabilityCounts({}); + setDisponibilidades([]) + setAvailabilityCounts({}) } - setSelectedDate(""); - setSelectedTime(""); - setAvailableTimes([]); - }, [selectedDoctor, loadDoctorDisponibilidades]); + setSelectedDate("") + setSelectedTime("") + setAvailableTimes([]) + }, [selectedDoctor, loadDoctorDisponibilidades]) // 🔹 Buscar horários disponíveis - const fetchAvailableSlots = useCallback( - async (doctorId: string, date: string) => { - if (!doctorId || !date) return; - setLoadingSlots(true); - setAvailableTimes([]); - try { - const disponibilidades = 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(); - const diaAPI = diaJS === 0 ? 7 : diaJS; - const disponibilidadeDia = disponibilidades.find( - (d: any) => getWeekdayNumber(d.weekday) === diaAPI - ); - if (!disponibilidadeDia) { - toast({ - title: "Nenhuma disponibilidade", - description: "Nenhum horário para este dia.", - }); - return setAvailableTimes([]); - } - 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)); - setAvailableTimes(livres); - } catch (err) { - console.error(err); - toast({ title: "Erro", description: "Falha ao carregar horários." }); - } finally { - setLoadingSlots(false); + const fetchAvailableSlots = useCallback(async (doctorId: string, date: string) => { + if (!doctorId || !date) return + setLoadingSlots(true) + setAvailableTimes([]) + try { + const disponibilidades = 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() + const diaAPI = diaJS === 0 ? 7 : diaJS + const disponibilidadeDia = disponibilidades.find((d: any) => getWeekdayNumber(d.weekday) === diaAPI) + if (!disponibilidadeDia) { + toast({ title: "Nenhuma disponibilidade", description: "Nenhum horário para este dia." }) + return setAvailableTimes([]) } - }, - [] - ); + 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)) + setAvailableTimes(livres) + } catch (err) { + console.error(err) + toast({ title: "Erro", description: "Falha ao carregar horários." }) + } finally { + setLoadingSlots(false) + } + }, []) useEffect(() => { - if (selectedDoctor && selectedDate) - fetchAvailableSlots(selectedDoctor, selectedDate); - }, [selectedDoctor, selectedDate, fetchAvailableSlots]); + if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate) + }, [selectedDoctor, selectedDate, fetchAvailableSlots]) // 🔹 Submeter agendamento - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + e.preventDefault() - const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role); - const patientId = isSecretaryLike ? selectedPatient : userId; + const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role) + const patientId = isSecretaryLike ? selectedPatient : userId if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { - toast({ - title: "Campos obrigatórios", - description: "Preencha todos os campos.", - }); - return; + toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }) + return } try { @@ -292,7 +251,7 @@ export default function ScheduleForm() { duration_minutes: Number(duracao), notes, appointment_type: tipoConsulta, - }; + } await appointmentsService.create(body); @@ -309,44 +268,38 @@ export default function ScheduleForm() { try { if (isSecretaryLike) { - const patient = patients.find((p: any) => p.id === patientId); - const rawPhone = patient?.phone || patient?.phone_mobile || null; - if (rawPhone) phoneNumber = rawPhone; + const patient = patients.find((p: any) => p.id === patientId) + const rawPhone = patient?.phone || patient?.phone_mobile || null + if (rawPhone) phoneNumber = rawPhone } else { - const me = await usersService.getMe(); + const me = await usersService.getMe() const rawPhone = me?.profile?.phone || (typeof me?.profile === "object" && "phone_mobile" in me.profile ? (me.profile as any).phone_mobile : null) || - (typeof me === "object" && "user_metadata" in me - ? (me as any).user_metadata?.phone - : null) || - null; - if (rawPhone) phoneNumber = rawPhone; + (typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) || + null + if (rawPhone) phoneNumber = rawPhone } - // 🔹 Normaliza para formato internacional (+55) if (phoneNumber) { - phoneNumber = phoneNumber.replace(/\D/g, ""); - if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; - phoneNumber = `+${phoneNumber}`; + phoneNumber = phoneNumber.replace(/\D/g, "") + if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}` + phoneNumber = `+${phoneNumber}` } - console.log("📞 Telefone usado:", phoneNumber); + console.log("📞 Telefone usado:", phoneNumber) } catch (err) { - console.warn("⚠️ Não foi possível obter telefone do paciente:", err); + console.warn("⚠️ Não foi possível obter telefone do paciente:", err) } - // 💬 envia o SMS de confirmação - // 💬 Envia o SMS de lembrete (sem mostrar nada ao paciente) - // 💬 Envia o SMS de lembrete (somente loga no console, não mostra no sistema) try { const smsRes = await smsService.sendSms({ phone_number: phoneNumber, message: `Lembrete: sua consulta é em ${dateFormatted} às ${selectedTime} na Clínica MediConnect.`, patient_id: patientId, - }); + }) if (smsRes?.success) { console.log("✅ SMS enviado com sucesso:", smsRes.message_sid); @@ -371,187 +324,100 @@ export default function ScheduleForm() { // 🔹 Tooltip no calendário useEffect(() => { - const cont = calendarRef.current; - if (!cont) return; + const cont = calendarRef.current + if (!cont) return const onMove = (ev: MouseEvent) => { - const target = ev.target as HTMLElement | null; - const btn = target?.closest("button"); - if (!btn) return setTooltip(null); - const aria = btn.getAttribute("aria-label") || btn.textContent || ""; - const parsed = new Date(aria); - if (isNaN(parsed.getTime())) return setTooltip(null); - const key = format(getBrazilDate(parsed), "yyyy-MM-dd"); - const count = availabilityCounts[key] ?? 0; + const target = ev.target as HTMLElement | null + const btn = target?.closest("button") + if (!btn) return setTooltip(null) + const aria = btn.getAttribute("aria-label") || btn.textContent || "" + const parsed = new Date(aria) + if (isNaN(parsed.getTime())) return setTooltip(null) + 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); + }) + } + const onLeave = () => setTooltip(null) + cont.addEventListener("mousemove", onMove) + cont.addEventListener("mouseleave", onLeave) return () => { - cont.removeEventListener("mousemove", onMove); - cont.removeEventListener("mouseleave", onLeave); - }; - }, [availabilityCounts]); + cont.removeEventListener("mousemove", onMove) + cont.removeEventListener("mouseleave", onLeave) + } + }, [availabilityCounts]) return ( -
-

Agendar Consulta

+
+
+
+

Agendar Consulta

+

Selecione o médico, data e horário para sua consulta

+
- - - Dados da Consulta - - -
-
- {" "} - {/* Ajuste: maior espaçamento vertical geral */} - {/* Se secretária/gestor/admin → COMBOBOX de Paciente */} - {["secretaria", "gestor", "admin"].includes(role) && ( -
- {" "} - {/* Ajuste: gap entre Label e Input */} - - - - - - - - - - - Nenhum paciente encontrado. - - - {patients.map((patient) => ( - { - setSelectedPatient( - patient.id === selectedPatient - ? "" - : patient.id - ); - setOpenPatientCombobox(false); - }} - > - - {patient.full_name} - - ))} - - - - - +
+ {/* Coluna do Formulário */} + + + + Informações Básicas + + + {["secretaria", "gestor", "admin"].includes(role) && ( +
+ + +
+ )} + +
+ +
- )} - {/* COMBOBOX de Médico (Nova funcionalidade) */} -
- {" "} - {/* Ajuste: gap entre Label e Input */} - - - - - - - - - - Nenhum médico encontrado. - - {[...doctors] - .sort((a, b) => - String(a.full_name).localeCompare( - String(b.full_name) - ) - ) - .map((doctor) => ( - { - setSelectedDoctor( - doctor.id === selectedDoctor - ? "" - : doctor.id - ); - setOpenDoctorCombobox(false); - }} - > - -
- {doctor.full_name} - - {doctor.specialty} - -
-
- ))} -
-
-
-
-
-
-
- -
+ + + + + + Selecione a Data + + +
{ - if (!date) return; - const formatted = format( - new Date(date.getTime() + 12 * 60 * 60 * 1000), - "yyyy-MM-dd" - ); - setSelectedDate(formatted); + if (!date) return + const formatted = format(new Date(date.getTime() + 12 * 60 * 60 * 1000), "yyyy-MM-dd") + setSelectedDate(formatted) }} + className="rounded-md" />
-
-
- + + + + + + Observações (Opcional) + +