feat(overdracht): Incident badges + expandable rapportage content

Incident telling:
- Nieuwe incident_count in PatientOverzicht alerts
- Telt type='incident', 'crisis' én verpleegkundig category='incident'
- Oranje badge in patiëntenlijst
- "Met alerts" filter includeert nu ook incidenten

Expandable content:
- Rapportages in timeline tonen nu volledige tekst
- "Lees meer" / "Minder tonen" toggle voor berichten >5 regels
- Behoudt whitespace/regeleinden in tekst

Opruiming:
- Oude screenshots verwijderd

🤖 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 14:26:07 +01:00
parent 75c7e284b9
commit 593bef4dcf
15 changed files with 145 additions and 13 deletions

View File

@@ -87,6 +87,7 @@ export async function GET(request: NextRequest) {
{ data: risksData },
{ data: vitalsData },
{ data: logsData },
{ data: incidentReportsData },
] = await Promise.all([
// High risk assessments (via intakes)
supabase
@@ -114,6 +115,15 @@ export async function GET(request: NextRequest) {
.eq('shift_date', targetDate)
.eq('include_in_handover', true)
.is('deleted_at', null),
// Incident reports today (type='incident' OR verpleegkundig with category='incident')
supabase
.from('reports')
.select('id, patient_id, type, structured_data')
.in('patient_id', patientIds)
.gte('created_at', dayStart)
.lte('created_at', dayEnd)
.is('deleted_at', null),
]);
// Count alerts per patient
@@ -121,6 +131,7 @@ export async function GET(request: NextRequest) {
high_risk_count: number;
abnormal_vitals_count: number;
marked_logs_count: number;
incident_count: number;
}>();
// Initialize all patients with zero counts
@@ -129,6 +140,7 @@ export async function GET(request: NextRequest) {
high_risk_count: 0,
abnormal_vitals_count: 0,
marked_logs_count: 0,
incident_count: 0,
});
}
@@ -157,6 +169,27 @@ export async function GET(request: NextRequest) {
}
}
// Count incidents (type='incident' OR type='crisis' OR verpleegkundig with category='incident')
for (const report of incidentReportsData || []) {
if (!report.patient_id) continue;
const counts = alertCounts.get(report.patient_id);
if (!counts) continue;
// Direct incident/crisis type
if (report.type === 'incident' || report.type === 'crisis') {
counts.incident_count++;
continue;
}
// Verpleegkundig report with incident category
if (report.type === 'verpleegkundig' && report.structured_data) {
const data = report.structured_data as { category?: string };
if (data.category === 'incident') {
counts.incident_count++;
}
}
}
// 3. Build response
const patients: PatientOverzicht[] = Array.from(patientMap.values()).map(
(patient) => {
@@ -164,6 +197,7 @@ export async function GET(request: NextRequest) {
high_risk_count: 0,
abnormal_vitals_count: 0,
marked_logs_count: 0,
incident_count: 0,
};
return {
@@ -177,7 +211,8 @@ export async function GET(request: NextRequest) {
total:
alerts.high_risk_count +
alerts.abnormal_vitals_count +
alerts.marked_logs_count,
alerts.marked_logs_count +
alerts.incident_count,
},
};
}

View File

@@ -135,6 +135,7 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
{ data: risksData },
{ data: vitalsData },
{ data: logsData },
{ data: incidentReportsData },
] = await Promise.all([
// High risk assessments (via intakes) - these are not time-bound
supabase
@@ -163,6 +164,14 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
.lte('shift_date', targetDate)
.eq('include_in_handover', true)
.is('deleted_at', null),
// Incident reports in period (type='incident' OR verpleegkundig with category='incident')
supabase
.from('reports')
.select('id, patient_id, type, structured_data')
.in('patient_id', patientIds)
.gte('created_at', periodStartISO)
.is('deleted_at', null),
]);
// Count alerts per patient
@@ -170,6 +179,7 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
high_risk_count: number;
abnormal_vitals_count: number;
marked_logs_count: number;
incident_count: number;
}>();
// Initialize all patients with zero counts
@@ -178,6 +188,7 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
high_risk_count: 0,
abnormal_vitals_count: 0,
marked_logs_count: 0,
incident_count: 0,
});
}
@@ -206,6 +217,27 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
}
}
// Count incidents (type='incident' OR type='crisis' OR verpleegkundig with category='incident')
for (const report of incidentReportsData || []) {
if (!report.patient_id) continue;
const counts = alertCounts.get(report.patient_id);
if (!counts) continue;
// Direct incident/crisis type
if (report.type === 'incident' || report.type === 'crisis') {
counts.incident_count++;
continue;
}
// Verpleegkundig report with incident category
if (report.type === 'verpleegkundig' && report.structured_data) {
const data = report.structured_data as { category?: string };
if (data.category === 'incident') {
counts.incident_count++;
}
}
}
// 3. Build response
const patients: PatientOverzicht[] = Array.from(patientMap.values()).map(
(patient) => {
@@ -213,6 +245,7 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
high_risk_count: 0,
abnormal_vitals_count: 0,
marked_logs_count: 0,
incident_count: 0,
};
return {
@@ -226,7 +259,8 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
total:
alerts.high_risk_count +
alerts.abnormal_vitals_count +
alerts.marked_logs_count,
alerts.marked_logs_count +
alerts.incident_count,
},
};
}

View File

