From aa409fde0f4b1320f1c8e8a122da7c12e57f9092 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Tue, 2 Dec 2025 22:46:09 -0300 Subject: [PATCH] Ajuste de reponsividade, Barra de pesquisa agendar consultas --- components/schedule/schedule-form.tsx | 818 ++++++++++++++------------ 1 file changed, 426 insertions(+), 392 deletions(-) diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index 93b24f7..2baf243 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -19,12 +19,12 @@ import { import { Textarea } from "@/components/ui/textarea"; import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; import { format, addDays } from "date-fns"; -import { User, StickyNote, Check, ChevronsUpDown } from "lucide-react"; +import { User, StickyNote, CalendarDays, Stethoscope, Check, ChevronsUpDown } from "lucide-react"; import { smsService } from "@/services/Sms.mjs"; import { toast } from "@/hooks/use-toast"; import { cn } from "@/lib/utils"; -// Componentes do Combobox (Barra de Pesquisa) +// --- Importações do Combobox --- import { Command, CommandEmpty, @@ -40,19 +40,21 @@ import { } from "@/components/ui/popover"; export default function ScheduleForm() { - // Estado do usuário e role - const [role, setRole] = useState("paciente") - const [userId, setUserId] = useState(null) + // --- ESTADOS --- + const [role, setRole] = useState("paciente"); + const [userId, setUserId] = useState(null); - // Listas e seleções + // Estados de Paciente const [patients, setPatients] = useState([]); const [selectedPatient, setSelectedPatient] = useState(""); const [openPatientCombobox, setOpenPatientCombobox] = useState(false); + // Estados de Médico const [doctors, setDoctors] = useState([]); const [selectedDoctor, setSelectedDoctor] = useState(""); - const [openDoctorCombobox, setOpenDoctorCombobox] = useState(false); // Novo estado para médico + const [openDoctorCombobox, setOpenDoctorCombobox] = useState(false); + // Estados de Agendamento const [selectedDate, setSelectedDate] = useState(""); const [selectedTime, setSelectedTime] = useState(""); const [notes, setNotes] = useState(""); @@ -60,187 +62,180 @@ export default function ScheduleForm() { 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) + // Configurações + 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 + // --- HELPER FUNCTIONS --- 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 + // --- EFFECTS --- 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[] - ) => { + 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 + // --- SUBMIT --- const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role) - const patientId = isSecretaryLike ? selectedPatient : userId + e.preventDefault(); + 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 { @@ -251,66 +246,16 @@ export default function ScheduleForm() { 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 || "" - }.`, + description: `Consulta marcada para ${dateFormatted} às ${selectedTime}.`, }); - 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); - } - - // 🧹 limpa os campos setSelectedDoctor(""); setSelectedDate(""); setSelectedTime(""); @@ -322,241 +267,330 @@ export default function ScheduleForm() { } }; - // 🔹 Tooltip no calendário + // --- TOOLTIP --- 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

-

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

+

+ Agendar Consulta +

+

+ Preencha os dados abaixo para marcar seu horário. +

-
- {/* Coluna do Formulário */} -
- - - Informações Básicas - - - {["secretaria", "gestor", "admin"].includes(role) && ( +
+ + {/* == ESQUERDA == */} +
+ +
+ + {/* BLOCO 1: SELEÇÃO */} + + + + + Dados da Consulta + + + + + {/* COMBOBOX DE PACIENTE */} + {["secretaria", "gestor", "admin"].includes(role) && ( +
+ + + + + + + + {/* AQUI: align="start" e w igual ao trigger garantem que não invada a lateral */} + + + + + {/* AQUI: max-h-[130px] no mobile deixa a lista bem compacta */} + + Nenhum paciente encontrado. + + {patients.map((p) => ( + { + setSelectedPatient(p.id === selectedPatient ? "" : p.id); + setOpenPatientCombobox(false); + }} + className="text-xs md:text-sm py-1.5 md:py-2" + > + + {p.full_name} + + ))} + + + + + +
+ )} + + {/* COMBOBOX DE MÉDICO */}
- - + + + + + + + + {/* AQUI: Configurações de largura e posicionamento corrigidos */} + + + + + {/* AQUI: Altura reduzida no mobile */} + + Nenhum médico encontrado. + + {doctors.map((doctor) => ( + { + setSelectedDoctor(doctor.id === selectedDoctor ? "" : doctor.id); + setOpenDoctorCombobox(false); + }} + className="text-xs md:text-sm py-1.5 md:py-2" + > + +
+ {doctor.full_name} + {doctor.specialty} +
+
+ ))} +
+
+
+
+
+ +

+ Digite para filtrar por nome. +

- )} +
+
-
- - -
- - + {/* BLOCO 2: CALENDÁRIO */} + + + + + Data Disponível + + + +
+ { + if (!date) return; + const formatted = format(new Date(date.getTime() + 12 * 60 * 60 * 1000), "yyyy-MM-dd"); + setSelectedDate(formatted); + }} + className="rounded-md border p-3 w-fit" + /> +
+
+
+
+ {/* BLOCO 3: OBSERVAÇÕES */} - - Selecione a Data + + + + Observações (Opcional) + - -
- { - 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) - - +