"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, 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) import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; export default function ScheduleForm() { // Estado do usuário e role const [role, setRole] = useState("paciente"); const [userId, setUserId] = useState(null); // Listas e seleções const [patients, setPatients] = useState([]); const [selectedPatient, setSelectedPatient] = useState(""); const [openPatientCombobox, setOpenPatientCombobox] = useState(false); const [doctors, setDoctors] = useState([]); const [selectedDoctor, setSelectedDoctor] = useState(""); const [openDoctorCombobox, setOpenDoctorCombobox] = useState(false); // Novo estado para médico 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< Record >({}); 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; const getBrazilDate = (date: Date) => new Date( Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0) ); // 🔹 Buscar dados do usuário e role useEffect(() => { (async () => { try { 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 || []); } } catch (err) { console.error("Erro ao carregar usuário:", err); } })(); }, []); // 🔹 Buscar médicos const fetchDoctors = useCallback(async () => { setLoadingDoctors(true); try { 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.", }); } finally { setLoadingDoctors(false); } }, []); useEffect(() => { fetchDoctors(); }, [fetchDoctors]); // 🔹 Buscar disponibilidades const loadDoctorDisponibilidades = useCallback(async (doctorId?: string) => { if (!doctorId) return; try { const disp = await AvailabilityService.listById(doctorId); setDisponibilidades(disp || []); await computeAvailabilityCountsPreview(doctorId, disp || []); } catch (err) { 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 appointments = await appointmentsService.search_appointment( `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 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 ); if (dailyDisp.length === 0) { counts[key] = 0; continue; } 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); } setAvailabilityCounts(counts); } catch (err) { console.error("Erro ao calcular contagens:", err); setAvailabilityCounts({}); } }; // 🔹 Quando médico muda useEffect(() => { if (selectedDoctor) { loadDoctorDisponibilidades(selectedDoctor); } else { setDisponibilidades([]); setAvailabilityCounts({}); } 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); } }, [] ); useEffect(() => { if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate); }, [selectedDoctor, selectedDate, fetchAvailableSlots]); // 🔹 Submeter agendamento const handleSubmit = async (e: React.FormEvent) => { 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; } 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; } // 🔹 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 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; 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); return () => { cont.removeEventListener("mousemove", onMove); cont.removeEventListener("mouseleave", onLeave); }; }, [availabilityCounts]); return (

Agendar 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} ))}
)} {/* 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}
))}
{ if (!date) return; const formatted = format( new Date(date.getTime() + 12 * 60 * 60 * 1000), "yyyy-MM-dd" ); setSelectedDate(formatted); }} />