From 7ab488b34648b26087bec08876ba2aee73423d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Deir=C3=B3=20Rodrigues?= Date: Thu, 27 Nov 2025 04:27:12 -0300 Subject: [PATCH] =?UTF-8?q?altera=C3=A7=C3=B5es=20est=C3=A9ticas=20p=C3=A1?= =?UTF-8?q?gina=20de=20agendamento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/schedule/schedule-form.tsx | 819 +++++++++++++------------- 1 file changed, 420 insertions(+), 399 deletions(-) diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index 89ffb05..4747258 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -1,508 +1,527 @@ -"use client"; - -import { useState, useEffect, useCallback, useRef } from "react"; -import { usersService } from "@/services/usersApi.mjs"; -import { patientsService } from "@/services/patientsApi.mjs"; -import { doctorsService } from "@/services/doctorsApi.mjs"; -import { appointmentsService } from "@/services/appointmentsApi.mjs"; -import { AvailabilityService } from "@/services/availabilityApi.mjs"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; -import { format, addDays } from "date-fns"; -import { User, StickyNote, Calendar } from "lucide-react"; -import {smsService } from "@/services/Sms.mjs" -import { toast } from "@/hooks/use-toast"; +"use client" +import { useState, useEffect, useCallback, useRef } from "react" +import type React from "react" +import { usersService } from "@/services/usersApi.mjs" +import { patientsService } from "@/services/patientsApi.mjs" +import { doctorsService } from "@/services/doctorsApi.mjs" +import { appointmentsService } from "@/services/appointmentsApi.mjs" +import { AvailabilityService } from "@/services/availabilityApi.mjs" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { Calendar as CalendarShadcn } from "@/components/ui/calendar" +import { format, addDays } from "date-fns" +import { User, StickyNote } from "lucide-react" +import { smsService } from "@/services/Sms.mjs" +import { toast } from "@/hooks/use-toast" 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([]); - const [selectedPatient, setSelectedPatient] = useState(""); - const [doctors, setDoctors] = useState([]); - const [selectedDoctor, setSelectedDoctor] = useState(""); - const [selectedDate, setSelectedDate] = useState(""); - const [selectedTime, setSelectedTime] = useState(""); - const [notes, setNotes] = useState(""); - const [availableTimes, setAvailableTimes] = useState([]); - const [loadingDoctors, setLoadingDoctors] = useState(true); - const [loadingSlots, setLoadingSlots] = useState(false); + const [patients, setPatients] = useState([]) + const [selectedPatient, setSelectedPatient] = useState("") + const [doctors, setDoctors] = useState([]) + const [selectedDoctor, setSelectedDoctor] = useState("") + const [selectedDate, setSelectedDate] = useState("") + const [selectedTime, setSelectedTime] = useState("") + const [notes, setNotes] = useState("") + const [availableTimes, setAvailableTimes] = useState([]) + const [loadingDoctors, setLoadingDoctors] = useState(true) + const [loadingSlots, setLoadingSlots] = useState(false) // Outras configs - 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); + 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([]); + if (!doctorId || !date) return + setLoadingSlots(true) + setAvailableTimes([]) try { - const disponibilidades = await AvailabilityService.listById(doctorId); + 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 - ); + `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([]); + 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); + 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); + 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); + 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." }); + console.error(err) + toast({ title: "Erro", description: "Falha ao carregar horários." }) } finally { - setLoadingSlots(false); + setLoadingSlots(false) } - }, []); + }, []) useEffect(() => { - if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate); - }, [selectedDoctor, selectedDate, fetchAvailableSlots]); + if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate) + }, [selectedDoctor, selectedDate, fetchAvailableSlots]) // 🔹 Submeter agendamento - // 🔹 Submeter agendamento - // 🔹 Submeter agendamento -// 🔹 Submeter agendamento -// 🔹 Submeter agendamento -const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (e: React.FormEvent) => { + 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; + if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { + toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }) + return + } + + try { + const body = { + doctor_id: selectedDoctor, + patient_id: patientId, + scheduled_at: `${selectedDate}T${selectedTime}:00`, + duration_minutes: Number(duracao), + notes, + appointment_type: tipoConsulta, + } + + await appointmentsService.create(body) + + const dateFormatted = selectedDate.split("-").reverse().join("/") + + toast({ + title: "Consulta agendada!", + description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${ + doctors.find((d) => d.id === selectedDoctor)?.full_name || "" + }.`, + }) + + let phoneNumber = "+5511999999999" + + try { + if (isSecretaryLike) { + 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 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 + } + + if (phoneNumber) { + phoneNumber = phoneNumber.replace(/\D/g, "") + if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}` + phoneNumber = `+${phoneNumber}` + } + + console.log("📞 Telefone usado:", phoneNumber) + } catch (err) { + console.warn("⚠️ Não foi possível obter telefone do paciente:", err) + } + + 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) + } else { + console.warn("⚠️ Falha no envio do SMS:", smsRes) + } + } catch (smsErr) { + console.error("❌ Erro ao enviar SMS:", smsErr) + } + + setSelectedDoctor("") + setSelectedDate("") + setSelectedTime("") + setNotes("") + setSelectedPatient("") + } catch (err) { + console.error("❌ Erro ao agendar consulta:", err) + toast({ title: "Erro", description: "Falha ao agendar consulta." }) + } } - try { - const body = { - doctor_id: selectedDoctor, - patient_id: patientId, - scheduled_at: `${selectedDate}T${selectedTime}:00`, - duration_minutes: Number(duracao), - notes, - appointment_type: tipoConsulta, - }; - - // ✅ mantém o fluxo original de criação (funcional) - await appointmentsService.create(body); - - const dateFormatted = selectedDate.split("-").reverse().join("/"); - - toast({ - title: "Consulta agendada!", - description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${ - doctors.find((d) => d.id === selectedDoctor)?.full_name || "" - }.`, - }); - -let phoneNumber = "+5511999999999"; // fallback - -try { - if (isSecretaryLike) { - // Secretária/admin → telefone do paciente selecionado - const patient = patients.find((p: any) => p.id === patientId); - - // Pacientes criados no sistema podem ter phone ou phone_mobile - const rawPhone = patient?.phone || patient?.phone_mobile || null; - - if (rawPhone) phoneNumber = rawPhone; - } else { - // Paciente → telefone vem do perfil do próprio usuário logado - 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; - } - - // 🔹 Normaliza para formato internacional (+55) - if (phoneNumber) { - phoneNumber = phoneNumber.replace(/\D/g, ""); - if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; - phoneNumber = `+${phoneNumber}`; - } - - console.log("📞 Telefone usado:", phoneNumber); -} catch (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); - } else { - console.warn("⚠️ Falha no envio do SMS:", smsRes); - } -} catch (smsErr) { - console.error("❌ Erro ao enviar SMS:", smsErr); -} - - - - - // 🧹 limpa os campos - setSelectedDoctor(""); - setSelectedDate(""); - setSelectedTime(""); - setNotes(""); - setSelectedPatient(""); - } catch (err) { - console.error("❌ Erro ao agendar consulta:", err); - toast({ title: "Erro", description: "Falha ao agendar consulta." }); - } -}; - - - - - // 🔹 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 - - -
-
- {/* Se secretária/gestor/admin → mostrar campo Paciente */} - {["secretaria", "gestor", "admin"].includes(role) && ( -
- - + + + + + {patients.map((p) => ( + + {p.full_name} + + ))} + + +
+ )} + +
+ +
- )} + + -
- - -
- -
- -
+ + + 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) + +