diff --git a/susconecta/app/(main-routes)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx index ebdc9ad..59bad50 100644 --- a/susconecta/app/(main-routes)/consultas/page.tsx +++ b/susconecta/app/(main-routes)/consultas/page.tsx @@ -55,7 +55,7 @@ import { } from "@/components/ui/select"; import { mockProfessionals } from "@/lib/mocks/appointment-mocks"; -import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds } from "@/lib/api"; +import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds, atualizarAgendamento } from "@/lib/api"; import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form"; const formatDate = (date: string | Date) => { @@ -80,15 +80,17 @@ export default function ConsultasPage() { const [showForm, setShowForm] = useState(false); const [editingAppointment, setEditingAppointment] = useState(null); const [viewingAppointment, setViewingAppointment] = useState(null); + // Local form state used when editing. Keep hook at top-level to avoid Hooks order changes. + const [localForm, setLocalForm] = useState(null); const mapAppointmentToFormData = (appointment: any) => { - const professional = mockProfessionals.find((p) => p.id === appointment.professional); const appointmentDate = new Date(appointment.time || appointment.scheduled_at || Date.now()); return { id: appointment.id, patientName: appointment.patient, - professionalName: professional ? professional.name : "", + patientId: appointment.patient_id || appointment.patientId || null, + professionalName: appointment.professional || "", appointmentDate: appointmentDate.toISOString().split("T")[0], startTime: appointmentDate.toTimeString().split(" ")[0].substring(0, 5), endTime: new Date(appointmentDate.getTime() + (appointment.duration || 30) * 60000) @@ -127,22 +129,72 @@ export default function ConsultasPage() { const handleCancel = () => { setEditingAppointment(null); setShowForm(false); + setLocalForm(null); }; - const handleSave = (formData: any) => { - const updatedAppointment = { - id: formData.id, - patient: formData.patientName, - time: new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(), - duration: 30, - type: formData.appointmentType as any, - status: formData.status as any, - professional: appointments.find((a) => a.id === formData.id)?.professional || "", - notes: formData.notes, - }; + const handleSave = async (formData: any) => { + try { + // build scheduled_at ISO (formData.startTime is 'HH:MM') + const scheduled_at = new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(); - setAppointments((prev) => prev.map((a) => (a.id === updatedAppointment.id ? updatedAppointment : a))); - handleCancel(); + // compute duration from start/end times when available + let duration_minutes = 30; + try { + if (formData.startTime && formData.endTime) { + const [sh, sm] = String(formData.startTime).split(":").map((n: string) => Number(n)); + const [eh, em] = String(formData.endTime).split(":").map((n: string) => Number(n)); + const start = (sh || 0) * 60 + (sm || 0); + const end = (eh || 0) * 60 + (em || 0); + if (!Number.isNaN(start) && !Number.isNaN(end) && end > start) duration_minutes = end - start; + } + } catch (e) { + // fallback to default + duration_minutes = 30; + } + + const payload: any = { + scheduled_at, + duration_minutes, + status: formData.status || undefined, + notes: formData.notes ?? null, + chief_complaint: formData.chief_complaint ?? null, + patient_notes: formData.patient_notes ?? null, + insurance_provider: formData.insurance_provider ?? null, + // convert local datetime-local inputs (which may be in 'YYYY-MM-DDTHH:MM' format) to proper ISO if present + checked_in_at: formData.checked_in_at ? new Date(formData.checked_in_at).toISOString() : null, + completed_at: formData.completed_at ? new Date(formData.completed_at).toISOString() : null, + cancelled_at: formData.cancelled_at ? new Date(formData.cancelled_at).toISOString() : null, + cancellation_reason: formData.cancellation_reason ?? null, + }; + + // Call PATCH endpoint + const updated = await atualizarAgendamento(formData.id, payload); + + // Build UI-friendly row using server response and existing local fields + const existing = appointments.find((a) => a.id === formData.id) || {}; + const mapped = { + id: updated.id, + patient: formData.patientName || existing.patient || '', + time: updated.scheduled_at || updated.created_at || scheduled_at, + duration: updated.duration_minutes || duration_minutes, + type: updated.appointment_type || formData.appointmentType || existing.type || 'presencial', + status: updated.status || formData.status || existing.status, + professional: existing.professional || formData.professionalName || '', + notes: updated.notes || updated.patient_notes || formData.notes || existing.notes || '', + }; + + setAppointments((prev) => prev.map((a) => (a.id === mapped.id ? mapped : a))); + handleCancel(); + } catch (err) { + console.error('[ConsultasPage] Falha ao atualizar agendamento', err); + // Inform the user + try { + const msg = err instanceof Error ? err.message : String(err); + alert('Falha ao salvar alterações: ' + msg); + } catch (e) { + // ignore + } + } }; useEffect(() => { @@ -190,6 +242,7 @@ export default function ConsultasPage() { return { id: a.id, patient, + patient_id: a.patient_id, time: a.scheduled_at || a.created_at || "", duration: a.duration_minutes || 30, type: a.appointment_type || "presencial", @@ -214,15 +267,23 @@ export default function ConsultasPage() { }; }, []); - // editing view: render the calendar registration form with controlled data - if (showForm && editingAppointment) { - const [localForm, setLocalForm] = useState(editingAppointment); - const onFormChange = (d: any) => setLocalForm(d); + // Keep localForm synchronized with editingAppointment + useEffect(() => { + if (showForm && editingAppointment) { + setLocalForm(editingAppointment); + } + if (!showForm) setLocalForm(null); + }, [showForm, editingAppointment]); - const saveLocal = () => { - handleSave(localForm); - }; + const onFormChange = (d: any) => setLocalForm(d); + const saveLocal = async () => { + if (!localForm) return; + await handleSave(localForm); + }; + + // If editing, render the edit form as a focused view (keeps hooks stable) + if (showForm && localForm) { return (
diff --git a/susconecta/components/forms/calendar-registration-form.tsx b/susconecta/components/forms/calendar-registration-form.tsx index 5a6c21c..f3fa4e5 100644 --- a/susconecta/components/forms/calendar-registration-form.tsx +++ b/susconecta/components/forms/calendar-registration-form.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; +import { buscarPacientePorId } from "@/lib/api"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; @@ -27,6 +28,16 @@ interface FormData { requestingProfessional?: string; appointmentType?: string; notes?: string; + // API-editable appointment fields + status?: string; + duration_minutes?: number; + chief_complaint?: string; + patient_notes?: string; + insurance_provider?: string; + checked_in_at?: string; // ISO datetime + completed_at?: string; // ISO datetime + cancelled_at?: string; // ISO datetime + cancellation_reason?: string; } interface CalendarRegistrationFormProperties { @@ -47,120 +58,187 @@ const formatValidityDate = (value: string) => { export function CalendarRegistrationForm({ formData, onFormChange }: CalendarRegistrationFormProperties) { const [isAdditionalInfoOpen, setIsAdditionalInfoOpen] = useState(false); + const [patientDetails, setPatientDetails] = useState(null); + const [loadingPatient, setLoadingPatient] = useState(false); + + // Helpers to convert between ISO (server) and input[type=datetime-local] value + const isoToDatetimeLocal = (iso?: string | null) => { + if (!iso) return ""; + try { + let s = String(iso).trim(); + // normalize common variants: space between date/time -> T + s = s.replace(" ", "T"); + // If no timezone info (no Z or +/-), try treating as UTC by appending Z + if (!/[zZ]$/.test(s) && !/[+-]\d{2}:?\d{2}$/.test(s)) { + // try parse first; if invalid, append Z + const tryParse = Date.parse(s); + if (isNaN(tryParse)) { + s = s + "Z"; + } + } + const d = new Date(s); + if (isNaN(d.getTime())) return ""; + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + const hh = String(d.getHours()).padStart(2, "0"); + const min = String(d.getMinutes()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}T${hh}:${min}`; + } catch (e) { + return ""; + } + }; + + const datetimeLocalToIso = (value: string) => { + if (!value) return null; + // value expected: YYYY-MM-DDTHH:MM or with seconds + try { + // If the browser gives a value without seconds, Date constructor will treat as local when we split + const [datePart, timePart] = value.split("T"); + if (!datePart || !timePart) return null; + const [y, m, d] = datePart.split("-").map((s) => parseInt(s, 10)); + const timeParts = timePart.split(":"); + const hh = parseInt(timeParts[0], 10); + const min = parseInt(timeParts[1] || "0", 10); + const sec = parseInt(timeParts[2] || "0", 10); + if ([y, m, d, hh, min, sec].some((n) => Number.isNaN(n))) return null; + const dt = new Date(y, m - 1, d, hh, min, sec, 0); + return dt.toISOString(); + } catch (e) { + return null; + } + }; + + // Automatically fetch patient details when the form receives a patientId + useEffect(() => { + const maybeId = (formData as any).patientId || (formData as any).patient_id || null; + if (!maybeId) { + setPatientDetails(null); + return; + } + let mounted = true; + setLoadingPatient(true); + buscarPacientePorId(maybeId) + .then((p) => { + if (!mounted) return; + setPatientDetails(p); + }) + .catch((e) => { + if (!mounted) return; + setPatientDetails({ error: String(e) }); + }) + .finally(() => { + if (!mounted) return; + setLoadingPatient(false); + }); + return () => { + mounted = false; + }; + }, [(formData as any).patientId, (formData as any).patient_id]); const handleChange = (event: React.ChangeEvent) => { const { name, value } = event.target; - if (name === 'validade') { - const formattedValue = formatValidityDate(value); - onFormChange({ ...formData, [name]: formattedValue }); - } else { - onFormChange({ ...formData, [name]: value }); + // map datetime-local fields to ISO before sending up + if (name === 'checked_in_at' || name === 'completed_at' || name === 'cancelled_at') { + const iso = datetimeLocalToIso(value as string); + onFormChange({ ...formData, [name]: iso }); + return; } + + if (name === 'validade') { + const formattedValue = formatValidityDate(value); + onFormChange({ ...formData, [name]: formattedValue }); + return; + } + + // ensure duration is stored as a number + if (name === 'duration_minutes') { + const n = Number(value); + onFormChange({ ...formData, duration_minutes: Number.isNaN(n) ? undefined : n }); + return; + } + + // If user edits endTime manually, accept the value and clear lastAutoEndRef so auto-calc won't overwrite + if (name === 'endTime') { + // store as-is (HH:MM) + try { + // clear auto flag so user edits persist + (lastAutoEndRef as any).current = null; + } catch (e) {} + onFormChange({ ...formData, endTime: value }); + return; + } + + onFormChange({ ...formData, [name]: value }); }; + // Auto-calculate endTime from startTime + duration_minutes + const lastAutoEndRef = useRef(null); + + useEffect(() => { + const start = (formData as any).startTime; + const dur = (formData as any).duration_minutes; + const date = (formData as any).appointmentDate; // YYYY-MM-DD + if (!start) return; + // if duration is not a finite number, don't compute + const minutes = typeof dur === 'number' && Number.isFinite(dur) ? dur : 0; + // build a Date from appointmentDate + startTime; fall back to today if appointmentDate missing + const datePart = date || new Date().toISOString().slice(0, 10); + const [y, m, d] = String(datePart).split('-').map((s) => parseInt(s, 10)); + const [hh, mm] = String(start).split(':').map((s) => parseInt(s, 10)); + if ([y, m, d, hh, mm].some((n) => Number.isNaN(n))) return; + const dt = new Date(y, m - 1, d, hh, mm, 0, 0); + const dt2 = new Date(dt.getTime() + minutes * 60000); + const newEnd = `${String(dt2.getHours()).padStart(2, '0')}:${String(dt2.getMinutes()).padStart(2, '0')}`; + const currentEnd = (formData as any).endTime; + // Only overwrite if endTime is empty or it was the previously auto-calculated value + if (!currentEnd || currentEnd === lastAutoEndRef.current) { + lastAutoEndRef.current = newEnd; + onFormChange({ ...formData, endTime: newEnd }); + } + }, [(formData as any).startTime, (formData as any).duration_minutes, (formData as any).appointmentDate]); + + return (

Informações do paciente

-
- -
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
- -
- - -
-
-
-
-
- - -
-
- - -
-
-
-
-
setIsAdditionalInfoOpen(!isAdditionalInfoOpen)} - > -
- - -
-
- {isAdditionalInfoOpen && ( -
+
+
- - + +
- )} +
+
+ {loadingPatient ? ( +
Carregando dados do paciente...
+ ) : patientDetails ? ( + patientDetails.error ? ( +
Erro ao carregar paciente: {String(patientDetails.error)}
+ ) : ( +
+
CPF: {patientDetails.cpf || '-'}
+
Telefone: {patientDetails.phone_mobile || patientDetails.telefone || '-'}
+
E-mail: {patientDetails.email || '-'}
+
Data de nascimento: {patientDetails.birth_date || '-'}
+
+ ) + ) : ( +
Paciente não vinculado
+ )} +
Para editar os dados do paciente, acesse a ficha do paciente.
+
@@ -170,27 +248,18 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
-
- - -
-
-
-
- - -
-
- -
- - -
-
+
+ + +
+
+ +
+ + +
+
@@ -200,48 +269,84 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
-
- -
- - - -
-
+ {/* Profissional solicitante removed per user request */}
-
- -
- - -
-
-
- - -
+
+
-
-
- -
- - -
-
-