riseup-squad20/susconecta/components/forms/calendar-registration-form.tsx
2025-10-18 17:02:59 -03:00

353 lines
17 KiB
TypeScript

"use client";
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";
import { Calendar, Search, ChevronDown } from "lucide-react";
interface FormData {
patientName?: string;
cpf?: string;
rg?: string;
birthDate?: string;
phoneCode?: string;
phoneNumber?: string;
email?: string;
convenio?: string;
matricula?: string;
validade?: string;
documentos?: string;
professionalName?: string;
unit?: string;
appointmentDate?: string;
startTime?: string;
endTime?: string;
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 {
formData: FormData;
onFormChange: (data: FormData) => void;
}
const formatValidityDate = (value: string) => {
const cleaned = value.replaceAll(/\D/g, "");
if (cleaned.length > 4) {
return `${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)}/${cleaned.slice(4, 8)}`;
}
if (cleaned.length > 2) {
return `${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)}`;
}
return cleaned;
};
export function CalendarRegistrationForm({ formData, onFormChange }: CalendarRegistrationFormProperties) {
const [isAdditionalInfoOpen, setIsAdditionalInfoOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<any | null>(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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = event.target;
// 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<string | null>(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 (
<form className="space-y-8">
<div className="border border-border rounded-md p-6 space-y-4 bg-card">
<h2 className="font-medium text-foreground">Informações do paciente</h2>
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Nome</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
name="patientName"
placeholder="Nome do paciente"
className="h-11 pl-8 rounded-md transition-colors bg-muted/10"
value={formData.patientName || ''}
disabled
/>
</div>
</div>
<div className="md:col-span-6 flex items-start justify-end">
<div className="text-right text-sm">
{loadingPatient ? (
<div>Carregando dados do paciente...</div>
) : patientDetails ? (
patientDetails.error ? (
<div className="text-red-500">Erro ao carregar paciente: {String(patientDetails.error)}</div>
) : (
<div className="text-sm text-muted-foreground space-y-1">
<div><strong>CPF:</strong> {patientDetails.cpf || '-'}</div>
<div><strong>Telefone:</strong> {patientDetails.phone_mobile || patientDetails.telefone || '-'}</div>
<div><strong>E-mail:</strong> {patientDetails.email || '-'}</div>
<div><strong>Data de nascimento:</strong> {patientDetails.birth_date || '-'}</div>
</div>
)
) : (
<div className="text-xs text-muted-foreground">Paciente não vinculado</div>
)}
<div className="mt-1 text-xs text-muted-foreground">Para editar os dados do paciente, acesse a ficha do paciente.</div>
</div>
</div>
</div>
</div>
<div className="border border-border rounded-md p-6 space-y-4 bg-card">
<h2 className="font-medium text-foreground">Informações do atendimento</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-[13px]">Nome do profissional *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="professionalName" className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30" value={formData.professionalName || ''} onChange={handleChange} disabled />
</div>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Data *</Label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="appointmentDate" type="date" className="h-11 w-full rounded-md pl-8 pr-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentDate || ''} onChange={handleChange} />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Início *</Label>
<Input name="startTime" type="time" className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.startTime || ''} onChange={handleChange} />
</div>
<div className="space-y-2">
<Label className="text-[13px]">Término *</Label>
<Input name="endTime" type="time" className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.endTime || ''} onChange={handleChange} />
</div>
{/* Profissional solicitante removed per user request */}
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Tipo de atendimento *</Label>
</div>
<div className="relative mt-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input name="appointmentType" placeholder="Pesquisar" className="h-11 w-full rounded-md pl-8 pr-8 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentType || ''} onChange={handleChange} disabled />
</div>
<div className="grid grid-cols-3 gap-3 mt-3">
<div>
<Label className="text-[13px]">Status</Label>
<select name="status" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-3 text-[13px]" value={formData.status || ''} onChange={handleChange}>
<option value="">Selecione</option>
<option value="requested">Solicitado</option>
<option value="confirmed">Confirmado</option>
<option value="checked_in">Check-in</option>
<option value="in_progress">Em andamento</option>
<option value="completed">Concluído</option>
<option value="cancelled">Cancelado</option>
<option value="no_show">Não compareceu</option>
</select>
</div>
<div>
<Label className="text-[13px]">Duração (min)</Label>
<Input name="duration_minutes" type="number" min={1} className="h-11 w-full rounded-md" value={formData.duration_minutes ?? ''} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Convênio</Label>
<Input name="insurance_provider" placeholder="Operadora" className="h-11 w-full rounded-md" value={formData.insurance_provider || ''} onChange={handleChange} />
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Observações</Label>
</div>
<Textarea name="notes" rows={4} className="text-[13px] min-h-[80px] resize-none rounded-md transition-colors hover:bg-muted/30" value={formData.notes || ''} onChange={handleChange} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
<div>
<Label className="text-[13px]">Queixa principal</Label>
<Textarea name="chief_complaint" rows={3} className="text-[13px] rounded-md" value={formData.chief_complaint || ''} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Notas do paciente</Label>
<Textarea name="patient_notes" rows={3} className="text-[13px] rounded-md" value={formData.patient_notes || ''} onChange={handleChange} />
</div>
</div>
<div className="grid grid-cols-3 gap-3 mt-3">
<div>
<Label className="text-[13px]">Horário de check-in</Label>
<Input name="checked_in_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.checked_in_at as any)} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Concluído em</Label>
<Input name="completed_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.completed_at as any)} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Cancelado em</Label>
<Input name="cancelled_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.cancelled_at as any)} onChange={handleChange} />
</div>
</div>
<div className="mt-3">
<Label className="text-[13px]">Motivo do cancelamento</Label>
<Input name="cancellation_reason" className="h-11 w-full rounded-md" value={formData.cancellation_reason || ''} onChange={handleChange} />
</div>
</div>
</div>
</div>
</div>
</form>
);
}