feat(overdracht): E2 + E3 - API overdracht en dagregistratie UI
Epic 2 - API Overdracht: - GET /api/overdracht/patients (patiënten met alerts) - GET /api/overdracht/[patientId] (detail met vitals, reports, logs, risks) - POST /api/overdracht/generate (AI samenvatting met bronverwijzingen) - lib/ai/overdracht-prompt.ts (system + user prompt) - lib/types/overdracht.ts (TypeScript types) Epic 3 - Dagregistratie UI: - /epd/dagregistratie/[patientId] pagina - Quick entry form (categorie, tijd, tekst, overdracht checkbox) - Edit/Delete functionaliteit met inline editing - Confirm delete dialog - Summary cards (totaal, overdracht, incidenten) Bouwplan bijgewerkt naar v1.2 (10/19 stories voltooid) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
181
app/api/overdracht/[patientId]/route.ts
Normal file
181
app/api/overdracht/[patientId]/route.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import type {
|
||||
PatientDetail,
|
||||
VitalSign,
|
||||
Report,
|
||||
RiskAssessment,
|
||||
Condition,
|
||||
} from '@/lib/types/overdracht';
|
||||
import type { NursingLog } from '@/lib/types/nursing-log';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ patientId: string }>;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { patientId } = await params;
|
||||
|
||||
if (!z.string().uuid().safeParse(patientId).success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'patientId moet een geldige UUID zijn' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
// Date calculations
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayStart = `${today}T00:00:00.000Z`;
|
||||
const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
// Parallel queries for all data
|
||||
const [
|
||||
patientResult,
|
||||
vitalsResult,
|
||||
reportsResult,
|
||||
logsResult,
|
||||
risksResult,
|
||||
conditionsResult,
|
||||
] = await Promise.all([
|
||||
// 1. Patient info
|
||||
supabase
|
||||
.from('patients')
|
||||
.select('id, name_given, name_family, name_prefix, birth_date, gender')
|
||||
.eq('id', patientId)
|
||||
.single(),
|
||||
|
||||
// 2. Vitals today
|
||||
supabase
|
||||
.from('observations')
|
||||
.select('id, code_display, value_quantity_value, value_quantity_unit, interpretation_code, effective_datetime')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('category', 'vital-signs')
|
||||
.gte('effective_datetime', todayStart)
|
||||
.order('effective_datetime', { ascending: false }),
|
||||
|
||||
// 3. Reports last 24h
|
||||
supabase
|
||||
.from('reports')
|
||||
.select('id, type, content, created_at, created_by')
|
||||
.eq('patient_id', patientId)
|
||||
.gte('created_at', last24h)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: false }),
|
||||
|
||||
// 4. Nursing logs today (all, not just marked)
|
||||
supabase
|
||||
.from('nursing_logs')
|
||||
.select('*')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('shift_date', today)
|
||||
.order('timestamp', { ascending: false }),
|
||||
|
||||
// 5. Risks via intakes
|
||||
supabase
|
||||
.from('risk_assessments')
|
||||
.select('id, risk_type, risk_level, rationale, created_at, intakes!inner(patient_id)')
|
||||
.eq('intakes.patient_id', patientId)
|
||||
.in('risk_level', ['laag', 'gemiddeld', 'hoog', 'zeer_hoog']),
|
||||
|
||||
// 6. Active conditions
|
||||
supabase
|
||||
.from('conditions')
|
||||
.select('id, code_display, clinical_status, onset_datetime')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('clinical_status', 'active'),
|
||||
]);
|
||||
|
||||
// Check if patient exists
|
||||
if (patientResult.error || !patientResult.data) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Patiënt niet gevonden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Handle errors for other queries gracefully (return empty arrays)
|
||||
if (vitalsResult.error) {
|
||||
console.error('Error fetching vitals:', vitalsResult.error);
|
||||
}
|
||||
if (reportsResult.error) {
|
||||
console.error('Error fetching reports:', reportsResult.error);
|
||||
}
|
||||
if (logsResult.error) {
|
||||
console.error('Error fetching nursing logs:', logsResult.error);
|
||||
}
|
||||
if (risksResult.error) {
|
||||
console.error('Error fetching risks:', risksResult.error);
|
||||
}
|
||||
if (conditionsResult.error) {
|
||||
console.error('Error fetching conditions:', conditionsResult.error);
|
||||
}
|
||||
|
||||
// Map vitals
|
||||
const vitals: VitalSign[] = (vitalsResult.data || []).map((v) => ({
|
||||
id: v.id,
|
||||
code_display: v.code_display,
|
||||
value_quantity_value: v.value_quantity_value,
|
||||
value_quantity_unit: v.value_quantity_unit,
|
||||
interpretation_code: v.interpretation_code,
|
||||
effective_datetime: v.effective_datetime,
|
||||
}));
|
||||
|
||||
// Map reports
|
||||
const reports: Report[] = (reportsResult.data || []).map((r) => ({
|
||||
id: r.id,
|
||||
type: r.type,
|
||||
content: r.content,
|
||||
created_at: r.created_at,
|
||||
created_by: r.created_by,
|
||||
}));
|
||||
|
||||
// Nursing logs (already typed correctly from database)
|
||||
const nursingLogs: NursingLog[] = logsResult.data || [];
|
||||
|
||||
// Map risks (remove the intakes join data)
|
||||
const risks: RiskAssessment[] = (risksResult.data || []).map((r) => ({
|
||||
id: r.id,
|
||||
risk_type: r.risk_type,
|
||||
risk_level: r.risk_level,
|
||||
rationale: r.rationale,
|
||||
created_at: r.created_at,
|
||||
}));
|
||||
|
||||
// Map conditions
|
||||
const conditions: Condition[] = (conditionsResult.data || []).map((c) => ({
|
||||
id: c.id,
|
||||
code_display: c.code_display,
|
||||
clinical_status: c.clinical_status,
|
||||
onset_datetime: c.onset_datetime || undefined,
|
||||
}));
|
||||
|
||||
// Build response
|
||||
const response: PatientDetail = {
|
||||
patient: {
|
||||
id: patientResult.data.id,
|
||||
name_given: patientResult.data.name_given,
|
||||
name_family: patientResult.data.name_family,
|
||||
name_prefix: patientResult.data.name_prefix || undefined,
|
||||
birth_date: patientResult.data.birth_date,
|
||||
gender: patientResult.data.gender,
|
||||
},
|
||||
vitals,
|
||||
reports,
|
||||
nursingLogs,
|
||||
risks,
|
||||
conditions,
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in GET /api/overdracht/[patientId]:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Onverwachte serverfout' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
329
app/api/overdracht/generate/route.ts
Normal file
329
app/api/overdracht/generate/route.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* Overdracht Generate API
|
||||
*
|
||||
* POST /api/overdracht/generate
|
||||
* Genereert een overdracht samenvatting met Claude AI
|
||||
*/
|
||||
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
OVERDRACHT_SYSTEM_PROMPT,
|
||||
buildOverdrachtUserPrompt,
|
||||
calculateAge,
|
||||
formatPatientName,
|
||||
type OverdrachtContext,
|
||||
} from '@/lib/ai/overdracht-prompt';
|
||||
import {
|
||||
GenerateOverdrachtSchema,
|
||||
type AISamenvatting,
|
||||
type Aandachtspunt,
|
||||
} from '@/lib/types/overdracht';
|
||||
import type { NursingLog } from '@/lib/types/nursing-log';
|
||||
|
||||
// Zod schema for AI response validation
|
||||
const AandachtspuntSchema = z.object({
|
||||
tekst: z.string(),
|
||||
urgent: z.boolean(),
|
||||
bron: z.object({
|
||||
type: z.enum(['observatie', 'rapportage', 'dagnotitie', 'risico']),
|
||||
id: z.string(),
|
||||
datum: z.string(),
|
||||
label: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const AIResponseSchema = z.object({
|
||||
samenvatting: z.string(),
|
||||
aandachtspunten: z.array(AandachtspuntSchema).max(5),
|
||||
actiepunten: z.array(z.string()).max(3),
|
||||
});
|
||||
|
||||
/**
|
||||
* Load context from database
|
||||
*/
|
||||
async function loadOverdrachtContext(
|
||||
supabase: Awaited<ReturnType<typeof createClient>>,
|
||||
patientId: string
|
||||
): Promise<OverdrachtContext> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayStart = `${today}T00:00:00.000Z`;
|
||||
const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
// Parallel queries
|
||||
const [
|
||||
patientResult,
|
||||
vitalsResult,
|
||||
reportsResult,
|
||||
logsResult,
|
||||
risksResult,
|
||||
conditionsResult,
|
||||
] = await Promise.all([
|
||||
supabase
|
||||
.from('patients')
|
||||
.select('id, name_given, name_family, name_prefix, birth_date, gender')
|
||||
.eq('id', patientId)
|
||||
.single(),
|
||||
supabase
|
||||
.from('observations')
|
||||
.select('id, code_display, value_quantity_value, value_quantity_unit, interpretation_code, effective_datetime')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('category', 'vital-signs')
|
||||
.gte('effective_datetime', todayStart)
|
||||
.order('effective_datetime', { ascending: false }),
|
||||
supabase
|
||||
.from('reports')
|
||||
.select('id, type, content, created_at, created_by')
|
||||
.eq('patient_id', patientId)
|
||||
.gte('created_at', last24h)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: false }),
|
||||
supabase
|
||||
.from('nursing_logs')
|
||||
.select('*')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('shift_date', today)
|
||||
.order('timestamp', { ascending: false }),
|
||||
supabase
|
||||
.from('risk_assessments')
|
||||
.select('id, risk_type, risk_level, rationale, created_at, intakes!inner(patient_id)')
|
||||
.eq('intakes.patient_id', patientId),
|
||||
supabase
|
||||
.from('conditions')
|
||||
.select('id, code_display, clinical_status, onset_datetime')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('clinical_status', 'active'),
|
||||
]);
|
||||
|
||||
if (patientResult.error || !patientResult.data) {
|
||||
throw new Error('Patiënt niet gevonden');
|
||||
}
|
||||
|
||||
const patient = patientResult.data;
|
||||
|
||||
return {
|
||||
patientId,
|
||||
patientName: formatPatientName(
|
||||
patient.name_given,
|
||||
patient.name_family,
|
||||
patient.name_prefix || undefined
|
||||
),
|
||||
age: calculateAge(patient.birth_date),
|
||||
gender: patient.gender,
|
||||
conditions: (conditionsResult.data || []).map((c) => ({
|
||||
id: c.id,
|
||||
code_display: c.code_display,
|
||||
clinical_status: c.clinical_status,
|
||||
onset_datetime: c.onset_datetime || undefined,
|
||||
})),
|
||||
vitals: (vitalsResult.data || []).map((v) => ({
|
||||
id: v.id,
|
||||
code_display: v.code_display,
|
||||
value_quantity_value: v.value_quantity_value,
|
||||
value_quantity_unit: v.value_quantity_unit,
|
||||
interpretation_code: v.interpretation_code,
|
||||
effective_datetime: v.effective_datetime,
|
||||
})),
|
||||
reports: (reportsResult.data || []).map((r) => ({
|
||||
id: r.id,
|
||||
type: r.type,
|
||||
content: r.content,
|
||||
created_at: r.created_at,
|
||||
created_by: r.created_by,
|
||||
})),
|
||||
nursingLogs: (logsResult.data || []) as NursingLog[],
|
||||
risks: (risksResult.data || []).map((r) => ({
|
||||
id: r.id,
|
||||
risk_type: r.risk_type,
|
||||
risk_level: r.risk_level,
|
||||
rationale: r.rationale,
|
||||
created_at: r.created_at,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Claude API
|
||||
*/
|
||||
async function callClaudeAPI(context: OverdrachtContext): Promise<{
|
||||
samenvatting: string;
|
||||
aandachtspunten: Aandachtspunt[];
|
||||
actiepunten: string[];
|
||||
}> {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('ANTHROPIC_API_KEY ontbreekt in environment');
|
||||
}
|
||||
|
||||
const userPrompt = buildOverdrachtUserPrompt(context);
|
||||
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 2048,
|
||||
temperature: 0.3,
|
||||
system: OVERDRACHT_SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
console.error('Claude API error:', errorBody);
|
||||
throw new Error(`Claude API fout: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const rawText = data?.content?.[0]?.text;
|
||||
|
||||
if (!rawText) {
|
||||
throw new Error('Geen response van Claude API');
|
||||
}
|
||||
|
||||
// Parse JSON from response (handle potential markdown code blocks)
|
||||
let jsonText = rawText.trim();
|
||||
if (jsonText.startsWith('```json')) {
|
||||
jsonText = jsonText.slice(7);
|
||||
}
|
||||
if (jsonText.startsWith('```')) {
|
||||
jsonText = jsonText.slice(3);
|
||||
}
|
||||
if (jsonText.endsWith('```')) {
|
||||
jsonText = jsonText.slice(0, -3);
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonText.trim());
|
||||
|
||||
// Validate with Zod schema
|
||||
const validated = AIResponseSchema.parse(parsed);
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log AI event to database
|
||||
*/
|
||||
async function logAIEvent(
|
||||
supabase: Awaited<ReturnType<typeof createClient>>,
|
||||
patientId: string,
|
||||
context: OverdrachtContext,
|
||||
result: { samenvatting: string; aandachtspunten: Aandachtspunt[]; actiepunten: string[] },
|
||||
durationMs: number
|
||||
) {
|
||||
try {
|
||||
await supabase.from('ai_events').insert({
|
||||
kind: 'overdracht_generate',
|
||||
patient_id: patientId,
|
||||
input_data: {
|
||||
vitalCount: context.vitals.length,
|
||||
reportCount: context.reports.length,
|
||||
logCount: context.nursingLogs.length,
|
||||
riskCount: context.risks.length,
|
||||
conditionCount: context.conditions.length,
|
||||
},
|
||||
output_data: {
|
||||
aandachtspuntenCount: result.aandachtspunten.length,
|
||||
actiepuntenCount: result.actiepunten.length,
|
||||
urgentCount: result.aandachtspunten.filter((a) => a.urgent).length,
|
||||
},
|
||||
duration_ms: durationMs,
|
||||
});
|
||||
} catch (error) {
|
||||
// Log but don't fail the request
|
||||
console.error('Failed to log AI event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/overdracht/generate
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input
|
||||
const result = GenerateOverdrachtSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Validatiefout',
|
||||
details: result.error.issues.map((e) => ({
|
||||
field: e.path.join('.'),
|
||||
message: e.message,
|
||||
})),
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { patientId } = result.data;
|
||||
const supabase = await createClient();
|
||||
|
||||
// Check auth
|
||||
const { data: authData } = await supabase.auth.getUser();
|
||||
if (!authData?.user) {
|
||||
return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Load context
|
||||
const context = await loadOverdrachtContext(supabase, patientId);
|
||||
|
||||
// Call Claude API
|
||||
const aiResult = await callClaudeAPI(context);
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
|
||||
// Log AI event
|
||||
await logAIEvent(supabase, patientId, context, aiResult, durationMs);
|
||||
|
||||
// Build response
|
||||
const response: AISamenvatting = {
|
||||
samenvatting: aiResult.samenvatting,
|
||||
aandachtspunten: aiResult.aandachtspunten,
|
||||
actiepunten: aiResult.actiepunten,
|
||||
generatedAt: new Date().toISOString(),
|
||||
durationMs,
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error generating overdracht:', error);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Onbekende fout';
|
||||
|
||||
if (errorMessage.includes('Patiënt niet gevonden')) {
|
||||
return NextResponse.json({ error: errorMessage }, { status: 404 });
|
||||
}
|
||||
|
||||
if (errorMessage.includes('ANTHROPIC_API_KEY')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI service niet geconfigureerd' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Claude API')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI service tijdelijk niet beschikbaar', details: errorMessage },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Fout bij genereren overdracht',
|
||||
details: process.env.NODE_ENV === 'development' ? errorMessage : undefined,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
206
app/api/overdracht/patients/route.ts
Normal file
206
app/api/overdracht/patients/route.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import type { PatientOverzicht, PatientOverzichtResponse } from '@/lib/types/overdracht';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const dateParam = searchParams.get('date');
|
||||
|
||||
// Use provided date or today
|
||||
const targetDate = dateParam || new Date().toISOString().split('T')[0];
|
||||
|
||||
// Validate date format
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(targetDate)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'date moet in YYYY-MM-DD formaat zijn' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get start and end of day for date filtering
|
||||
const dayStart = `${targetDate}T00:00:00.000Z`;
|
||||
const dayEnd = `${targetDate}T23:59:59.999Z`;
|
||||
|
||||
// 1. Get patients with encounters today
|
||||
const { data: encounterData, error: encounterError } = await supabase
|
||||
.from('encounters')
|
||||
.select(`
|
||||
patient_id,
|
||||
patients!inner (
|
||||
id,
|
||||
name_given,
|
||||
name_family,
|
||||
birth_date,
|
||||
gender
|
||||
)
|
||||
`)
|
||||
.gte('period_start', dayStart)
|
||||
.lte('period_start', dayEnd)
|
||||
.in('status', ['planned', 'in-progress', 'completed']);
|
||||
|
||||
if (encounterError) {
|
||||
console.error('Error fetching encounters:', encounterError);
|
||||
return NextResponse.json(
|
||||
{ error: 'Fout bij ophalen patiënten', details: encounterError.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Deduplicate patients (one patient may have multiple encounters)
|
||||
const patientMap = new Map<string, {
|
||||
id: string;
|
||||
name_given: string[];
|
||||
name_family: string;
|
||||
birth_date: string;
|
||||
gender: string;
|
||||
}>();
|
||||
|
||||
for (const encounter of encounterData || []) {
|
||||
const patient = encounter.patients as unknown as {
|
||||
id: string;
|
||||
name_given: string[];
|
||||
name_family: string;
|
||||
birth_date: string;
|
||||
gender: string;
|
||||
};
|
||||
if (patient && !patientMap.has(patient.id)) {
|
||||
patientMap.set(patient.id, patient);
|
||||
}
|
||||
}
|
||||
|
||||
const patientIds = Array.from(patientMap.keys());
|
||||
|
||||
if (patientIds.length === 0) {
|
||||
const response: PatientOverzichtResponse = {
|
||||
patients: [],
|
||||
total: 0,
|
||||
date: targetDate,
|
||||
};
|
||||
return NextResponse.json(response);
|
||||
}
|
||||
|
||||
// 2. Get alert counts in parallel
|
||||
const [
|
||||
{ data: risksData },
|
||||
{ data: vitalsData },
|
||||
{ data: logsData },
|
||||
] = await Promise.all([
|
||||
// High risk assessments (via intakes)
|
||||
supabase
|
||||
.from('risk_assessments')
|
||||
.select('id, intakes!inner(patient_id)')
|
||||
.in('intakes.patient_id', patientIds)
|
||||
.in('risk_level', ['hoog', 'zeer_hoog']),
|
||||
|
||||
// Abnormal vitals today
|
||||
supabase
|
||||
.from('observations')
|
||||
.select('id, patient_id, interpretation_code')
|
||||
.in('patient_id', patientIds)
|
||||
.eq('category', 'vital-signs')
|
||||
.gte('effective_datetime', dayStart)
|
||||
.lte('effective_datetime', dayEnd)
|
||||
.in('interpretation_code', ['H', 'L', 'HH', 'LL']),
|
||||
|
||||
// Marked nursing logs for handover
|
||||
supabase
|
||||
.from('nursing_logs')
|
||||
.select('id, patient_id')
|
||||
.in('patient_id', patientIds)
|
||||
.eq('shift_date', targetDate)
|
||||
.eq('include_in_handover', true),
|
||||
]);
|
||||
|
||||
// Count alerts per patient
|
||||
const alertCounts = new Map<string, {
|
||||
high_risk_count: number;
|
||||
abnormal_vitals_count: number;
|
||||
marked_logs_count: number;
|
||||
}>();
|
||||
|
||||
// Initialize all patients with zero counts
|
||||
for (const patientId of patientIds) {
|
||||
alertCounts.set(patientId, {
|
||||
high_risk_count: 0,
|
||||
abnormal_vitals_count: 0,
|
||||
marked_logs_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Count high risks
|
||||
for (const risk of risksData || []) {
|
||||
const intake = risk.intakes as unknown as { patient_id: string };
|
||||
if (intake?.patient_id) {
|
||||
const counts = alertCounts.get(intake.patient_id);
|
||||
if (counts) counts.high_risk_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count abnormal vitals
|
||||
for (const vital of vitalsData || []) {
|
||||
if (vital.patient_id) {
|
||||
const counts = alertCounts.get(vital.patient_id);
|
||||
if (counts) counts.abnormal_vitals_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count marked logs
|
||||
for (const log of logsData || []) {
|
||||
if (log.patient_id) {
|
||||
const counts = alertCounts.get(log.patient_id);
|
||||
if (counts) counts.marked_logs_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Build response
|
||||
const patients: PatientOverzicht[] = Array.from(patientMap.values()).map(
|
||||
(patient) => {
|
||||
const alerts = alertCounts.get(patient.id) || {
|
||||
high_risk_count: 0,
|
||||
abnormal_vitals_count: 0,
|
||||
marked_logs_count: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
id: patient.id,
|
||||
name_given: patient.name_given,
|
||||
name_family: patient.name_family,
|
||||
birth_date: patient.birth_date,
|
||||
gender: patient.gender,
|
||||
alerts: {
|
||||
...alerts,
|
||||
total:
|
||||
alerts.high_risk_count +
|
||||
alerts.abnormal_vitals_count +
|
||||
alerts.marked_logs_count,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Sort by total alerts (descending), then by name
|
||||
patients.sort((a, b) => {
|
||||
if (b.alerts.total !== a.alerts.total) {
|
||||
return b.alerts.total - a.alerts.total;
|
||||
}
|
||||
return a.name_family.localeCompare(b.name_family);
|
||||
});
|
||||
|
||||
const response: PatientOverzichtResponse = {
|
||||
patients,
|
||||
total: patients.length,
|
||||
date: targetDate,
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in GET /api/overdracht/patients:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Onverwachte serverfout' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user