FHIR, API, datamodel

This commit is contained in:
colinislit
2025-11-21 22:32:29 +01:00
parent 5f7ef0801d
commit d5c6cf208b
39 changed files with 17974 additions and 1004 deletions

View File

@@ -0,0 +1,254 @@
'use client';
/**
* Patient Form Component (FHIR-based)
* Form for creating/editing patients using FHIR format
*/
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Save, Loader2 } from 'lucide-react';
import { createPatient, updatePatient } from '../actions';
import type { FHIRPatient } from '@/lib/fhir';
interface PatientFormProps {
patient?: FHIRPatient;
}
export function PatientForm({ patient }: PatientFormProps) {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const existingName = patient?.name?.[0];
const existingBsn = patient?.identifier?.find(
(id) => id.system === 'http://fhir.nl/fhir/NamingSystem/bsn'
)?.value;
const existingPhone = patient?.telecom?.find((t) => t.system === 'phone')?.value;
const existingEmail = patient?.telecom?.find((t) => t.system === 'email')?.value;
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setIsSubmitting(true);
setError(null);
try {
const formData = new FormData(e.currentTarget);
// Build FHIR Patient resource
const fhirPatient: FHIRPatient = {
resourceType: 'Patient',
identifier: [
{
system: 'http://fhir.nl/fhir/NamingSystem/bsn',
value: formData.get('bsn') as string,
use: 'official' as const,
},
],
name: [
{
use: 'official' as const,
family: formData.get('family') as string,
given: [formData.get('given') as string].filter(Boolean),
prefix: formData.get('prefix')
? [(formData.get('prefix') as string)]
: undefined,
},
],
gender: formData.get('gender') as 'male' | 'female' | 'other' | 'unknown',
birthDate: formData.get('birthDate') as string,
telecom: [
formData.get('phone')
? {
system: 'phone' as const,
value: formData.get('phone') as string,
use: 'mobile' as const,
}
: undefined,
formData.get('email')
? {
system: 'email' as const,
value: formData.get('email') as string,
}
: undefined,
].filter((t): t is NonNullable<typeof t> => t !== undefined),
active: true,
};
if (patient?.id) {
// Update existing patient
await updatePatient(patient.id, fhirPatient);
} else {
// Create new patient
await createPatient(fhirPatient);
}
router.push('/epd/patients');
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : 'Er is een fout opgetreden');
setIsSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
{/* Name Fields */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label htmlFor="prefix" className="block text-sm font-medium text-slate-700 mb-1">
Voorvoegsel
</label>
<input
type="text"
id="prefix"
name="prefix"
defaultValue={existingName?.prefix?.[0] || ''}
placeholder="Dhr./Mevr."
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="given" className="block text-sm font-medium text-slate-700 mb-1">
Voornaam *
</label>
<input
type="text"
id="given"
name="given"
defaultValue={existingName?.given?.[0] || ''}
required
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="family" className="block text-sm font-medium text-slate-700 mb-1">
Achternaam *
</label>
<input
type="text"
id="family"
name="family"
defaultValue={existingName?.family || ''}
required
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
</div>
{/* BSN and Birth Date */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="bsn" className="block text-sm font-medium text-slate-700 mb-1">
BSN *
</label>
<input
type="text"
id="bsn"
name="bsn"
defaultValue={existingBsn || ''}
required
placeholder="123456789"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="birthDate" className="block text-sm font-medium text-slate-700 mb-1">
Geboortedatum *
</label>
<input
type="date"
id="birthDate"
name="birthDate"
defaultValue={patient?.birthDate || ''}
required
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
</div>
{/* Gender */}
<div>
<label htmlFor="gender" className="block text-sm font-medium text-slate-700 mb-1">
Geslacht *
</label>
<select
id="gender"
name="gender"
defaultValue={patient?.gender || 'unknown'}
required
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
>
<option value="male">Man</option>
<option value="female">Vrouw</option>
<option value="other">Anders</option>
<option value="unknown">Onbekend</option>
</select>
</div>
{/* Contact Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="phone" className="block text-sm font-medium text-slate-700 mb-1">
Telefoonnummer
</label>
<input
type="tel"
id="phone"
name="phone"
defaultValue={existingPhone || ''}
placeholder="+31612345678"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-1">
E-mail
</label>
<input
type="email"
id="email"
name="email"
defaultValue={existingEmail || ''}
placeholder="patient@example.com"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-4 pt-4 border-t border-slate-200">
<button
type="submit"
disabled={isSubmitting}
className="inline-flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-teal-600 to-teal-700 hover:from-teal-700 hover:to-teal-800 text-white font-medium rounded-lg shadow-sm hover:shadow-md transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<span>Opslaan...</span>
</>
) : (
<>
<Save className="h-4 w-4" />
<span>{patient ? 'Bijwerken' : 'Aanmaken'}</span>
</>
)}
</button>
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 text-slate-700 hover:text-slate-900 font-medium transition-colors"
>
Annuleren
</button>
</div>
</form>
);
}

View File

@@ -0,0 +1,142 @@
'use client';
/**
* Patient List Component (FHIR-based)
* Displays patients from FHIR API
*/
import { useState } from 'react';
import Link from 'next/link';
import { User, Calendar, Phone, Mail } from 'lucide-react';
import type { FHIRPatient } from '@/lib/fhir';
interface PatientListProps {
initialPatients: FHIRPatient[];
}
export function PatientList({ initialPatients }: PatientListProps) {
const [patients] = useState<FHIRPatient[]>(initialPatients);
if (patients.length === 0) {
return (
<div className="text-center py-12 bg-white rounded-lg border border-slate-200">
<User className="mx-auto h-12 w-12 text-slate-400" />
<h3 className="mt-4 text-lg font-medium text-slate-900">Geen patiënten gevonden</h3>
<p className="mt-2 text-sm text-slate-600">
Begin met het toevoegen van een nieuwe patiënt.
</p>
</div>
);
}
return (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Patiënt
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
BSN
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Geboortedatum
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Contact
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Geslacht
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-slate-200">
{patients.map((patient) => {
const name = patient.name?.[0];
const fullName = [
...(name?.prefix || []),
...(name?.given || []),
name?.family,
]
.filter(Boolean)
.join(' ');
const bsn = patient.identifier?.find(
(id) => id.system === 'http://fhir.nl/fhir/NamingSystem/bsn'
)?.value;
const phone = patient.telecom?.find((t) => t.system === 'phone')?.value;
const email = patient.telecom?.find((t) => t.system === 'email')?.value;
return (
<tr
key={patient.id}
className="hover:bg-slate-50 transition-colors cursor-pointer"
>
<td className="px-6 py-4 whitespace-nowrap">
<Link
href={`/epd/patients/${patient.id}`}
className="flex items-center group"
>
<div className="flex-shrink-0 h-10 w-10 bg-gradient-to-br from-teal-400 to-teal-600 rounded-full flex items-center justify-center">
<span className="text-white font-semibold text-sm">
{name?.given?.[0]?.[0]}
{name?.family?.[0]}
</span>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-slate-900 group-hover:text-teal-600 transition-colors">
{fullName}
</div>
<div className="text-sm text-slate-500">ID: {patient.id}</div>
</div>
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-slate-900">{bsn || '-'}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-slate-900">
<Calendar className="h-4 w-4 text-slate-400 mr-2" />
{patient.birthDate || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
{phone && (
<div className="flex items-center text-sm text-slate-900">
<Phone className="h-4 w-4 text-slate-400 mr-2" />
{phone}
</div>
)}
{email && (
<div className="flex items-center text-sm text-slate-900">
<Mail className="h-4 w-4 text-slate-400 mr-2" />
{email}
</div>
)}
{!phone && !email && (
<span className="text-sm text-slate-400">-</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm text-slate-900 capitalize">
{patient.gender === 'male' && 'Man'}
{patient.gender === 'female' && 'Vrouw'}
{patient.gender === 'other' && 'Anders'}
{patient.gender === 'unknown' && 'Onbekend'}
{!patient.gender && '-'}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}