DRY refactor: Extract reusable patient search logic from ZoekenBlock. New files: - lib/cortex/hooks/use-patient-search.ts (116 lines) Debounced patient search with abort controller - lib/cortex/hooks/use-patient-selection.ts (98 lines) FHIR fetch + store update flow - lib/fhir/patient-mapper.ts (109 lines) FHIR Patient to DB Patient mapping + utilities - components/cortex/shared/patient-list-item.tsx (114 lines) Reusable patient list item with loading states Refactored: - ZoekenBlock: 349 -> 127 lines (-64%) Documentation: - docs/intent/patient-search/ux-analyse-patient-selectie.md - docs/intent/patient-search/bouwplan-patient-selectie-v1.md CLAUDE.md updated with Cortex architecture documentation. Epic 0 complete: 4/4 stories (4 SP) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* usePatientSelection Hook
|
|
*
|
|
* Handles patient selection flow: fetch FHIR data, map to DB format, update store.
|
|
* Extracted from ZoekenBlock for DRY compliance.
|
|
*
|
|
* Epic: E0.S4 (Patient Selectie - Refactor)
|
|
*/
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { useCortexStore } from '@/stores/cortex-store';
|
|
import { safeFetch, getErrorInfo } from '@/lib/cortex/error-handler';
|
|
import { mapFhirToDbPatient, type Patient } from '@/lib/fhir/patient-mapper';
|
|
import type { PatientSearchResult } from './use-patient-search';
|
|
|
|
interface UsePatientSelectionOptions {
|
|
/** Callback after successful selection */
|
|
onSuccess?: (patient: Patient, patientName: string) => void;
|
|
/** Callback on error */
|
|
onError?: (error: Error) => void;
|
|
/** Add to recent actions (default: true) */
|
|
trackRecentAction?: boolean;
|
|
/** Show toast on success (default: true) */
|
|
showSuccessToast?: boolean;
|
|
}
|
|
|
|
interface UsePatientSelectionReturn {
|
|
/** Select a patient by search result */
|
|
selectPatient: (patient: PatientSearchResult) => Promise<Patient | null>;
|
|
/** Whether a selection is in progress */
|
|
isSelecting: boolean;
|
|
/** ID of the patient currently being selected */
|
|
selectedId: string | null;
|
|
/** Clear selection state */
|
|
clearSelection: () => void;
|
|
}
|
|
|
|
export function usePatientSelection(
|
|
options: UsePatientSelectionOptions = {}
|
|
): UsePatientSelectionReturn {
|
|
const {
|
|
onSuccess,
|
|
onError,
|
|
trackRecentAction = true,
|
|
showSuccessToast = true,
|
|
} = options;
|
|
|
|
const [isSelecting, setIsSelecting] = useState(false);
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const { toast } = useToast();
|
|
|
|
const { setActivePatient, addRecentAction } = useCortexStore();
|
|
|
|
const selectPatient = useCallback(
|
|
async (patient: PatientSearchResult): Promise<Patient | null> => {
|
|
setIsSelecting(true);
|
|
setSelectedId(patient.id);
|
|
|
|
try {
|
|
// Fetch full FHIR patient data
|
|
const response = await safeFetch(
|
|
`/api/fhir/Patient/${patient.id}`,
|
|
undefined,
|
|
{ operation: 'Patient data ophalen' }
|
|
);
|
|
const fhirPatient = await response.json();
|
|
|
|
// Map to DB format
|
|
const dbPatient = mapFhirToDbPatient(fhirPatient);
|
|
|
|
// Update store
|
|
setActivePatient(dbPatient);
|
|
|
|
// Track recent action
|
|
if (trackRecentAction) {
|
|
addRecentAction({
|
|
intent: 'zoeken',
|
|
label: `Patient geselecteerd: ${patient.name}`,
|
|
patientName: patient.name,
|
|
});
|
|
}
|
|
|
|
// Show success toast
|
|
if (showSuccessToast) {
|
|
toast({
|
|
title: 'Patient geselecteerd',
|
|
description: `${patient.name} is nu actief`,
|
|
});
|
|
}
|
|
|
|
// Call success callback
|
|
onSuccess?.(dbPatient, patient.name);
|
|
|
|
return dbPatient;
|
|
} catch (error) {
|
|
console.error('Failed to select patient:', error);
|
|
|
|
const errorInfo = getErrorInfo(error, { operation: 'Patient selecteren' });
|
|
toast({
|
|
variant: 'destructive',
|
|
title: errorInfo.title,
|
|
description: errorInfo.description,
|
|
});
|
|
|
|
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
|
|
return null;
|
|
} finally {
|
|
setIsSelecting(false);
|
|
setSelectedId(null);
|
|
}
|
|
},
|
|
[setActivePatient, addRecentAction, toast, trackRecentAction, showSuccessToast, onSuccess, onError]
|
|
);
|
|
|
|
const clearSelection = useCallback(() => {
|
|
setIsSelecting(false);
|
|
setSelectedId(null);
|
|
}, []);
|
|
|
|
return {
|
|
selectPatient,
|
|
isSelecting,
|
|
selectedId,
|
|
clearSelection,
|
|
};
|
|
}
|