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:
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user