'use client'; /** * Overdracht Block * * Block voor het genereren van overdracht samenvattingen per patiënt. * E3.S6: Volledige implementatie met AI samenvatting per patiënt. */ import { useState, useEffect, useCallback } from 'react'; import { useToast } from '@/hooks/use-toast'; import { useSwiftStore } from '@/stores/swift-store'; import { BlockContainer } from './block-container'; import type { BlockPrefillData } from '@/stores/swift-store'; import { BLOCK_CONFIGS } from '@/lib/swift/types'; import type { PatientOverzicht, AISamenvatting } from '@/lib/types/overdracht'; import { Sparkles, Loader2, AlertTriangle, CheckCircle2, Clock, RefreshCw, Calendar, Users, } from 'lucide-react'; import { format } from 'date-fns'; import { nl } from 'date-fns/locale/nl'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; interface OverdrachtBlockProps { prefill?: BlockPrefillData; } type PeriodValue = '1d' | '3d' | '7d' | '14d'; const PERIOD_OPTIONS: { value: PeriodValue; label: string; description: string }[] = [ { value: '1d', label: 'Vandaag', description: 'Laatste 24 uur' }, { value: '3d', label: '3 dagen', description: 'Afgelopen 3 dagen' }, { value: '7d', label: '1 week', description: 'Afgelopen 7 dagen' }, { value: '14d', label: '2 weken', description: 'Afgelopen 14 dagen' }, ]; interface PatientSummary { patient: PatientOverzicht; summary: AISamenvatting | null; loading: boolean; error: string | null; } export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) { const config = BLOCK_CONFIGS.overdracht; const { activePatient } = useSwiftStore(); const { toast } = useToast(); const [period, setPeriod] = useState('1d'); const [patients, setPatients] = useState([]); const [isLoadingPatients, setIsLoadingPatients] = useState(true); const [patientSummaries, setPatientSummaries] = useState>(new Map()); // Load patients list useEffect(() => { const fetchPatients = async () => { setIsLoadingPatients(true); try { const response = await fetch('/api/overdracht/patients'); if (!response.ok) { throw new Error('Kon patiëntenlijst niet laden'); } const data = await response.json(); setPatients(data.patients || []); // Initialize summaries map const summaries = new Map(); (data.patients || []).forEach((patient: PatientOverzicht) => { summaries.set(patient.id, { patient, summary: null, loading: false, error: null, }); }); setPatientSummaries(summaries); } catch (error) { console.error('Failed to fetch patients:', error); toast({ variant: 'destructive', title: 'Laden mislukt', description: error instanceof Error ? error.message : 'Kon patiëntenlijst niet laden', }); } finally { setIsLoadingPatients(false); } }; fetchPatients(); }, [toast]); // Auto-generate summary for activePatient if set (only once when block opens) useEffect(() => { if (activePatient && patients.length > 0 && patientSummaries.size > 0) { const summaryData = patientSummaries.get(activePatient.id); // Only auto-generate if no summary exists and not already loading if (summaryData && !summaryData.summary && !summaryData.loading) { generateSummary(activePatient.id); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activePatient?.id, patients.length, patientSummaries.size]); // Generate summary for a patient const generateSummary = useCallback(async (patientId: string) => { setPatientSummaries((prev) => { const updated = new Map(prev); const existing = updated.get(patientId); if (existing) { updated.set(patientId, { ...existing, loading: true, error: null, }); } return updated; }); try { const response = await fetch('/api/overdracht/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patientId, period }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Genereren mislukt' })); throw new Error(errorData.error || 'Genereren mislukt'); } const summary: AISamenvatting = await response.json(); setPatientSummaries((prev) => { const updated = new Map(prev); const existing = updated.get(patientId); if (existing) { updated.set(patientId, { ...existing, summary, loading: false, error: null, }); } return updated; }); } catch (error) { console.error('Failed to generate summary:', error); setPatientSummaries((prev) => { const updated = new Map(prev); const existing = updated.get(patientId); if (existing) { updated.set(patientId, { ...existing, loading: false, error: error instanceof Error ? error.message : 'Onbekende fout', }); } return updated; }); } }, [period]); // Filter patients: if activePatient is set, only show that one const displayPatients = activePatient ? patients.filter((p) => p.id === activePatient.id) : patients; const formatPatientName = (patient: PatientOverzicht): string => { return `${patient.name_given?.join(' ') || ''} ${patient.name_family || ''}`.trim(); }; const formatDuration = (ms: number): string => { if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; }; const formatTime = (datetime: string): string => { return format(new Date(datetime), 'HH:mm', { locale: nl }); }; return (
{/* Period Selector */}
{PERIOD_OPTIONS.map((option) => ( ))}

{PERIOD_OPTIONS.find((o) => o.value === period)?.description}

{/* Patients List */} {isLoadingPatients ? (
Patiënten laden...
) : displayPatients.length === 0 ? (

Geen patiënten gevonden voor deze periode

) : (
{displayPatients.map((patient) => { const summaryData = patientSummaries.get(patient.id); const summary = summaryData?.summary; const loading = summaryData?.loading || false; const error = summaryData?.error; return (
{/* Patient Header */}

{formatPatientName(patient)}

{patient.alerts.total > 0 && (
{patient.alerts.total} alert{patient.alerts.total > 1 ? 's' : ''} {patient.alerts.high_risk_count > 0 && ( {patient.alerts.high_risk_count} risico )}
)}
{!summary && !loading && ( )}
{/* Loading State */} {loading && (

Samenvatting wordt gegenereerd...

)} {/* Error State */} {error && (

{error}

)} {/* Summary Content */} {summary && (
{/* Samenvatting */}

Samenvatting

{summary.samenvatting}

{/* Aandachtspunten */} {summary.aandachtspunten.length > 0 && (

Aandachtspunten ({summary.aandachtspunten.length})

{summary.aandachtspunten.map((punt, index) => ( ))}
)} {/* Actiepunten */} {summary.actiepunten.length > 0 && (

Actiepunten ({summary.actiepunten.length})

    {summary.actiepunten.map((actie, index) => (
  • {actie}
  • ))}
)} {/* Footer */}
{formatTime(summary.generatedAt)} ({formatDuration(summary.durationMs)})
)}
); })}
)}
); } function AandachtspuntItem({ punt }: { punt: AISamenvatting['aandachtspunten'][0] }) { const getBronTypeLabel = (type: string): string => { const labels: Record = { observatie: 'Vitale functie', rapportage: 'Rapportage', verpleegkundig: 'Verpleegkundig', risico: 'Risicobeoordeling', }; return labels[type] || type; }; const getBronTypeStyle = (type: string): { bg: string; text: string } => { switch (type) { case 'observatie': return { bg: 'bg-teal-900/30', text: 'text-teal-300' }; case 'rapportage': return { bg: 'bg-indigo-900/30', text: 'text-indigo-300' }; case 'verpleegkundig': return { bg: 'bg-amber-900/30', text: 'text-amber-300' }; case 'risico': return { bg: 'bg-red-900/30', text: 'text-red-300' }; default: return { bg: 'bg-slate-800', text: 'text-slate-400' }; } }; const bronStyle = getBronTypeStyle(punt.bron.type); return (
{punt.urgent && }

{punt.tekst}

{getBronTypeLabel(punt.bron.type)} {punt.bron.label} • {punt.bron.datum}
); }