Files
triqura-ecd/app/api/patients/search/route.ts
colinislit 43d4d095ce fix: case-insensitive voornaam zoeken door capitalize toe te passen
Voornaam zoeken werkte niet omdat name_given array case-sensitive is.
Capitalize nu de zoekterm (eerste letter hoofdletter) voordat deze
wordt gebruikt in de array search, zodat 'jan' matcht met 'Jan'.

Aangepast in beide search endpoints:
- /api/patients/search
- /api/cortex/patients/search
2026-01-03 11:47:12 +01:00

203 lines
5.6 KiB
TypeScript

/**
* 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 {
// Capitalize first letter, lowercase rest (matches Dutch name format)
const capitalizedQuery = searchQuery.charAt(0).toUpperCase() +
searchQuery.slice(1).toLowerCase();
// Search name fields (family name and given names)
// name_family uses ilike (case-insensitive), name_given uses cs with capitalized query
dbQuery = dbQuery.or(
`name_family.ilike.%${searchQuery}%,name_given.cs.{${capitalizedQuery}}`
);
}
// 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 }
);
}
}