Files
triqura-ecd/app/api/fhir/Patient/route.ts
colinislit 86995a4466 feat: implement patient list and new patient form (E2.S1 & E2.S2)
Epic 2 - Cliëntenbeheer: Complete implementation of patient list with
search/filter functionality and comprehensive new patient form with
John Doe crisis admission support.

E2.S1 - Cliëntenlijst:
- Add client-side search bar with form submission
- Implement status filter dropdown (all/screening/active/finished/cancelled)
- Create StatusBadge component with color-coded status indicators
- Update table columns: Status, Name, BSN, Last Updated
- Add status filtering support to FHIR Patient API route
- Update FHIR patient transform to include episode-status extension
- Sort patients by updated_at descending (newest first)

E2.S2 - Nieuwe Cliënt Flow:
- Complete rewrite of patient form with all FO-required fields
- Implement John Doe checkbox with conditional BSN requirement
- Add BSN validation with Dutch Modulo-11 check algorithm
- Add comprehensive form fields:
  * Name fields: prefix, given name, family name
  * BSN with 9-digit pattern validation
  * Birth date with max date validation
  * Gender selection
  * Address: street, postal code, city
  * Contact: phone, email
  * Insurance: company, policy number
- Add warning messages for John Doe crisis admissions
- Set default episode status to 'planned' for all new patients
- Redirect to patient detail page after successful creation
- Update FHIR transform bidirectional insurance extension support:
  * dbPatientToFHIR: serialize insurance to JSON extension
  * fhirPatientToDB: parse insurance from JSON extension
- Pre-populate form fields when editing existing patients

Technical changes:
- app/api/fhir/Patient/route.ts: Add status query param and sorting
- app/epd/patients/actions.ts: Add status filter parameter
- app/epd/patients/page.tsx: Pass status searchParam
- app/epd/patients/components/patient-list.tsx: Complete UI rewrite
- app/epd/patients/components/patient-form.tsx: Complete form rewrite
- lib/fhir/transforms/patient.ts: Add insurance extension handling
- docs/specs/screening-intake/bouwplan-screening-intake-v1.0.md: Update status

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 13:58:31 +01:00

158 lines
3.9 KiB
TypeScript

/**
* FHIR Patient API - Collection Endpoints
* GET /api/fhir/Patient - List patients
* POST /api/fhir/Patient - Create patient
*/
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase/server';
import {
dbPatientToFHIR,
fhirPatientToDB,
createOperationOutcome,
validateFHIRResource,
} from '@/lib/fhir';
import type { FHIRBundle, FHIRPatient } from '@/lib/fhir';
/**
* GET /api/fhir/Patient
* Returns a FHIR Bundle with searchset of patients
* Supports query parameters: name, identifier, birthdate
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
// Build query
let query = supabaseAdmin.from('patients').select('*');
// Search by name (family or given)
const name = searchParams.get('name');
if (name) {
query = query.or(
`name_family.ilike.%${name}%,name_given.cs.{${name}}`
);
}
// Search by identifier (BSN)
const identifier = searchParams.get('identifier');
if (identifier) {
query = query.eq('identifier_bsn', identifier);
}
// Search by birth date
const birthdate = searchParams.get('birthdate');
if (birthdate) {
query = query.eq('birth_date', birthdate);
}
// Filter by status
const status = searchParams.get('status');
if (status && status !== 'all') {
query = query.eq('status', status);
}
// Order by updated_at descending (newest first)
query = query.order('updated_at', { ascending: false });
// Execute query
const { data: patients, error } = await query;
if (error) {
return NextResponse.json(
createOperationOutcome('error', 'processing', error.message),
{ status: 500 }
);
}
// Transform to FHIR Bundle
const bundle: FHIRBundle<FHIRPatient> = {
resourceType: 'Bundle',
type: 'searchset',
total: patients?.length || 0,
entry: patients?.map((patient) => ({
resource: dbPatientToFHIR(patient),
})) || [],
};
return NextResponse.json(bundle);
} catch (error) {
return NextResponse.json(
createOperationOutcome(
'error',
'exception',
error instanceof Error ? error.message : 'Unknown error'
),
{ status: 500 }
);
}
}
/**
* POST /api/fhir/Patient
* Creates a new patient from FHIR Patient resource
*/
export async function POST(request: NextRequest) {
try {
const fhirPatient: FHIRPatient = await request.json();
// Validate required fields
const validation = validateFHIRResource(fhirPatient, [
'resourceType',
'name',
'gender',
'birthDate',
]);
if (!validation.valid) {
return NextResponse.json(
createOperationOutcome('error', 'invalid', validation.errors.join(', ')),
{ status: 400 }
);
}
// Verify resourceType
if (fhirPatient.resourceType !== 'Patient') {
return NextResponse.json(
createOperationOutcome(
'error',
'invalid',
`Expected resourceType "Patient", got "${fhirPatient.resourceType}"`
),
{ status: 400 }
);
}
// Transform to database format
const patientInsert = fhirPatientToDB(fhirPatient);
// Insert into database
const { data: newPatient, error } = await supabaseAdmin
.from('patients')
.insert(patientInsert)
.select()
.single();
if (error) {
return NextResponse.json(
createOperationOutcome('error', 'processing', error.message),
{ status: 500 }
);
}
// Return created patient as FHIR resource
const fhirResponse = dbPatientToFHIR(newPatient);
return NextResponse.json(fhirResponse, { status: 201 });
} catch (error) {
return NextResponse.json(
createOperationOutcome(
'error',
'exception',
error instanceof Error ? error.message : 'Unknown error'
),
{ status: 500 }
);
}
}