diff --git a/app/api/fhir/Patient/route.ts b/app/api/fhir/Patient/route.ts index 17b88b2..a6fc278 100644 --- a/app/api/fhir/Patient/route.ts +++ b/app/api/fhir/Patient/route.ts @@ -26,20 +26,44 @@ export async function GET(request: NextRequest) { // Build query let query = supabaseAdmin.from('patients').select('*'); - // Search by name (family or given) + // General search (searches name, BSN, and client number) + const q = searchParams.get('q'); + if (q) { + // Check if input looks like a number (BSN or client number) + const isNumeric = /^\d+$/.test(q.trim()); + if (isNumeric) { + // Search BSN and client number + query = query.or( + `identifier_bsn.ilike.%${q}%,identifier_client_number.ilike.%${q}%` + ); + } else { + // Search name fields + query = query.or( + `name_family.ilike.%${q}%,name_given.cs.{${q}}` + ); + } + } + + // Search by name (family or given) - legacy support const name = searchParams.get('name'); - if (name) { + if (name && !q) { query = query.or( `name_family.ilike.%${name}%,name_given.cs.{${name}}` ); } - // Search by identifier (BSN) + // Search by identifier (BSN) - legacy support const identifier = searchParams.get('identifier'); - if (identifier) { + if (identifier && !q) { query = query.eq('identifier_bsn', identifier); } + // Search by client number + const clientNumber = searchParams.get('clientNumber'); + if (clientNumber && !q) { + query = query.ilike('identifier_client_number', `%${clientNumber}%`); + } + // Search by birth date const birthdate = searchParams.get('birthdate'); if (birthdate) { @@ -55,6 +79,15 @@ export async function GET(request: NextRequest) { // Order by updated_at descending (newest first) query = query.order('updated_at', { ascending: false }); + // Limit results (_count parameter) + const count = searchParams.get('_count'); + if (count) { + const limit = parseInt(count, 10); + if (!isNaN(limit) && limit > 0) { + query = query.limit(limit); + } + } + // Execute query const { data: patients, error } = await query; diff --git a/app/api/reports/route.ts b/app/api/reports/route.ts index e9614d0..37fcc56 100644 --- a/app/api/reports/route.ts +++ b/app/api/reports/route.ts @@ -81,7 +81,7 @@ export async function POST(request: NextRequest) { ); } - const { patient_id, type, content, ai_confidence, ai_reasoning } = result.data; + const { patient_id, type, content, ai_confidence, ai_reasoning, encounter_id, intake_id } = result.data; const { data, error } = await supabase .from('reports') .insert({ @@ -90,6 +90,8 @@ export async function POST(request: NextRequest) { content, ai_confidence, ai_reasoning, + encounter_id, + intake_id, created_by: authData.user.id, }) .select('*') diff --git a/app/epd/agenda/actions.ts b/app/epd/agenda/actions.ts index 5dd482d..a7b54b1 100644 --- a/app/epd/agenda/actions.ts +++ b/app/epd/agenda/actions.ts @@ -70,7 +70,7 @@ export async function getEncounters({ } | null; const patientName = patient - ? `${patient.name_given?.[0] || ''} ${patient.name_family}`.trim() + ? `${patient.name_given?.[0] || ''} ${patient.name_family || ''}`.trim() || 'Onbekende patiënt' : 'Onbekende patiënt'; const typeCode = encounter.type_code as AppointmentTypeCode; @@ -94,7 +94,7 @@ export async function getEncounters({ interface CreateEncounterParams { patientId: string; - practitionerId: string; + practitionerId?: string; periodStart: string; periodEnd?: string; typeCode: string; @@ -111,7 +111,7 @@ export async function createEncounter(params: CreateEncounterParams) { .from('encounters') .insert({ patient_id: params.patientId, - practitioner_id: params.practitionerId, + practitioner_id: params.practitionerId || null, period_start: params.periodStart, period_end: params.periodEnd, type_code: params.typeCode, @@ -137,18 +137,26 @@ export async function updateEncounter( encounterId: string, updates: { periodStart?: string; - periodEnd?: string; + periodEnd?: string | null; status?: string; notes?: string; + typeCode?: string; + typeDisplay?: string; + classCode?: string; + classDisplay?: string; } ) { const supabase = await createClient(); const updateData: Record = {}; if (updates.periodStart) updateData.period_start = updates.periodStart; - if (updates.periodEnd) updateData.period_end = updates.periodEnd; + if (updates.periodEnd !== undefined) updateData.period_end = updates.periodEnd; if (updates.status) updateData.status = updates.status; if (updates.notes !== undefined) updateData.notes = updates.notes; + if (updates.typeCode) updateData.type_code = updates.typeCode; + if (updates.typeDisplay) updateData.type_display = updates.typeDisplay; + if (updates.classCode) updateData.class_code = updates.classCode; + if (updates.classDisplay) updateData.class_display = updates.classDisplay; const { data, error } = await supabase .from('encounters') @@ -180,3 +188,66 @@ export async function rescheduleEncounter( periodEnd: newEnd || undefined, }); } + +/** + * Get encounters for a specific patient (for linking reports to appointments) + */ +export async function getPatientEncounters(patientId: string) { + const supabase = await createClient(); + + const { data, error } = await supabase + .from('encounters') + .select('id, period_start, period_end, type_code, type_display, status, notes') + .eq('patient_id', patientId) + .neq('status', 'cancelled') + .order('period_start', { ascending: false }) + .limit(50); + + if (error) { + console.error('Error fetching patient encounters:', error); + return []; + } + + return data || []; +} + +/** + * Get reports linked to a specific encounter + */ +export async function getEncounterReports(encounterId: string) { + const supabase = await createClient(); + + const { data, error } = await supabase + .from('reports') + .select('id, type, content, created_at') + .eq('encounter_id', encounterId) + .is('deleted_at', null) + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching encounter reports:', error); + return []; + } + + return data || []; +} + +/** + * Get a single encounter by ID (for navigation from report to appointment) + */ +export async function getEncounterById(encounterId: string) { + const supabase = await createClient(); + + const { data, error } = await supabase + .from('encounters') + .select('id, period_start, period_end, type_code, type_display, status') + .eq('id', encounterId) + .single(); + + if (error) { + console.error('Error fetching encounter:', error); + return null; + } + + return data; +} diff --git a/app/epd/agenda/components/agenda-view.tsx b/app/epd/agenda/components/agenda-view.tsx index 536b59e..32f610e 100644 --- a/app/epd/agenda/components/agenda-view.tsx +++ b/app/epd/agenda/components/agenda-view.tsx @@ -6,28 +6,59 @@ * Client-side wrapper managing calendar state, view switching, and interactions. */ -import { useState, useCallback, useRef, useEffect, useTransition } from 'react'; +import { useState, useCallback, useRef, useTransition, useEffect } from 'react'; import { startOfWeek, endOfWeek, addDays, format } from 'date-fns'; import { toast } from '@/hooks/use-toast'; import { AgendaCalendar } from './agenda-calendar'; import { AgendaToolbar } from './agenda-toolbar'; +import { AppointmentModal } from './appointment-modal'; +import { RescheduleDialog } from './reschedule-dialog'; import { getEncounters, rescheduleEncounter } from '../actions'; import type { CalendarEvent, CalendarView } from '../types'; +interface PendingReschedule { + event: CalendarEvent; + newStart: Date; + newEnd: Date | null; +} + interface AgendaViewProps { initialEvents: CalendarEvent[]; initialDate?: Date; + highlightEncounterId?: string; } -export function AgendaView({ initialEvents, initialDate }: AgendaViewProps) { +export function AgendaView({ initialEvents, initialDate, highlightEncounterId }: AgendaViewProps) { const [events, setEvents] = useState(initialEvents); const [currentDate, setCurrentDate] = useState(initialDate || new Date()); const [currentView, setCurrentView] = useState('timeGridWeek'); const [isPending, startTransition] = useTransition(); + // Modal state + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalInitialDate, setModalInitialDate] = useState(); + const [modalInitialStartTime, setModalInitialStartTime] = useState(); + const [modalInitialEndTime, setModalInitialEndTime] = useState(); + const [editingEvent, setEditingEvent] = useState(); + + // Reschedule dialog state + const [pendingReschedule, setPendingReschedule] = useState(null); + const [isRescheduling, setIsRescheduling] = useState(false); + const calendarRef = useRef<{ getApi: () => { prev: () => void; next: () => void; today: () => void; changeView: (view: string) => void; getDate: () => Date } } | null>(null); + // Auto-open appointment modal when navigating from a report + useEffect(() => { + if (highlightEncounterId && initialEvents.length > 0) { + const eventToOpen = initialEvents.find((e) => e.id === highlightEncounterId); + if (eventToOpen) { + setEditingEvent(eventToOpen); + setIsModalOpen(true); + } + } + }, [highlightEncounterId, initialEvents]); + // Fetch events when date range changes const fetchEvents = useCallback(async (start: Date, end: Date) => { startTransition(async () => { @@ -75,42 +106,52 @@ export function AgendaView({ initialEvents, initialDate }: AgendaViewProps) { fetchEvents(start, end); }, [fetchEvents]); - // Handle event click + // Handle event click - open edit modal const handleEventClick = useCallback((event: CalendarEvent) => { - // TODO: Open appointment details modal - toast({ - title: `Afspraak: ${event.title}`, - description: event.extendedProps.encounter.type_display, - }); + setEditingEvent(event); + setIsModalOpen(true); }, []); // Handle date selection (for creating new appointment) const handleDateSelect = useCallback((start: Date, end: Date) => { - // TODO: Open new appointment modal with pre-filled dates - toast({ - title: 'Nieuwe afspraak', - description: `${format(start, 'HH:mm')} - ${format(end, 'HH:mm')}`, - }); + setEditingEvent(undefined); + setModalInitialDate(start); + setModalInitialStartTime(format(start, 'HH:mm')); + setModalInitialEndTime(format(end, 'HH:mm')); + setIsModalOpen(true); }, []); - // Handle event drag-and-drop - const handleEventDrop = useCallback(async ( + // Handle event drag-and-drop - show confirmation dialog + const handleEventDrop = useCallback(( eventId: string, newStart: Date, newEnd: Date | null ) => { + const event = events.find((e) => e.id === eventId); + if (event) { + setPendingReschedule({ event, newStart, newEnd }); + } + }, [events]); + + // Confirm reschedule + const confirmReschedule = useCallback(async () => { + if (!pendingReschedule) return; + + setIsRescheduling(true); + const { event, newStart, newEnd } = pendingReschedule; + const result = await rescheduleEncounter( - eventId, + event.id, newStart.toISOString(), newEnd?.toISOString() || null ); if (result.success) { toast({ title: 'Afspraak verzet' }); - // Update local state optimistically + // Update local state setEvents((prev) => prev.map((e) => - e.id === eventId + e.id === event.id ? { ...e, start: newStart, end: newEnd || undefined } : e ) @@ -126,13 +167,39 @@ export function AgendaView({ initialEvents, initialDate }: AgendaViewProps) { const end = endOfWeek(currentDate, { weekStartsOn: 1 }); fetchEvents(start, end); } + + setIsRescheduling(false); + setPendingReschedule(null); + }, [pendingReschedule, currentDate, fetchEvents]); + + // Cancel reschedule - revert the visual change + const cancelReschedule = useCallback(() => { + // Refresh events to revert the visual drag + const start = startOfWeek(currentDate, { weekStartsOn: 1 }); + const end = endOfWeek(currentDate, { weekStartsOn: 1 }); + fetchEvents(start, end); + setPendingReschedule(null); }, [currentDate, fetchEvents]); // Handle new appointment button const handleNewAppointment = useCallback(() => { - // TODO: Open new appointment modal - toast({ title: 'Nieuwe afspraak modal (nog te implementeren)' }); - }, []); + setEditingEvent(undefined); + setModalInitialDate(currentDate); + setModalInitialStartTime('09:00'); + setModalInitialEndTime('10:00'); + setIsModalOpen(true); + }, [currentDate]); + + // Handle modal success (refresh events) + const handleModalSuccess = useCallback(() => { + const start = currentView === 'timeGridDay' + ? currentDate + : startOfWeek(currentDate, { weekStartsOn: 1 }); + const end = currentView === 'timeGridDay' + ? addDays(currentDate, 1) + : endOfWeek(currentDate, { weekStartsOn: 1 }); + fetchEvents(start, end); + }, [currentDate, currentView, fetchEvents]); return (
@@ -159,6 +226,35 @@ export function AgendaView({ initialEvents, initialDate }: AgendaViewProps) { onDateChange={handleDateChange} />
+ + {/* Appointment Modal */} + + + {/* Reschedule Confirmation Dialog */} + {pendingReschedule && ( + { + if (!open) cancelReschedule(); + }} + patientName={pendingReschedule.event.title} + appointmentType={pendingReschedule.event.extendedProps.encounter.type_display} + oldStart={new Date(pendingReschedule.event.start)} + oldEnd={pendingReschedule.event.end ? new Date(pendingReschedule.event.end) : null} + newStart={pendingReschedule.newStart} + newEnd={pendingReschedule.newEnd} + onConfirm={confirmReschedule} + isLoading={isRescheduling} + /> + )} ); } diff --git a/app/epd/agenda/components/appointment-modal.tsx b/app/epd/agenda/components/appointment-modal.tsx new file mode 100644 index 0000000..3745f3b --- /dev/null +++ b/app/epd/agenda/components/appointment-modal.tsx @@ -0,0 +1,777 @@ +'use client'; + +/** + * Appointment Modal Component + * + * Modal for creating and editing appointments (encounters). + */ + +import { useState, useEffect, useCallback } from 'react'; +import { format } from 'date-fns'; +import { nl } from 'date-fns/locale'; +import { Calendar, Clock, User, MapPin, FileText, Search, X, Trash2, PenLine } from 'lucide-react'; +import Link from 'next/link'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { toast } from '@/hooks/use-toast'; + +import { createEncounter, updateEncounter, cancelEncounter, getEncounterReports } from '../actions'; +import { CancelDialog } from './cancel-dialog'; +import { + APPOINTMENT_TYPES, + LOCATION_CLASSES, + type AppointmentTypeCode, + type LocationClassCode, + type CalendarEvent, +} from '../types'; + +interface Patient { + id: string; + name_family: string; + name_given: string[]; + birth_date: string; + identifier_bsn?: string; + identifier_client_number?: string; +} + +interface LinkedReport { + id: string; + type: string; + content: string; + created_at: string; +} + +interface AppointmentModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + initialDate?: Date; + initialStartTime?: string; + initialEndTime?: string; + editingEvent?: CalendarEvent; + onSuccess?: () => void; +} + +const inputClassName = "w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent text-sm"; +const selectClassName = "w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent text-sm bg-white"; +const labelClassName = "block text-sm font-medium text-slate-700 mb-1"; + +export function AppointmentModal({ + open, + onOpenChange, + initialDate, + initialStartTime, + initialEndTime, + editingEvent, + onSuccess, +}: AppointmentModalProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [patientSearch, setPatientSearch] = useState(''); + const [patients, setPatients] = useState([]); + const [selectedPatient, setSelectedPatient] = useState(null); + const [isSearching, setIsSearching] = useState(false); + const [showPatientDropdown, setShowPatientDropdown] = useState(false); + + // Recent patients + const [recentPatients, setRecentPatients] = useState([]); + const [isInputFocused, setIsInputFocused] = useState(false); + + // Cancel dialog state + const [showCancelDialog, setShowCancelDialog] = useState(false); + const [isCancelling, setIsCancelling] = useState(false); + + // Linked reports state + const [linkedReports, setLinkedReports] = useState([]); + const [isLoadingReports, setIsLoadingReports] = useState(false); + + // Form state + const [date, setDate] = useState( + initialDate ? format(initialDate, 'yyyy-MM-dd') : format(new Date(), 'yyyy-MM-dd') + ); + const [startTime, setStartTime] = useState(initialStartTime || '09:00'); + const [endTime, setEndTime] = useState(initialEndTime || '10:00'); + const [typeCode, setTypeCode] = useState('behandeling'); + const [classCode, setClassCode] = useState('AMB'); + const [notes, setNotes] = useState(''); + + // Determine if we're in edit mode + const isEditMode = !!editingEvent; + + // Reset form when modal opens/closes + useEffect(() => { + if (open) { + if (editingEvent) { + // Edit mode: pre-fill from existing event + const encounter = editingEvent.extendedProps.encounter; + const patient = editingEvent.extendedProps.patient; + + // Set patient + if (patient) { + setSelectedPatient(patient as Patient); + setPatientSearch(`${patient.name_given?.[0] || ''} ${patient.name_family || ''}`.trim()); + } + + // Set date/time + const startDate = new Date(encounter.period_start); + setDate(format(startDate, 'yyyy-MM-dd')); + setStartTime(format(startDate, 'HH:mm')); + + if (encounter.period_end) { + const endDate = new Date(encounter.period_end); + setEndTime(format(endDate, 'HH:mm')); + } else { + setEndTime(''); + } + + // Set type and location + setTypeCode((encounter.type_code as AppointmentTypeCode) || 'behandeling'); + setClassCode((encounter.class_code as LocationClassCode) || 'AMB'); + setNotes(encounter.notes || ''); + } else { + // Create mode: use initial values + if (initialDate) { + setDate(format(initialDate, 'yyyy-MM-dd')); + } + if (initialStartTime) { + setStartTime(initialStartTime); + } + if (initialEndTime) { + setEndTime(initialEndTime); + } + } + } else { + // Reset on close + setSelectedPatient(null); + setPatientSearch(''); + setNotes(''); + setTypeCode('behandeling'); + setClassCode('AMB'); + setLinkedReports([]); + } + }, [open, initialDate, initialStartTime, initialEndTime, editingEvent]); + + // Fetch linked reports when editing an appointment + useEffect(() => { + if (open && editingEvent) { + setIsLoadingReports(true); + getEncounterReports(editingEvent.id) + .then((reports) => { + setLinkedReports(reports); + }) + .catch((error) => { + console.error('Failed to fetch linked reports:', error); + }) + .finally(() => { + setIsLoadingReports(false); + }); + } + }, [open, editingEvent]); + + // Fetch recent patients (last 5 by updated_at) + const fetchRecentPatients = useCallback(async () => { + try { + const response = await fetch('/api/fhir/Patient?_count=5'); + if (response.ok) { + const data = await response.json(); + const mappedPatients = data.entry?.map((e: { resource: unknown }) => + mapFhirPatient(e.resource as Parameters[0]) + ) || []; + setRecentPatients(mappedPatients); + } + } catch (error) { + console.error('Failed to fetch recent patients:', error); + } + }, []); + + // Fetch recent patients when modal opens (for new appointments) + useEffect(() => { + if (open && !editingEvent) { + fetchRecentPatients(); + } + }, [open, editingEvent, fetchRecentPatients]); + + // Map FHIR Patient to internal format + const mapFhirPatient = (fhirPatient: { + id: string; + name?: Array<{ family?: string; given?: string[] }>; + birthDate?: string; + identifier?: Array<{ system?: string; value?: string }>; + }): Patient => { + const bsnIdentifier = fhirPatient.identifier?.find( + (id) => id.system === 'http://fhir.nl/fhir/NamingSystem/bsn' + ); + const clientNumberIdentifier = fhirPatient.identifier?.find( + (id) => id.system?.includes('client') || id.system?.includes('999.7.6') + ); + + return { + id: fhirPatient.id, + name_family: fhirPatient.name?.[0]?.family || '', + name_given: fhirPatient.name?.[0]?.given || [], + birth_date: fhirPatient.birthDate || '', + identifier_bsn: bsnIdentifier?.value, + identifier_client_number: clientNumberIdentifier?.value, + }; + }; + + // Search patients + const searchPatients = useCallback(async (query: string) => { + if (query.length < 2) { + setPatients([]); + return; + } + + setIsSearching(true); + try { + // Use general search parameter that searches name, BSN, and client number + const response = await fetch(`/api/fhir/Patient?q=${encodeURIComponent(query)}`); + if (response.ok) { + const data = await response.json(); + const mappedPatients = data.entry?.map((e: { resource: unknown }) => + mapFhirPatient(e.resource as Parameters[0]) + ) || []; + setPatients(mappedPatients); + } + } catch (error) { + console.error('Failed to search patients:', error); + } finally { + setIsSearching(false); + } + }, []); + + // Debounced search - only search when no patient is selected + useEffect(() => { + // Skip search if patient is already selected + if (selectedPatient) { + setShowPatientDropdown(false); + return; + } + + const timer = setTimeout(() => { + if (patientSearch.length >= 2) { + searchPatients(patientSearch); + setShowPatientDropdown(true); + } else { + setPatients([]); + setShowPatientDropdown(false); + } + }, 300); + + return () => clearTimeout(timer); + }, [patientSearch, searchPatients, selectedPatient]); + + // Handle patient selection + const handleSelectPatient = (patient: Patient) => { + setSelectedPatient(patient); + setPatientSearch(`${patient.name_given?.[0] || ''} ${patient.name_family || ''}`.trim()); + setShowPatientDropdown(false); + }; + + // Handle form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!selectedPatient && !isEditMode) { + toast({ + variant: 'destructive', + title: 'Selecteer een patiënt', + description: 'Kies een patiënt uit de zoekresultaten.', + }); + return; + } + + setIsSubmitting(true); + + try { + const periodStart = `${date}T${startTime}:00`; + const periodEnd = endTime ? `${date}T${endTime}:00` : null; + + if (isEditMode && editingEvent) { + // Update existing encounter + const result = await updateEncounter(editingEvent.id, { + periodStart, + periodEnd, + typeCode, + typeDisplay: APPOINTMENT_TYPES[typeCode], + classCode, + classDisplay: LOCATION_CLASSES[classCode], + notes: notes || '', + }); + + if (result.success) { + toast({ + title: 'Afspraak bijgewerkt', + description: `${APPOINTMENT_TYPES[typeCode]} is aangepast.`, + }); + onOpenChange(false); + onSuccess?.(); + } else { + toast({ + variant: 'destructive', + title: 'Bewerken mislukt', + description: result.error, + }); + } + } else { + // Create new encounter + const result = await createEncounter({ + patientId: selectedPatient!.id, + practitionerId: '', // TODO: Get current practitioner + periodStart, + periodEnd: periodEnd || undefined, + typeCode, + typeDisplay: APPOINTMENT_TYPES[typeCode], + classCode, + classDisplay: LOCATION_CLASSES[classCode], + notes: notes || undefined, + }); + + if (result.success) { + toast({ + title: 'Afspraak aangemaakt', + description: `${APPOINTMENT_TYPES[typeCode]} met ${selectedPatient!.name_given?.[0] || ''} ${selectedPatient!.name_family || ''}`.trim(), + }); + onOpenChange(false); + onSuccess?.(); + } else { + toast({ + variant: 'destructive', + title: 'Afspraak aanmaken mislukt', + description: result.error, + }); + } + } + } catch (error) { + toast({ + variant: 'destructive', + title: 'Er ging iets mis', + description: 'Probeer het opnieuw.', + }); + } finally { + setIsSubmitting(false); + } + }; + + // Handle cancel appointment + const handleCancelAppointment = async () => { + if (!editingEvent) return; + + setIsCancelling(true); + try { + const result = await cancelEncounter(editingEvent.id); + + if (result.success) { + toast({ + title: 'Afspraak geannuleerd', + description: 'De afspraak is succesvol geannuleerd.', + }); + setShowCancelDialog(false); + onOpenChange(false); + onSuccess?.(); + } else { + toast({ + variant: 'destructive', + title: 'Annuleren mislukt', + description: result.error, + }); + } + } catch { + toast({ + variant: 'destructive', + title: 'Er ging iets mis', + description: 'Probeer het opnieuw.', + }); + } finally { + setIsCancelling(false); + } + }; + + const formatPatientName = (patient: Patient) => { + const name = `${patient.name_given?.[0] || ''} ${patient.name_family || ''}`.trim(); + const birthDate = patient.birth_date + ? format(new Date(patient.birth_date), 'd MMM yyyy', { locale: nl }) + : ''; + // Show client number or BSN (prefer client number) + const identifier = patient.identifier_client_number + ? `#${patient.identifier_client_number}` + : patient.identifier_bsn + ? `BSN ${patient.identifier_bsn.slice(-4)}` + : ''; + return { name, birthDate, identifier }; + }; + + return ( + + + + + + {editingEvent ? 'Afspraak bewerken' : 'Nieuwe Afspraak'} + + + +
+ {/* Patient Search */} +
+ +
+ + { + setPatientSearch(e.target.value); + if (selectedPatient) { + setSelectedPatient(null); + } + }} + onFocus={() => setIsInputFocused(true)} + onBlur={() => { + // Delay to allow click on dropdown items + setTimeout(() => setIsInputFocused(false), 200); + }} + placeholder="Zoek op naam, BSN of clientnummer..." + className={`${inputClassName} pl-9`} + required + /> + {selectedPatient && ( + + )} +
+ + {/* Patient Dropdown */} + {showPatientDropdown && patients.length > 0 && ( +
+ {patients.map((patient) => { + const { name, birthDate, identifier } = formatPatientName(patient); + return ( + + ); + })} +
+ )} + + {isSearching && ( +
+ Zoeken... +
+ )} + + {showPatientDropdown && patients.length === 0 && patientSearch.length >= 2 && !isSearching && ( +
+ Geen patiënten gevonden +
+ )} + + {/* Recent Patients Dropdown - shown when focused but no search query */} + {isInputFocused && !selectedPatient && patientSearch.length < 2 && recentPatients.length > 0 && !isEditMode && ( +
+
+ Recente patiënten +
+ {recentPatients.map((patient) => { + const { name, birthDate, identifier } = formatPatientName(patient); + return ( + + ); + })} +
+ )} +
+ + {/* Quick Patient Info Card - shown when patient is selected */} + {selectedPatient && ( +
+
+
+
+ {selectedPatient.name_given?.[0]} {selectedPatient.name_family} +
+
+ Geb. {selectedPatient.birth_date + ? format(new Date(selectedPatient.birth_date), 'd MMMM yyyy', { locale: nl }) + : 'Onbekend'} +
+ {selectedPatient.identifier_client_number && ( +
+ Clientnr: {selectedPatient.identifier_client_number} +
+ )} + {selectedPatient.identifier_bsn && !selectedPatient.identifier_client_number && ( +
+ BSN: ***{selectedPatient.identifier_bsn.slice(-4)} +
+ )} +
+ +
+
+ )} + + {/* Date and Time */} +
+
+ + setDate(e.target.value)} + className={inputClassName} + required + /> +
+
+ + setStartTime(e.target.value)} + className={inputClassName} + required + /> +
+
+ + setEndTime(e.target.value)} + className={inputClassName} + /> +
+
+ + {/* Type and Location */} +
+
+ + +
+
+ + +
+
+ + {/* Notes */} +
+ +