git commit -m "feat(swift): implement Epic 3 - P1 Blocks (E3.S0-S6)" -m "Epic 3 compleet: Alle P1 blocks geïmplementeerd met volledige functionaliteit." -m "E3.S0 - CanvasArea block rendering" -m "- Switch/case voor block types met prefill data" -m "- AnimatePresence voor smooth transitions" -m "- PatientContextCard auto-display wanneer activePatient is gezet" -m "" -m "E3.S1 - Block Container animaties" -m "- Framer Motion animaties voor container, content en close button" -m "- Stagger effecten voor content children" -m "- Hover/tap animaties voor close button" -m "" -m "E3.S2 - DagnotatieBlock" -m "- Patient search met debounced FHIR API integratie" -m "- Category selector (medicatie, adl, gedrag, incident, observatie)" -m "- Textarea met character counter (max 500)" -m "- Opslaan naar /api/reports met validatie en error handling" -m "" -m "E3.S3 - Patient search API" -m "- GET /api/patients/search?q= met fuzzy search" -m "- Match score berekening voor resultaten" -m "- Auth check en error handling" -m "" -m "E3.S4 - ZoekenBlock" -m "- Debounced patient search met dropdown" -m "- Patient selectie → setActivePatient in store" -m "- Auto-close na selectie + recent action" -m "" -m "E3.S5 - PatientContextCard" -m "- Auto-open na patient selectie" -m "- Notities, vitals, diagnoses en risico's secties" -m "- API integratie met /api/overdracht/[patientId]" -m "" -m "E3.S6 - OverdrachtBlock" -m "- Lijst van patiënten met activiteit" -m "- Period selector (1d, 3d, 7d, 14d)" -m "- AI samenvatting generatie per patiënt" -m "- Aandachtspunten en actiepunten weergave" -m "" -m "Technische verbeteringen:" -m "- Technische debt opgelost: BlockContainer animaties, CanvasArea rendering" -m "- PatientContextCard toegevoegd aan blocks" -m "- Alle blocks gebruiken BlockContainer voor consistente styling" -m "" -m "Voortgang: 56 SP / 72 SP (78%) - Epic 3 compleet"

This commit is contained in:
ikbenlit
2025-12-24 09:06:39 +01:00
parent 3e94271827
commit 2a67a0a2d9
10 changed files with 1800 additions and 87 deletions

View File

@@ -0,0 +1,197 @@
/**
* Patient Search API
*
* GET /api/patients/search?q=query&limit=5
*
* Fuzzy search voor patiënten met eenvoudige JSON response.
* Gebruikt voor Swift blocks en andere client-side componenten.
*/
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/auth/server';
interface PatientSearchResult {
id: string;
name: string;
birthDate: string;
identifier_bsn?: string;
identifier_client_number?: string;
matchScore: number;
}
interface SearchResponse {
patients: PatientSearchResult[];
totalCount: number;
}
/**
* Calculate simple match score based on query match
* - Exact match: 1.0
* - Starts with: 0.9
* - Contains: 0.7
* - Partial match: 0.5
*/
function calculateMatchScore(
query: string,
patient: {
name_family?: string | null;
name_given?: string[] | null;
identifier_bsn?: string | null;
identifier_client_number?: string | null;
}
): number {
const normalizedQuery = query.toLowerCase().trim();
const fullName = `${patient.name_given?.join(' ') || ''} ${patient.name_family || ''}`
.toLowerCase()
.trim();
// Exact match on full name
if (fullName === normalizedQuery) {
return 1.0;
}
// Starts with query
if (fullName.startsWith(normalizedQuery)) {
return 0.9;
}
// Contains query
if (fullName.includes(normalizedQuery)) {
return 0.7;
}
// Check individual name parts
const nameParts = fullName.split(' ');
const queryParts = normalizedQuery.split(' ');
let matchedParts = 0;
for (const queryPart of queryParts) {
if (nameParts.some((part) => part.startsWith(queryPart) || part.includes(queryPart))) {
matchedParts++;
}
}
if (matchedParts > 0) {
return 0.5 + (matchedParts / queryParts.length) * 0.2;
}
// Check BSN match
if (patient.identifier_bsn && patient.identifier_bsn.includes(normalizedQuery)) {
return 0.8;
}
// Check client number match
if (
patient.identifier_client_number &&
patient.identifier_client_number.toLowerCase().includes(normalizedQuery)
) {
return 0.8;
}
return 0.3; // Default low score for any match
}
export async function GET(request: NextRequest) {
try {
// Check authentication
const supabase = await createClient();
const { data: authData } = await supabase.auth.getUser();
if (!authData?.user) {
return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
const limitParam = searchParams.get('limit');
const limit = limitParam ? Math.min(Math.max(parseInt(limitParam, 10), 1), 50) : 10;
if (!query || query.trim().length < 2) {
return NextResponse.json<SearchResponse>(
{
patients: [],
totalCount: 0,
},
{ status: 200 }
);
}
const searchQuery = query.trim();
// Build Supabase query
let dbQuery = supabase.from('patients').select('id, name_family, name_given, birth_date, identifier_bsn, identifier_client_number');
// Check if input looks like a number (BSN or client number)
const isNumeric = /^\d+$/.test(searchQuery);
if (isNumeric) {
// Search BSN and client number
dbQuery = dbQuery.or(
`identifier_bsn.ilike.%${searchQuery}%,identifier_client_number.ilike.%${searchQuery}%`
);
} else {
// Search name fields (family name and given names)
dbQuery = dbQuery.or(
`name_family.ilike.%${searchQuery}%,name_given.cs.{${searchQuery}}`
);
}
// Order by updated_at descending (newest first)
dbQuery = dbQuery.order('updated_at', { ascending: false }).limit(limit * 2); // Get more to calculate scores
// Execute query
const { data: patients, error } = await dbQuery;
if (error) {
console.error('Error searching patients:', error);
return NextResponse.json(
{ error: 'Zoeken mislukt', details: error.message },
{ status: 500 }
);
}
if (!patients || patients.length === 0) {
return NextResponse.json<SearchResponse>(
{
patients: [],
totalCount: 0,
},
{ status: 200 }
);
}
// Calculate match scores and format results
const results: PatientSearchResult[] = patients
.map((patient) => {
const fullName = `${patient.name_given?.join(' ') || ''} ${patient.name_family || ''}`.trim();
const matchScore = calculateMatchScore(searchQuery, patient);
return {
id: patient.id,
name: fullName || 'Naamloos',
birthDate: patient.birth_date || '',
identifier_bsn: patient.identifier_bsn || undefined,
identifier_client_number: patient.identifier_client_number || undefined,
matchScore,
};
})
.sort((a, b) => b.matchScore - a.matchScore) // Sort by match score descending
.slice(0, limit); // Limit results
const response: SearchResponse = {
patients: results,
totalCount: results.length,
};
return NextResponse.json(response, { status: 200 });
} catch (error) {
console.error('Unexpected error in patient search:', error);
return NextResponse.json(
{
error: 'Onverwachte fout',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}