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>
@@ -87,6 +87,7 @@ export async function GET(request: NextRequest) {
|
|||||||
{ data: risksData },
|
{ data: risksData },
|
||||||
{ data: vitalsData },
|
{ data: vitalsData },
|
||||||
{ data: logsData },
|
{ data: logsData },
|
||||||
|
{ data: incidentReportsData },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// High risk assessments (via intakes)
|
// High risk assessments (via intakes)
|
||||||
supabase
|
supabase
|
||||||
@@ -114,6 +115,15 @@ export async function GET(request: NextRequest) {
|
|||||||
.eq('shift_date', targetDate)
|
.eq('shift_date', targetDate)
|
||||||
.eq('include_in_handover', true)
|
.eq('include_in_handover', true)
|
||||||
.is('deleted_at', null),
|
.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
|
// Count alerts per patient
|
||||||
@@ -121,6 +131,7 @@ export async function GET(request: NextRequest) {
|
|||||||
high_risk_count: number;
|
high_risk_count: number;
|
||||||
abnormal_vitals_count: number;
|
abnormal_vitals_count: number;
|
||||||
marked_logs_count: number;
|
marked_logs_count: number;
|
||||||
|
incident_count: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Initialize all patients with zero counts
|
// Initialize all patients with zero counts
|
||||||
@@ -129,6 +140,7 @@ export async function GET(request: NextRequest) {
|
|||||||
high_risk_count: 0,
|
high_risk_count: 0,
|
||||||
abnormal_vitals_count: 0,
|
abnormal_vitals_count: 0,
|
||||||
marked_logs_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
|
// 3. Build response
|
||||||
const patients: PatientOverzicht[] = Array.from(patientMap.values()).map(
|
const patients: PatientOverzicht[] = Array.from(patientMap.values()).map(
|
||||||
(patient) => {
|
(patient) => {
|
||||||
@@ -164,6 +197,7 @@ export async function GET(request: NextRequest) {
|
|||||||
high_risk_count: 0,
|
high_risk_count: 0,
|
||||||
abnormal_vitals_count: 0,
|
abnormal_vitals_count: 0,
|
||||||
marked_logs_count: 0,
|
marked_logs_count: 0,
|
||||||
|
incident_count: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -177,7 +211,8 @@ export async function GET(request: NextRequest) {
|
|||||||
total:
|
total:
|
||||||
alerts.high_risk_count +
|
alerts.high_risk_count +
|
||||||
alerts.abnormal_vitals_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: risksData },
|
||||||
{ data: vitalsData },
|
{ data: vitalsData },
|
||||||
{ data: logsData },
|
{ data: logsData },
|
||||||
|
{ data: incidentReportsData },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// High risk assessments (via intakes) - these are not time-bound
|
// High risk assessments (via intakes) - these are not time-bound
|
||||||
supabase
|
supabase
|
||||||
@@ -163,6 +164,14 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
|
|||||||
.lte('shift_date', targetDate)
|
.lte('shift_date', targetDate)
|
||||||
.eq('include_in_handover', true)
|
.eq('include_in_handover', true)
|
||||||
.is('deleted_at', null),
|
.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
|
// Count alerts per patient
|
||||||
@@ -170,6 +179,7 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
|
|||||||
high_risk_count: number;
|
high_risk_count: number;
|
||||||
abnormal_vitals_count: number;
|
abnormal_vitals_count: number;
|
||||||
marked_logs_count: number;
|
marked_logs_count: number;
|
||||||
|
incident_count: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Initialize all patients with zero counts
|
// Initialize all patients with zero counts
|
||||||
@@ -178,6 +188,7 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
|
|||||||
high_risk_count: 0,
|
high_risk_count: 0,
|
||||||
abnormal_vitals_count: 0,
|
abnormal_vitals_count: 0,
|
||||||
marked_logs_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
|
// 3. Build response
|
||||||
const patients: PatientOverzicht[] = Array.from(patientMap.values()).map(
|
const patients: PatientOverzicht[] = Array.from(patientMap.values()).map(
|
||||||
(patient) => {
|
(patient) => {
|
||||||
@@ -213,6 +245,7 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
|
|||||||
high_risk_count: 0,
|
high_risk_count: 0,
|
||||||
abnormal_vitals_count: 0,
|
abnormal_vitals_count: 0,
|
||||||
marked_logs_count: 0,
|
marked_logs_count: 0,
|
||||||
|
incident_count: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -226,7 +259,8 @@ export async function getOverdrachtPatients(period: PeriodValue = '7d'): Promise
|
|||||||
total:
|
total:
|
||||||
alerts.high_risk_count +
|
alerts.high_risk_count +
|
||||||
alerts.abnormal_vitals_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
|
* 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 { format, isToday, isYesterday } from 'date-fns';
|
||||||
import { nl } from 'date-fns/locale';
|
import { nl } from 'date-fns/locale';
|
||||||
import type { Report } from '@/lib/types/overdracht';
|
import type { Report } from '@/lib/types/overdracht';
|
||||||
@@ -73,9 +76,52 @@ function getCategoryIcon(category: VerpleegkundigCategory | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncateContent(content: string, maxLength: number = 150): string {
|
// Check if content needs "Lees meer" (more than 5 lines or very long)
|
||||||
if (content.length <= maxLength) return content;
|
const MAX_LINES = 5;
|
||||||
return content.substring(0, maxLength).trim() + '...';
|
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
|
// Dagdeel helpers
|
||||||
@@ -157,7 +203,15 @@ function groupReportsByDayAndPart(reports: Report[]): GroupedReports[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ReportsBlock({ reports }: ReportsBlockProps) {
|
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 handoverCount = reports.filter(r => r.include_in_handover).length;
|
||||||
const groupedReports = groupReportsByDayAndPart(reports);
|
const groupedReports = groupReportsByDayAndPart(reports);
|
||||||
|
|
||||||
@@ -280,9 +334,7 @@ export function ReportsBlock({ reports }: ReportsBlockProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Report content */}
|
{/* Report content */}
|
||||||
<p className="text-sm text-slate-700 leading-relaxed">
|
<ExpandableContent content={report.content} />
|
||||||
{truncateContent(report.content)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Author if available */}
|
{/* Author if available */}
|
||||||
{report.created_by && (
|
{report.created_by && (
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function PatientListRow({ patient, isSelected, onClick }: PatientListRowP
|
|||||||
const hasHighRisk = patient.alerts.high_risk_count > 0;
|
const hasHighRisk = patient.alerts.high_risk_count > 0;
|
||||||
const hasAbnormalVitals = patient.alerts.abnormal_vitals_count > 0;
|
const hasAbnormalVitals = patient.alerts.abnormal_vitals_count > 0;
|
||||||
const hasMarkedLogs = patient.alerts.marked_logs_count > 0;
|
const hasMarkedLogs = patient.alerts.marked_logs_count > 0;
|
||||||
|
const hasIncidents = patient.alerts.incident_count > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -88,9 +89,18 @@ export function PatientListRow({ patient, isSelected, onClick }: PatientListRowP
|
|||||||
{patient.alerts.high_risk_count}
|
{patient.alerts.high_risk_count}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{hasAbnormalVitals && (
|
{hasIncidents && (
|
||||||
<span
|
<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"
|
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' : ''}`}
|
title={`${patient.alerts.abnormal_vitals_count} afwijkende vitale waarde${patient.alerts.abnormal_vitals_count > 1 ? 'n' : ''}`}
|
||||||
>
|
>
|
||||||
<Activity className="h-3 w-3" />
|
<Activity className="h-3 w-3" />
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export function PatientList({ patients, selectedPatientId, onSelectPatient }: Pa
|
|||||||
const [filter, setFilter] = useState<FilterType>('all');
|
const [filter, setFilter] = useState<FilterType>('all');
|
||||||
|
|
||||||
// Filter patients based on selected filter
|
// 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) =>
|
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'
|
const filteredPatients = filter === 'alerts'
|
||||||
? patients.filter(hasRealAlerts)
|
? patients.filter(hasRealAlerts)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 77 KiB |
@@ -12,6 +12,7 @@ export interface PatientOverzicht {
|
|||||||
high_risk_count: number;
|
high_risk_count: number;
|
||||||
abnormal_vitals_count: number;
|
abnormal_vitals_count: number;
|
||||||
marked_logs_count: number;
|
marked_logs_count: number;
|
||||||
|
incident_count: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||