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:
225
lib/ai/overdracht-prompt.ts
Normal file
225
lib/ai/overdracht-prompt.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Overdracht AI Prompt Templates
|
||||
*
|
||||
* System and user prompts for generating nursing handover summaries
|
||||
*/
|
||||
|
||||
import type {
|
||||
VitalSign,
|
||||
Report,
|
||||
RiskAssessment,
|
||||
Condition,
|
||||
} from '@/lib/types/overdracht';
|
||||
import type { NursingLog } from '@/lib/types/nursing-log';
|
||||
|
||||
export interface OverdrachtContext {
|
||||
patientId: string;
|
||||
patientName: string;
|
||||
age: number;
|
||||
gender: string;
|
||||
conditions: Condition[];
|
||||
vitals: VitalSign[];
|
||||
reports: Report[];
|
||||
nursingLogs: NursingLog[];
|
||||
risks: RiskAssessment[];
|
||||
}
|
||||
|
||||
export const OVERDRACHT_SYSTEM_PROMPT = `Je bent een ervaren verpleegkundige die overdrachten maakt in een GGZ-instelling.
|
||||
|
||||
Je taak: Maak een beknopte, relevante overdracht voor de opvolgende dienst.
|
||||
|
||||
## Outputformaat (JSON)
|
||||
{
|
||||
"samenvatting": "1-2 zinnen over de patiënt en wat er speelt",
|
||||
"aandachtspunten": [
|
||||
{
|
||||
"tekst": "Beschrijving van het aandachtspunt",
|
||||
"urgent": true/false,
|
||||
"bron": {
|
||||
"type": "observatie|rapportage|dagnotitie|risico",
|
||||
"id": "source-id",
|
||||
"datum": "DD-MM-YYYY HH:mm",
|
||||
"label": "Korte beschrijving bron"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actiepunten": [
|
||||
"Concrete actie voor opvolgende dienst"
|
||||
]
|
||||
}
|
||||
|
||||
## Regels
|
||||
1. Taal: Nederlands, zakelijk, beknopt
|
||||
2. Focus op: veranderingen, afwijkingen, aandachtspunten
|
||||
3. Elke aandachtspunt MOET een bronverwijzing hebben
|
||||
4. Maximum 5 aandachtspunten, 3 actiepunten
|
||||
5. Markeer urgent=true voor:
|
||||
- Medicatie-weigering
|
||||
- Incidenten
|
||||
- Sterk afwijkende vitals (HH, LL, H, L interpretatie)
|
||||
- Hoog-risico situaties (risico_level: hoog of zeer_hoog)
|
||||
6. Als er weinig data is, geef een korte samenvatting en meld dat er weinig bijzonderheden zijn
|
||||
7. Gebruik ALLEEN informatie uit de aangeleverde bronnen, verzin NIETS
|
||||
|
||||
## Bronverwijzing formaat
|
||||
- type "observatie" voor vitale functies (observations tabel)
|
||||
- type "rapportage" voor rapporten (reports tabel)
|
||||
- type "dagnotitie" voor nursing logs (nursing_logs tabel)
|
||||
- type "risico" voor risico assessments (risk_assessments tabel)
|
||||
|
||||
Geef je antwoord als PURE JSON, zonder markdown code blocks.`;
|
||||
|
||||
/**
|
||||
* Format timestamp to Dutch date format
|
||||
*/
|
||||
function formatDateTime(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('nl-NL', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interpretation label for vital signs
|
||||
*/
|
||||
function getInterpretationLabel(code: string | null | undefined): string {
|
||||
if (!code) return 'normaal';
|
||||
switch (code) {
|
||||
case 'HH':
|
||||
return 'kritiek hoog';
|
||||
case 'H':
|
||||
return 'hoog';
|
||||
case 'LL':
|
||||
return 'kritiek laag';
|
||||
case 'L':
|
||||
return 'laag';
|
||||
case 'N':
|
||||
return 'normaal';
|
||||
default:
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text to max length
|
||||
*/
|
||||
function truncate(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the user prompt with patient context
|
||||
*/
|
||||
export function buildOverdrachtUserPrompt(context: OverdrachtContext): string {
|
||||
const lines: string[] = [
|
||||
`PATIENT: ${context.patientName}, ${context.age} jaar, ${context.gender}`,
|
||||
'',
|
||||
];
|
||||
|
||||
// Diagnoses
|
||||
lines.push('DIAGNOSES:');
|
||||
if (context.conditions.length > 0) {
|
||||
for (const c of context.conditions) {
|
||||
lines.push(`- ${c.code_display} (source: conditions/${c.id})`);
|
||||
}
|
||||
} else {
|
||||
lines.push('- Geen actieve diagnoses geregistreerd');
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// Vitals
|
||||
lines.push('VITALE FUNCTIES (vandaag):');
|
||||
if (context.vitals.length > 0) {
|
||||
for (const v of context.vitals) {
|
||||
const value = v.value_quantity_value ?? 'onbekend';
|
||||
const unit = v.value_quantity_unit ?? '';
|
||||
const interpretation = getInterpretationLabel(v.interpretation_code);
|
||||
lines.push(
|
||||
`- ${v.code_display}: ${value} ${unit} [${interpretation}] ` +
|
||||
`(source: observations/${v.id}, ${formatDateTime(v.effective_datetime)})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lines.push('- Geen metingen vandaag');
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// Reports
|
||||
lines.push('RAPPORTAGES (laatste 24u):');
|
||||
if (context.reports.length > 0) {
|
||||
for (const r of context.reports) {
|
||||
lines.push(
|
||||
`- [${formatDateTime(r.created_at)}] ${r.type}: "${truncate(r.content, 200)}" ` +
|
||||
`(source: reports/${r.id})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lines.push('- Geen rapportages in de laatste 24 uur');
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// Nursing logs
|
||||
lines.push('DAGREGISTRATIES (vandaag):');
|
||||
if (context.nursingLogs.length > 0) {
|
||||
for (const l of context.nursingLogs) {
|
||||
const handoverMark = l.include_in_handover ? '[OVERDRACHT]' : '';
|
||||
lines.push(
|
||||
`- [${formatDateTime(l.timestamp)}] [${l.category.toUpperCase()}] ${handoverMark} ${l.content} ` +
|
||||
`(source: nursing_logs/${l.id})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lines.push('- Geen dagregistraties vandaag');
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// Risks
|
||||
lines.push('RISICOS:');
|
||||
if (context.risks.length > 0) {
|
||||
for (const r of context.risks) {
|
||||
const rationale = r.rationale ? `: "${truncate(r.rationale, 150)}"` : '';
|
||||
lines.push(
|
||||
`- [${r.risk_level.toUpperCase()}] ${r.risk_type}${rationale} ` +
|
||||
`(source: risk_assessments/${r.id})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lines.push('- Geen risico assessments geregistreerd');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate age from birth date
|
||||
*/
|
||||
export function calculateAge(birthDate: string): number {
|
||||
const birth = new Date(birthDate);
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birth.getFullYear();
|
||||
const monthDiff = today.getMonth() - birth.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format patient name
|
||||
*/
|
||||
export function formatPatientName(
|
||||
nameGiven: string[],
|
||||
nameFamily: string,
|
||||
namePrefix?: string
|
||||
): string {
|
||||
const given = nameGiven.join(' ');
|
||||
if (namePrefix) {
|
||||
return `${given} ${namePrefix} ${nameFamily}`;
|
||||
}
|
||||
return `${given} ${nameFamily}`;
|
||||
}
|
||||
103
lib/types/overdracht.ts
Normal file
103
lib/types/overdracht.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { z } from 'zod';
|
||||
import type { NursingLog } from './nursing-log';
|
||||
|
||||
// Patient overview for list view
|
||||
export interface PatientOverzicht {
|
||||
id: string;
|
||||
name_given: string[];
|
||||
name_family: string;
|
||||
birth_date: string;
|
||||
gender: string;
|
||||
alerts: {
|
||||
high_risk_count: number;
|
||||
abnormal_vitals_count: number;
|
||||
marked_logs_count: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PatientOverzichtResponse {
|
||||
patients: PatientOverzicht[];
|
||||
total: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
// Patient detail for detail view
|
||||
export interface PatientDetail {
|
||||
patient: {
|
||||
id: string;
|
||||
name_given: string[];
|
||||
name_family: string;
|
||||
name_prefix?: string;
|
||||
birth_date: string;
|
||||
gender: string;
|
||||
};
|
||||
vitals: VitalSign[];
|
||||
reports: Report[];
|
||||
nursingLogs: NursingLog[];
|
||||
risks: RiskAssessment[];
|
||||
conditions: Condition[];
|
||||
}
|
||||
|
||||
// Vital sign with interpretation
|
||||
export interface VitalSign {
|
||||
id: string;
|
||||
code_display: string;
|
||||
value_quantity_value: number | null;
|
||||
value_quantity_unit: string | null;
|
||||
interpretation_code?: string | null; // 'H' | 'L' | 'N'
|
||||
effective_datetime: string;
|
||||
}
|
||||
|
||||
// Report (rapportage)
|
||||
export interface Report {
|
||||
id: string;
|
||||
type: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
// Risk assessment
|
||||
export interface RiskAssessment {
|
||||
id: string;
|
||||
risk_type: string;
|
||||
risk_level: string;
|
||||
rationale?: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
// Condition (diagnose)
|
||||
export interface Condition {
|
||||
id: string;
|
||||
code_display: string;
|
||||
clinical_status: string;
|
||||
onset_datetime?: string;
|
||||
}
|
||||
|
||||
// AI Summary types
|
||||
export interface AISamenvatting {
|
||||
samenvatting: string;
|
||||
aandachtspunten: Aandachtspunt[];
|
||||
actiepunten: string[];
|
||||
generatedAt: string;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface Aandachtspunt {
|
||||
tekst: string;
|
||||
urgent: boolean;
|
||||
bron: {
|
||||
type: 'observatie' | 'rapportage' | 'dagnotitie' | 'risico';
|
||||
id: string;
|
||||
datum: string;
|
||||
label: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Zod schema for generate request
|
||||
export const GenerateOverdrachtSchema = z.object({
|
||||
patientId: z.string().uuid('Patient ID moet een geldige UUID zijn'),
|
||||
});
|
||||
|
||||
export type GenerateOverdrachtInput = z.infer<typeof GenerateOverdrachtSchema>;
|
||||
Reference in New Issue
Block a user