'use client'; import React, { useState, useEffect, useRef } from 'react'; import { format, addHours, parseISO } from 'date-fns'; import { nl } from 'date-fns/locale'; import { Calendar, Clock, MapPin, User, Check, X, AlertCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { motion } from 'framer-motion'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { toast } from '@/hooks/use-toast'; import { ToastAction } from '@/components/ui/toast'; import { createEncounter } from '@/app/epd/agenda/actions'; import { useCortexStore } from '@/stores/cortex-store'; import { formatPatientName as formatPatientNameFromDb } from '@/lib/fhir/patient-mapper'; import { APPOINTMENT_TYPES, LOCATION_CLASSES, AppointmentTypeCode, LocationClassCode, APPOINTMENT_TYPE_COLORS } from '@/app/epd/agenda/types'; import { AgendaErrorAlert } from './agenda-error-state'; interface AgendaCreateFormProps { prefillData?: { patient?: { id: string; name: string }; datetime?: { date: Date; time: string }; type?: AppointmentTypeCode; location?: LocationClassCode; notes?: string; }; onClose?: () => void; } interface PatientResult { id: string; name: string; bsn?: string; birthDate?: string; } function normalizePatientName(name: string) { return name.toLowerCase().trim().replace(/\s+/g, ' '); } export function AgendaCreateForm({ prefillData, onClose }: AgendaCreateFormProps) { const activePatient = useCortexStore((s) => s.activePatient); // Form State const [patientId, setPatientId] = useState(prefillData?.patient?.id || ''); const [patientName, setPatientName] = useState(prefillData?.patient?.name || ''); const [date, setDate] = useState( prefillData?.datetime?.date ? format(prefillData.datetime.date, 'yyyy-MM-dd') : format(new Date(), 'yyyy-MM-dd') ); const [time, setTime] = useState(prefillData?.datetime?.time || '09:00'); const [type, setType] = useState(prefillData?.type || 'behandeling'); const [location, setLocation] = useState(prefillData?.location || 'AMB'); const [notes, setNotes] = useState(prefillData?.notes || ''); // UI State const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); // Patient Search State const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showResults, setShowResults] = useState(false); const searchRef = useRef(null); const autoResolvedPatientRef = useRef(null); // Initialize search query if patient is prefilled but we want to allow editing useEffect(() => { if (prefillData?.patient?.name) { setSearchQuery(prefillData.patient.name); } }, [prefillData]); // If Cortex only extracted a name, resolve it once so the appointment form // can submit without forcing the user to retype and select the same client. useEffect(() => { const prefilledPatient = prefillData?.patient; if (!prefilledPatient?.name || prefilledPatient.id || patientId) return; const normalizedPrefill = normalizePatientName(prefilledPatient.name); if (autoResolvedPatientRef.current === normalizedPrefill) return; autoResolvedPatientRef.current = normalizedPrefill; if (activePatient) { const activePatientName = formatPatientNameFromDb(activePatient); if (normalizePatientName(activePatientName) === normalizedPrefill) { setPatientId(activePatient.id); setPatientName(activePatientName); setSearchQuery(activePatientName); setShowResults(false); return; } } let cancelled = false; async function resolvePrefilledPatient() { setIsSearching(true); try { const res = await fetch(`/api/cortex/patients/search?q=${encodeURIComponent(prefilledPatient.name)}`); if (!res.ok || cancelled) return; const data = await res.json(); const patients = (data.patients || []) as PatientResult[]; const exactMatch = patients.find( (patient) => normalizePatientName(patient.name) === normalizedPrefill ); const match = exactMatch || (patients.length === 1 ? patients[0] : null); if (match) { setPatientId(match.id); setPatientName(match.name); setSearchQuery(match.name); setSearchResults([]); setShowResults(false); } else { setSearchResults(patients); setShowResults(patients.length > 0); } } catch (err) { console.error('Failed to resolve prefilled patient', err); } finally { if (!cancelled) { setIsSearching(false); } } } resolvePrefilledPatient(); return () => { cancelled = true; }; }, [prefillData, patientId, activePatient]); // Handle outside click to close search results useEffect(() => { function handleClickOutside(event: MouseEvent) { if (searchRef.current && !searchRef.current.contains(event.target as Node)) { setShowResults(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Debounced search useEffect(() => { const timer = setTimeout(async () => { if (searchQuery.length < 2 || patientId) return; // Don't search if too short or if patient already selected setIsSearching(true); try { const res = await fetch(`/api/cortex/patients/search?q=${encodeURIComponent(searchQuery)}`); if (res.ok) { const data = await res.json(); setSearchResults(data.patients || []); setShowResults(true); } } catch (err) { console.error('Failed to search patients', err); } finally { setIsSearching(false); } }, 300); return () => clearTimeout(timer); }, [searchQuery, patientId]); const handlePatientSelect = (patient: PatientResult) => { setPatientId(patient.id); setPatientName(patient.name); setSearchQuery(patient.name); setShowResults(false); }; const handleSearchChange = (e: React.ChangeEvent) => { setSearchQuery(e.target.value); setPatientId(''); // Clear selection on edit setPatientName(''); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!patientId) { setError('Selecteer a.u.b. een patiënt.'); return; } setIsSubmitting(true); setError(null); try { // Construct Date objects const startDate = new Date(`${date}T${time}`); const endDate = addHours(startDate, 1); // Default duration 1 hour // Map codes to displays const typeDisplay = APPOINTMENT_TYPES[type]; const locationDisplay = LOCATION_CLASSES[location]; const result = await createEncounter({ patientId, periodStart: startDate.toISOString(), periodEnd: endDate.toISOString(), typeCode: type, typeDisplay, classCode: location, classDisplay: locationDisplay, notes: notes || undefined, }); if (result.success) { const formattedDate = format(startDate, "EEEE d MMMM 'om' HH:mm", { locale: nl }); toast({ title: '✓ Afspraak ingepland', description: `${patientName} — ${formattedDate}`, action: ( window.location.href = `/epd/agenda?highlight=${result.data?.id}&date=${date}`} > Bekijken ), }); onClose?.(); } else { setError(result.error || 'Er is een fout opgetreden.'); } } catch (err) { console.error('Submit error:', err); setError('Er is een onverwachte fout opgetreden.'); } finally { setIsSubmitting(false); } }; return ( {/* Header */}

Nieuwe afspraak inplannen

{/* Form Body */}
{error && ( setError(null)} showFallbackLink={true} /> )} {/* Patient Selection */}
{isSearching && (
)} {showResults && searchResults.length > 0 && (
{searchResults.map((p) => ( ))}
)}
{/* Date & Time */}
setDate(e.target.value)} className="pl-10 h-10 bg-white/50 backdrop-blur-sm border-black/10 focus:bg-white transition-colors" required />
setTime(e.target.value)} className="pl-10 h-10 bg-white/50 backdrop-blur-sm border-black/10 focus:bg-white transition-colors" required />
{/* Type Selection */}
{(Object.keys(APPOINTMENT_TYPES) as AppointmentTypeCode[]).map((t) => { const bg = APPOINTMENT_TYPE_COLORS[t].bg; const text = APPOINTMENT_TYPE_COLORS[t].text; const border = APPOINTMENT_TYPE_COLORS[t].border; const isActive = type === t; return ( ); })}
{/* Location Selection */}
{(Object.keys(LOCATION_CLASSES) as LocationClassCode[]).map((l) => ( ))}
{/* Notes */}