@@ -4,7 +4,10 @@
* Gegroepeerd per dag met dagdeel headers
*/
import { FileText, Clock, User, Pill, Utensils, AlertTriangle, CheckCircle2, Sun, Sunrise, Sunset, Moon } from 'lucide-react';
'use client';
import { useState } from 'react';
import { FileText, Clock, User, Pill, Utensils, AlertTriangle, CheckCircle2, Sun, Sunrise, Sunset, Moon, ChevronDown, ChevronUp } from 'lucide-react';
import { format, isToday, isYesterday } from 'date-fns';
import { nl } from 'date-fns/locale';
import type { Report } from '@/lib/types/overdracht';
@@ -73,9 +76,52 @@ function getCategoryIcon(category: VerpleegkundigCategory | null) {
}
}
function truncateContent(content: string, maxLength: number = 150): string {
if (content.length <= maxLength) return content;
return content.substring(0, maxLength).trim() + '...';
// Check if content needs "Lees meer" (more than 5 lines or very long)
const MAX_LINES = 5;
const CHARS_PER_LINE_ESTIMATE = 80;
function needsExpansion(content: string): boolean {
const lineCount = content.split('\n').length;
const estimatedLines = Math.ceil(content.length / CHARS_PER_LINE_ESTIMATE);
return lineCount > MAX_LINES || estimatedLines > MAX_LINES;
}
// Expandable content component
function ExpandableContent({ content }: { content: string }) {
const [expanded, setExpanded] = useState(false);
const showToggle = needsExpansion(content);
if (!showToggle) {
return (
<p className="text-sm text-slate-700 leading-relaxed whitespace-pre-line">
{content}
</p>
);
}
return (
<div>
<p className={`text-sm text-slate-700 leading-relaxed whitespace-pre-line ${!expanded ? 'line-clamp-5' : ''}`}>
{content}
</p>
<button
onClick={() => setExpanded(!expanded)}
className="mt-1 inline-flex items-center gap-1 text-xs font-medium text-teal-600 hover:text-teal-700 transition-colors"
>
{expanded ? (
<>
<ChevronUp className="h-3 w-3" />
Minder tonen
</>
) : (
<>
<ChevronDown className="h-3 w-3" />
Lees meer
</>
)}
</button>
</div>
);
}
// Dagdeel helpers
@@ -157,7 +203,15 @@ function groupReportsByDayAndPart(reports: Report[]): GroupedReports[] {
}
export function ReportsBlock({ reports }: ReportsBlockProps) {
const incidentCount = reports.filter(r => r.type === 'incident' || r.type === 'crisis').length;
// Count incidents: type='incident' OR type='crisis' OR verpleegkundig with category='incident'
const incidentCount = reports.filter(r => {
if (r.type === 'incident' || r.type === 'crisis') return true;
if (r.type === 'verpleegkundig') {
const category = getVerpleegkundigCategory(r.structured_data);
return category === 'incident';
}
return false;
}).length;
const handoverCount = reports.filter(r => r.include_in_handover).length;
const groupedReports = groupReportsByDayAndPart(reports);
@@ -280,9 +334,7 @@ export function ReportsBlock({ reports }: ReportsBlockProps) {
</div>
{/* Report content */}
<p className="text-sm text-slate-700 leading-relaxed">
{truncateContent(report.content)}
</p>
<ExpandableContent content={report.content} />
{/* Author if available */}
{report.created_by && (

View File

@@ -40,6 +40,7 @@ export function PatientListRow({ patient, isSelected, onClick }: PatientListRowP
const hasHighRisk = patient.alerts.high_risk_count > 0;
const hasAbnormalVitals = patient.alerts.abnormal_vitals_count > 0;
const hasMarkedLogs = patient.alerts.marked_logs_count > 0;
const hasIncidents = patient.alerts.incident_count > 0;
return (
<button
@@ -88,9 +89,18 @@ export function PatientListRow({ patient, isSelected, onClick }: PatientListRowP
{patient.alerts.high_risk_count}
</span>
)}
{hasAbnormalVitals && (
{hasIncidents && (
<span
className="flex items-center gap-0.5 px-1.5 py-0.5 bg-orange-100 text-orange-700 rounded text-xs font-medium"
title={`${patient.alerts.incident_count} incident${patient.alerts.incident_count > 1 ? 'en' : ''}`}
>
<AlertTriangle className="h-3 w-3" />
{patient.alerts.incident_count}
</span>
)}
{hasAbnormalVitals && (
<span
className="flex items-center gap-0.5 px-1.5 py-0.5 bg-amber-100 text-amber-700 rounded text-xs font-medium"
title={`${patient.alerts.abnormal_vitals_count} afwijkende vitale waarde${patient.alerts.abnormal_vitals_count > 1 ? 'n' : ''}`}
>
<Activity className="h-3 w-3" />

View File

@@ -24,9 +24,9 @@ export function PatientList({ patients, selectedPatientId, onSelectPatient }: Pa
const [filter, setFilter] = useState<FilterType>('all');
// Filter patients based on selected filter
// "Met alerts" = alleen echte alerts (hoge risico's + afwijkende vitals), niet overdracht notities
// "Met alerts" = alleen echte alerts (hoge risico's, incidenten, afwijkende vitals), niet overdracht notities
const hasRealAlerts = (p: PatientOverzicht) =>
p.alerts.high_risk_count > 0 || p.alerts.abnormal_vitals_count > 0;
p.alerts.high_risk_count > 0 || p.alerts.incident_count > 0 || p.alerts.abnormal_vitals_count > 0;
const filteredPatients = filter === 'alerts'
? patients.filter(hasRealAlerts)