feat(verpleegrapportage): Nieuwe module + opruiming codebase

Verpleegrapportage module:
- Nieuwe /epd/verpleegrapportage met patiëntenoverzicht
- Rapportage invoer workspace met timeline view
- Overdracht overzicht met AI-samenvatting
- API endpoints voor verpleegrapportage data

Opruiming:
- Oude /epd/overdracht en /epd/dagregistratie verwijderd (vervangen)
- Oude /api/nursing-logs verwijderd (geconsolideerd naar reports)
- Verouderde design docs en reports verwijderd
- Fonts verplaatst van docs/ naar public/fonts/

Bugfixes:
- Risk-manager: fix constraint violation (db values vs display labels)
- Overdracht API: filter op rapportages i.p.v. encounters

Database:
- Migratie voor consolidatie nursing_logs naar reports tabel

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
colinislit
2025-12-08 11:19:48 +01:00
parent d9340ba296
commit 75c7e284b9
222 changed files with 5473 additions and 7494 deletions

View File

@@ -10,7 +10,7 @@ import type {
RiskAssessment,
Condition,
} from '@/lib/types/overdracht';
import type { NursingLog } from '@/lib/types/nursing-log';
import { getVerpleegkundigCategory, CATEGORY_CONFIG } from '@/lib/types/report';
export interface OverdrachtContext {
patientId: string;
@@ -20,7 +20,6 @@ export interface OverdrachtContext {
conditions: Condition[];
vitals: VitalSign[];
reports: Report[];
nursingLogs: NursingLog[];
risks: RiskAssessment[];
}
@@ -36,7 +35,7 @@ Je taak: Maak een beknopte, relevante overdracht voor de opvolgende dienst.
"tekst": "Beschrijving van het aandachtspunt",
"urgent": true/false,
"bron": {
"type": "observatie|rapportage|dagnotitie|risico",
"type": "observatie|rapportage|verpleegkundig|risico",
"id": "source-id",
"datum": "DD-MM-YYYY HH:mm",
"label": "Korte beschrijving bron"
@@ -63,8 +62,8 @@ Je taak: Maak een beknopte, relevante overdracht voor de opvolgende dienst.
## Bronverwijzing formaat
- type "observatie" voor vitale functies (observations tabel)
- type "rapportage" voor rapporten (reports tabel)
- type "dagnotitie" voor nursing logs (nursing_logs tabel)
- type "rapportage" voor rapporten (reports met type != verpleegkundig)
- type "verpleegkundig" voor verpleegkundige notities (reports met type = verpleegkundig)
- type "risico" voor risico assessments (risk_assessments tabel)
Geef je antwoord als PURE JSON, zonder markdown code blocks.`;
@@ -149,10 +148,14 @@ export function buildOverdrachtUserPrompt(context: OverdrachtContext): string {
}
lines.push('');
// Reports
// Split reports into regular rapportages and verpleegkundige notities
const regularReports = context.reports.filter((r) => r.type !== 'verpleegkundig');
const verpleegkundigReports = context.reports.filter((r) => r.type === 'verpleegkundig');
// Regular reports
lines.push('RAPPORTAGES (laatste 24u):');
if (context.reports.length > 0) {
for (const r of context.reports) {
if (regularReports.length > 0) {
for (const r of regularReports) {
lines.push(
`- [${formatDateTime(r.created_at)}] ${r.type}: "${truncate(r.content, 200)}" ` +
`(source: reports/${r.id})`
@@ -163,18 +166,20 @@ export function buildOverdrachtUserPrompt(context: OverdrachtContext): string {
}
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]' : '';
// Verpleegkundige notities (was nursing logs)
lines.push('VERPLEEGKUNDIGE NOTITIES (vandaag):');
if (verpleegkundigReports.length > 0) {
for (const r of verpleegkundigReports) {
const category = getVerpleegkundigCategory(r.structured_data);
const categoryLabel = category ? CATEGORY_CONFIG[category]?.label : 'Notitie';
const handoverMark = r.include_in_handover ? '[OVERDRACHT]' : '';
lines.push(
`- [${formatDateTime(l.timestamp)}] [${l.category.toUpperCase()}] ${handoverMark} ${l.content} ` +
`(source: nursing_logs/${l.id})`
`- [${formatDateTime(r.created_at)}] [${categoryLabel.toUpperCase()}] ${handoverMark} ${r.content} ` +
`(source: reports/${r.id})`
);
}
} else {
lines.push('- Geen dagregistraties vandaag');
lines.push('- Geen verpleegkundige notities vandaag');
}
lines.push('');

View File

@@ -783,53 +783,6 @@ export type Database = {
}
Relationships: []
}
nursing_logs: {
Row: {
category: string
content: string
created_at: string
created_by: string
id: string
include_in_handover: boolean
patient_id: string
shift_date: string
timestamp: string
updated_at: string
}
Insert: {
category: string
content: string
created_at?: string
created_by: string
id?: string
include_in_handover?: boolean
patient_id: string
shift_date: string
timestamp?: string
updated_at?: string
}
Update: {
category?: string
content?: string
created_at?: string
created_by?: string
id?: string
include_in_handover?: boolean
patient_id?: string
shift_date?: string
timestamp?: string
updated_at?: string
}
Relationships: [
{
foreignKeyName: "nursing_logs_patient_id_fkey"
columns: ["patient_id"]
isOneToOne: false
referencedRelation: "patients"
referencedColumns: ["id"]
},
]
}
observations: {
Row: {
body_site: string | null
@@ -1209,9 +1162,11 @@ export type Database = {
deleted_at: string | null
encounter_id: string | null
id: string
include_in_handover: boolean | null
intake_id: string | null
parent_report_id: string | null
patient_id: string
shift_date: string | null
structured_data: Json | null
type: string
updated_at: string | null
@@ -1229,9 +1184,11 @@ export type Database = {
deleted_at?: string | null
encounter_id?: string | null
id?: string
include_in_handover?: boolean | null
intake_id?: string | null
parent_report_id?: string | null
patient_id: string
shift_date?: string | null
structured_data?: Json | null
type: string
updated_at?: string | null
@@ -1249,9 +1206,11 @@ export type Database = {
deleted_at?: string | null
encounter_id?: string | null
id?: string
include_in_handover?: boolean | null
intake_id?: string | null
parent_report_id?: string | null
patient_id?: string
shift_date?: string | null
structured_data?: Json | null
type?: string
updated_at?: string | null

View File

@@ -1,114 +0,0 @@
import { z } from 'zod';
import type { Database } from '@/lib/supabase/database.types';
// Database types
export type NursingLog = Database['public']['Tables']['nursing_logs']['Row'];
export type NursingLogInsert = Database['public']['Tables']['nursing_logs']['Insert'];
export type NursingLogUpdate = Database['public']['Tables']['nursing_logs']['Update'];
// Category enum
export const NURSING_LOG_CATEGORIES = [
'medicatie',
'adl',
'gedrag',
'incident',
'observatie',
] as const;
export type NursingLogCategory = (typeof NURSING_LOG_CATEGORIES)[number];
// Zod schemas for validation
export const NursingLogCategorySchema = z.enum(NURSING_LOG_CATEGORIES);
export const CreateNursingLogSchema = z.object({
patient_id: z.string().uuid('Patient ID moet een geldige UUID zijn'),
category: NursingLogCategorySchema,
content: z
.string()
.min(1, 'Notitie mag niet leeg zijn')
.max(500, 'Notitie mag maximaal 500 karakters bevatten'),
timestamp: z.string().datetime().optional(),
include_in_handover: z.boolean().default(false),
});
export type CreateNursingLogInput = z.infer<typeof CreateNursingLogSchema>;
export const UpdateNursingLogSchema = z.object({
category: NursingLogCategorySchema.optional(),
content: z
.string()
.min(1, 'Notitie mag niet leeg zijn')
.max(500, 'Notitie mag maximaal 500 karakters bevatten')
.optional(),
timestamp: z.string().datetime().optional(),
include_in_handover: z.boolean().optional(),
});
export type UpdateNursingLogInput = z.infer<typeof UpdateNursingLogSchema>;
// Response types
export interface NursingLogListResponse {
logs: NursingLog[];
total: number;
}
// Category display configuration
export const CATEGORY_CONFIG: Record<
NursingLogCategory,
{
label: string;
icon: string;
color: string;
bgColor: string;
textColor: string;
}
> = {
medicatie: {
label: 'Medicatie',
icon: 'Pill',
color: 'blue',
bgColor: 'bg-blue-100',
textColor: 'text-blue-700',
},
adl: {
label: 'ADL/verzorging',
icon: 'Utensils',
color: 'green',
bgColor: 'bg-green-100',
textColor: 'text-green-700',
},
gedrag: {
label: 'Gedragsobservatie',
icon: 'User',
color: 'purple',
bgColor: 'bg-purple-100',
textColor: 'text-purple-700',
},
incident: {
label: 'Incident',
icon: 'AlertTriangle',
color: 'red',
bgColor: 'bg-red-100',
textColor: 'text-red-700',
},
observatie: {
label: 'Algemene observatie',
icon: 'FileText',
color: 'gray',
bgColor: 'bg-gray-100',
textColor: 'text-gray-700',
},
};
// Helper function to calculate shift_date from timestamp
export function calculateShiftDate(timestamp: Date | string): string {
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const hours = date.getHours();
// Night shift (before 7:00) belongs to previous day
if (hours < 7) {
date.setDate(date.getDate() - 1);
}
return date.toISOString().split('T')[0];
}

View File

@@ -1,5 +1,5 @@
import { z } from 'zod';
import type { NursingLog } from './nursing-log';
import type { Json } from '@/lib/supabase/database.types';
// Patient overview for list view
export interface PatientOverzicht {
@@ -34,7 +34,6 @@ export interface PatientDetail {
};
vitals: VitalSign[];
reports: Report[];
nursingLogs: NursingLog[];
risks: RiskAssessment[];
conditions: Condition[];
}
@@ -49,13 +48,16 @@ export interface VitalSign {
effective_datetime: string;
}
// Report (rapportage)
// Report (rapportage) - now includes verpleegkundig type (was nursing_logs)
export interface Report {
id: string;
type: string;
content: string;
created_at: string;
created_by: string | null;
structured_data?: Json | null; // Contains category for verpleegkundig
include_in_handover?: boolean | null;
shift_date?: string | null;
}
// Risk assessment
@@ -88,7 +90,7 @@ export interface Aandachtspunt {
tekst: string;
urgent: boolean;
bron: {
type: 'observatie' | 'rapportage' | 'dagnotitie' | 'risico';
type: 'observatie' | 'rapportage' | 'verpleegkundig' | 'risico';
id: string;
datum: string;
label: string;
@@ -98,6 +100,7 @@ export interface Aandachtspunt {
// Zod schema for generate request
export const GenerateOverdrachtSchema = z.object({
patientId: z.string().uuid('Patient ID moet een geldige UUID zijn'),
period: z.enum(['1d', '3d', '7d', '14d']).optional().default('1d'),
});
export type GenerateOverdrachtInput = z.infer<typeof GenerateOverdrachtSchema>;

View File

@@ -15,9 +15,79 @@ export const REPORT_TYPES = [
'intake',
'behandeladvies',
'vrije_notitie',
'verpleegkundig', // Korte verpleegkundige notities (was nursing_logs)
] as const;
export type ReportType = (typeof REPORT_TYPES)[number];
// Types die getoond worden in het verpleegkundig overzicht
export const VERPLEEG_REPORT_TYPES = [
'verpleegkundig',
'observatie',
'incident',
'medicatie',
'crisis',
] as const;
export type VerpleegReportType = (typeof VERPLEEG_REPORT_TYPES)[number];
// Categorieën voor verpleegkundige notities (in structured_data.category)
export const VERPLEEGKUNDIG_CATEGORIES = [
'medicatie',
'adl',
'gedrag',
'incident',
'observatie',
] as const;
export type VerpleegkundigCategory = (typeof VERPLEEGKUNDIG_CATEGORIES)[number];
// Category display configuration voor verpleegkundige notities
export const CATEGORY_CONFIG: Record<
VerpleegkundigCategory,
{
label: string;
icon: string;
color: string;
bgColor: string;
textColor: string;
}
> = {
medicatie: {
label: 'Medicatie',
icon: 'Pill',
color: 'blue',
bgColor: 'bg-blue-100',
textColor: 'text-blue-700',
},
adl: {
label: 'ADL/verzorging',
icon: 'Utensils',
color: 'green',
bgColor: 'bg-green-100',
textColor: 'text-green-700',
},
gedrag: {
label: 'Gedragsobservatie',
icon: 'User',
color: 'purple',
bgColor: 'bg-purple-100',
textColor: 'text-purple-700',
},
incident: {
label: 'Incident',
icon: 'AlertTriangle',
color: 'red',
bgColor: 'bg-red-100',
textColor: 'text-red-700',
},
observatie: {
label: 'Algemene observatie',
icon: 'FileText',
color: 'gray',
bgColor: 'bg-gray-100',
textColor: 'text-gray-700',
},
};
// Schema voor standaard rapportages (20-5000 karakters)
export const CreateReportSchema = z.object({
patient_id: z.string().uuid('Patient ID moet een geldige UUID zijn'),
type: z.enum(REPORT_TYPES),
@@ -33,6 +103,33 @@ export const CreateReportSchema = z.object({
export type CreateReportInput = z.infer<typeof CreateReportSchema>;
// Schema voor verpleegkundige notities (1-500 karakters, met category)
export const CreateVerpleegkundigSchema = z.object({
patient_id: z.string().uuid('Patient ID moet een geldige UUID zijn'),
type: z.literal('verpleegkundig'),
content: z
.string()
.min(1, 'Notitie mag niet leeg zijn')
.max(500, 'Notitie mag maximaal 500 karakters bevatten'),
category: z.enum(VERPLEEGKUNDIG_CATEGORIES),
include_in_handover: z.boolean().default(false),
});
export type CreateVerpleegkundigInput = z.infer<typeof CreateVerpleegkundigSchema>;
// Update schema voor verpleegkundige notities
export const UpdateVerpleegkundigSchema = z.object({
content: z
.string()
.min(1, 'Notitie mag niet leeg zijn')
.max(500, 'Notitie mag maximaal 500 karakters bevatten')
.optional(),
category: z.enum(VERPLEEGKUNDIG_CATEGORIES).optional(),
include_in_handover: z.boolean().optional(),
});
export type UpdateVerpleegkundigInput = z.infer<typeof UpdateVerpleegkundigSchema>;
export interface ClassificationResult {
type: ReportType;
confidence: number;
@@ -43,3 +140,29 @@ export interface ReportListResponse {
reports: Report[];
total: number;
}
// Helper function to calculate shift_date from timestamp
// Night shift (before 7:00) belongs to previous day
export function calculateShiftDate(timestamp: Date | string): string {
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const hours = date.getHours();
// Night shift (before 7:00) belongs to previous day
if (hours < 7) {
date.setDate(date.getDate() - 1);
}
return date.toISOString().split('T')[0];
}
// Helper to extract category from structured_data
export function getVerpleegkundigCategory(
structuredData: Report['structured_data'] | undefined | null
): VerpleegkundigCategory | null {
if (!structuredData || typeof structuredData !== 'object') return null;
const data = structuredData as { category?: string };
if (data.category && VERPLEEGKUNDIG_CATEGORIES.includes(data.category as VerpleegkundigCategory)) {
return data.category as VerpleegkundigCategory;
}
return null;
}