diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index b5a7947..f1ce1b3 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -11,10 +11,7 @@ import { EventInput } from "@fullcalendar/core/index.js"; import { Sidebar } from "@/components/dashboard/sidebar"; import { PagesHeader } from "@/components/dashboard/header"; import { Button } from "@/components/ui/button"; -import { - mockAppointments, - mockWaitingList, -} from "@/lib/mocks/appointment-mocks"; +import { mockWaitingList } from "@/lib/mocks/appointment-mocks"; import "./index.css"; import Link from "next/link"; import { @@ -30,7 +27,7 @@ const ListaEspera = dynamic( ); export default function AgendamentoPage() { - const [appointments, setAppointments] = useState(mockAppointments); + const [appointments, setAppointments] = useState([]); const [waitingList, setWaitingList] = useState(mockWaitingList); const [activeTab, setActiveTab] = useState<"calendar" | "espera">("calendar"); const [requestsList, setRequestsList] = useState(); @@ -47,23 +44,51 @@ export default function AgendamentoPage() { }, []); useEffect(() => { - let events: EventInput[] = []; - appointments.forEach((obj) => { - const event: EventInput = { - title: `${obj.patient}: ${obj.type}`, - start: new Date(obj.time), - end: new Date(new Date(obj.time).getTime() + obj.duration * 60 * 1000), - color: - obj.status === "confirmed" - ? "#68d68a" - : obj.status === "pending" - ? "#ffe55f" - : "#ff5f5fff", - }; - events.push(event); - }); - setRequestsList(events); - }, [appointments]); + // Fetch real appointments and map to calendar events + let mounted = true; + (async () => { + try { + // listarAgendamentos accepts a query string; request a reasonable limit and order + const arr = await (await import('@/lib/api')).listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []); + if (!mounted) return; + if (!arr || !arr.length) { + setAppointments([]); + setRequestsList([]); + return; + } + + // Batch-fetch patient names for display + const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean))); + const patients = (patientIds && patientIds.length) ? await (await import('@/lib/api')).buscarPacientesPorIds(patientIds) : []; + const patientsById: Record = {}; + (patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; }); + + setAppointments(arr || []); + + const events: EventInput[] = (arr || []).map((obj: any) => { + const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null; + const start = scheduled ? new Date(scheduled) : null; + const duration = Number(obj.duration_minutes ?? obj.duration ?? 30) || 30; + const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente'; + const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim(); + const color = obj.status === 'confirmed' ? '#68d68a' : obj.status === 'pending' ? '#ffe55f' : '#ff5f5fff'; + return { + title, + start: start || new Date(), + end: start ? new Date(start.getTime() + duration * 60 * 1000) : undefined, + color, + extendedProps: { raw: obj }, + } as EventInput; + }); + setRequestsList(events || []); + } catch (err) { + console.warn('[AgendamentoPage] falha ao carregar agendamentos', err); + setAppointments([]); + setRequestsList([]); + } + })(); + return () => { mounted = false; }; + }, []); // mantive para caso a lógica de salvar consulta passe a funcionar const handleSaveAppointment = (appointment: any) => { diff --git a/susconecta/app/(main-routes)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx index ce70143..29462ac 100644 --- a/susconecta/app/(main-routes)/consultas/page.tsx +++ b/susconecta/app/(main-routes)/consultas/page.tsx @@ -508,7 +508,7 @@ export default function ConsultasPage() { {capitalize(appointment.status)} - {formatDate(appointment.time)} + {formatDate(appointment.scheduled_at ?? appointment.time)} @@ -560,7 +560,7 @@ export default function ConsultasPage() {
- {viewingAppointment?.time ? formatDate(viewingAppointment.time) : ''} + {(viewingAppointment?.scheduled_at ?? viewingAppointment?.time) ? formatDate(viewingAppointment?.scheduled_at ?? viewingAppointment?.time) : ''}
diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index 57dcf44..5aff998 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -4,7 +4,7 @@ import SignatureCanvas from "react-signature-canvas"; import Link from "next/link"; import ProtectedRoute from "@/components/ProtectedRoute"; import { useAuth } from "@/hooks/useAuth"; -import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api"; +import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api"; import { useReports } from "@/hooks/useReports"; import { CreateReportData } from "@/types/report-types"; import { Button } from "@/components/ui/button"; @@ -239,36 +239,127 @@ const ProfissionalPage = () => { - const [events, setEvents] = useState([ - - { - id: 1, - title: "Ana Souza", - type: "Cardiologia", - time: "09:00", - date: new Date().toISOString().split('T')[0], - pacienteId: "123.456.789-00", - color: colorsByType.Cardiologia - }, - { - id: 2, - title: "Bruno Lima", - type: "Cardiologia", - time: "10:30", - date: new Date().toISOString().split('T')[0], - pacienteId: "987.654.321-00", - color: colorsByType.Cardiologia - }, - { - id: 3, - title: "Carla Menezes", - type: "Dermatologia", - time: "14:00", - date: new Date().toISOString().split('T')[0], - pacienteId: "111.222.333-44", - color: colorsByType.Dermatologia - } - ]); + const [events, setEvents] = useState([]); + // Load real appointments for the logged in doctor and map to calendar events + useEffect(() => { + let mounted = true; + (async () => { + try { + // If we already have a doctorId (set earlier), use it. Otherwise try to resolve from the logged user + let docId = doctorId; + if (!docId && user && user.email) { + // buscarMedicos may return the doctor's record including id + try { + const docs = await buscarMedicos(user.email).catch(() => []); + if (Array.isArray(docs) && docs.length > 0) { + const chosen = docs.find(d => String((d as any).user_id) === String(user.id)) || docs[0]; + docId = (chosen as any)?.id ?? null; + if (mounted && !doctorId) setDoctorId(docId); + } + } catch (e) { + // ignore + } + } + + if (!docId) { + // nothing to fetch yet + return; + } + + // Fetch appointments for this doctor. We'll ask for future and recent past appointments + // using a simple filter: doctor_id=eq.&order=scheduled_at.asc&limit=200 + const qs = `?select=*&doctor_id=eq.${encodeURIComponent(String(docId))}&order=scheduled_at.asc&limit=200`; + const appts = await listarAgendamentos(qs).catch(() => []); + + if (!mounted) return; + + // Enrich appointments with patient names (batch fetch) and map to UI events + const patientIds = Array.from(new Set((appts || []).map((x:any) => String(x.patient_id || x.patient_id_raw || '').trim()).filter(Boolean))); + let patientMap = new Map(); + if (patientIds.length) { + try { + const patients = await buscarPacientesPorIds(patientIds).catch(() => []); + for (const p of patients || []) { + if (p && p.id) patientMap.set(String(p.id), p); + } + } catch (e) { + console.warn('[ProfissionalPage] falha ao buscar pacientes para eventos:', e); + } + } + + const mapped = (appts || []).map((a: any, idx: number) => { + const scheduled = a.scheduled_at || a.time || a.created_at || null; + // Use local date components to avoid UTC shift when showing the appointment day/time + let datePart = new Date().toISOString().split('T')[0]; + let timePart = ''; + if (scheduled) { + try { + const d = new Date(scheduled); + // build local date string YYYY-MM-DD using local getters + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + datePart = `${y}-${m}-${day}`; + timePart = `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; + } catch (e) { + // ignore + } + } + + const pid = a.patient_id || a.patient || a.patient_id_raw || a.patientId || null; + const patientObj = pid ? patientMap.get(String(pid)) : null; + const patientName = patientObj?.full_name || a.patient || a.patient_name || String(pid) || 'Paciente'; + const patientIdVal = pid || null; + + return { + id: a.id ?? `srv-${idx}-${String(a.scheduled_at || a.created_at || idx)}`, + title: patientName || 'Paciente', + type: a.appointment_type || 'Consulta', + time: timePart || '', + date: datePart, + pacienteId: patientIdVal, + color: colorsByType[a.specialty as keyof typeof colorsByType] || '#4dabf7', + raw: a, + }; + }); + + setEvents(mapped); + + // Helper: parse 'YYYY-MM-DD' into a local Date to avoid UTC parsing which can shift day + const parseYMDToLocal = (ymd?: string) => { + if (!ymd || typeof ymd !== 'string') return new Date(); + const parts = ymd.split('-').map((p) => Number(p)); + if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) return new Date(ymd); + const [y, m, d] = parts; + return new Date(y, (m || 1) - 1, d || 1); + }; + + // Set calendar view to nearest upcoming appointment (or today) + try { + const now = Date.now(); + const upcoming = mapped.find((m:any) => { + if (!m.raw) return false; + const s = m.raw.scheduled_at || m.raw.time || m.raw.created_at; + if (!s) return false; + const t = new Date(s).getTime(); + return !isNaN(t) && t >= now; + }); + if (upcoming) { + setCurrentCalendarDate(parseYMDToLocal(upcoming.date)); + } else if (mapped.length) { + // fallback: show the date of the first appointment + setCurrentCalendarDate(parseYMDToLocal(mapped[0].date)); + } + } catch (e) { + // ignore + } + } catch (err) { + console.warn('[ProfissionalPage] falha ao carregar agendamentos do servidor:', err); + // Keep mocked/empty events if fetch fails + } + })(); + return () => { mounted = false; }; + }, [doctorId, user?.id, user?.email]); const [editingEvent, setEditingEvent] = useState(null); const [showPopup, setShowPopup] = useState(false); const [showActionModal, setShowActionModal] = useState(false); diff --git a/susconecta/components/forms/calendar-registration-form.tsx b/susconecta/components/forms/calendar-registration-form.tsx index cf9f0be..df84275 100644 --- a/susconecta/components/forms/calendar-registration-form.tsx +++ b/susconecta/components/forms/calendar-registration-form.tsx @@ -766,7 +766,53 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
- + {/* + When creating a new appointment from a predefined slot, the end time + is derived from the slot's start + duration and therefore cannot be + edited. We disable/readOnly the input in create mode when either a + slot is selected (startTime corresponds to an availableSlots entry) + or the duration was locked from a slot (lockedDurationFromSlot). + */} + { + try { + const date = (formData as any).appointmentDate || ''; + const time = (formData as any).startTime || ''; + if (!date || !time) return false; + // Check if startTime matches one of the availableSlots (meaning slot-driven) + return (availableSlots || []).some((s) => { + try { + const d = new Date(s.datetime); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const dateOnly = d.toISOString().split('T')[0]; + return dateOnly === date && `${hh}:${mm}` === time; + } catch (e) { return false; } + }); + } catch (e) { return false; } + })()))} + disabled={createMode && (lockedDurationFromSlot || Boolean(((): boolean => { + try { + const date = (formData as any).appointmentDate || ''; + const time = (formData as any).startTime || ''; + if (!date || !time) return false; + return (availableSlots || []).some((s) => { + try { + const d = new Date(s.datetime); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const dateOnly = d.toISOString().split('T')[0]; + return dateOnly === date && `${hh}:${mm}` === time; + } catch (e) { return false; } + }); + } catch (e) { return false; } + })()))} + />
{/* Profissional solicitante removed per user request */}