feat(overdracht): E4 + E5 + E6 - Overdracht UI compleet
Epic 4 - Overdracht Overzicht: - /epd/overdracht pagina met patiënten grid - PatientCard component met alert badges - Filter tabs (Alle patiënten / Met alerts) - Responsive grid layout Epic 5 - Overdracht Detail: - /epd/overdracht/[patientId] pagina - VitalsBlock: vitale functies met interpretatie kleuren - ReportsBlock: rapportages laatste 24 uur - NursingLogsBlock: gemarkeerde dagnotities - RisksBlock: actieve risico's gesorteerd op niveau - AISummaryBlock: Claude AI samenvatting met bronverwijzingen Epic 6 - Integratie & Polish: - Overdracht link in EPD sidebar - Navigatie links dagregistratie <-> overdracht - Build en type check passed Bouwplan v1.3: alle 19 stories afgerond 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
116
app/epd/overdracht/components/patient-card.tsx
Normal file
116
app/epd/overdracht/components/patient-card.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* PatientCard Component
|
||||
* E4.S2: Naam, leeftijd, alert badge (rood=hoog risico), doorklik
|
||||
*/
|
||||
|
||||
import Link from 'next/link';
|
||||
import { AlertTriangle, User, ChevronRight, Activity, FileText, ShieldAlert } from 'lucide-react';
|
||||
import type { PatientOverzicht } from '@/lib/types/overdracht';
|
||||
|
||||
interface PatientCardProps {
|
||||
patient: PatientOverzicht;
|
||||
}
|
||||
|
||||
function formatPatientName(nameGiven: string[], nameFamily: string): string {
|
||||
const given = nameGiven.join(' ');
|
||||
return `${given} ${nameFamily}`;
|
||||
}
|
||||
|
||||
function calculateAge(birthDate: string): number {
|
||||
const birth = new Date(birthDate);
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birth.getFullYear();
|
||||
const monthDiff = today.getMonth() - birth.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
function getGenderLabel(gender: string): string {
|
||||
switch (gender) {
|
||||
case 'male': return 'M';
|
||||
case 'female': return 'V';
|
||||
default: return 'O';
|
||||
}
|
||||
}
|
||||
|
||||
export function PatientCard({ patient }: PatientCardProps) {
|
||||
const name = formatPatientName(patient.name_given, patient.name_family);
|
||||
const age = calculateAge(patient.birth_date);
|
||||
const genderLabel = getGenderLabel(patient.gender);
|
||||
const hasAlerts = patient.alerts.total > 0;
|
||||
const hasHighRisk = patient.alerts.high_risk_count > 0;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/epd/overdracht/${patient.id}`}
|
||||
className={`
|
||||
block bg-white rounded-xl border transition-all
|
||||
hover:shadow-md hover:border-slate-300
|
||||
${hasHighRisk ? 'border-red-200' : 'border-slate-200'}
|
||||
`}
|
||||
>
|
||||
<div className="p-4">
|
||||
{/* Header with name and alert badge */}
|
||||
<div className="flex items-start justify-between gap-2 mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`
|
||||
w-10 h-10 rounded-full flex items-center justify-center
|
||||
${hasHighRisk ? 'bg-red-100' : 'bg-slate-100'}
|
||||
`}>
|
||||
<User className={`h-5 w-5 ${hasHighRisk ? 'text-red-600' : 'text-slate-500'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-900 line-clamp-1">{name}</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
{age} jaar • {genderLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{hasAlerts && (
|
||||
<span className={`
|
||||
inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium
|
||||
${hasHighRisk ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}
|
||||
`}>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{patient.alerts.total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Alert details */}
|
||||
{hasAlerts && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{patient.alerts.high_risk_count > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-red-50 text-red-600 rounded text-xs">
|
||||
<ShieldAlert className="h-3 w-3" />
|
||||
{patient.alerts.high_risk_count} hoog risico
|
||||
</span>
|
||||
)}
|
||||
{patient.alerts.abnormal_vitals_count > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-600 rounded text-xs">
|
||||
<Activity className="h-3 w-3" />
|
||||
{patient.alerts.abnormal_vitals_count} afwijkend
|
||||
</span>
|
||||
)}
|
||||
{patient.alerts.marked_logs_count > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs">
|
||||
<FileText className="h-3 w-3" />
|
||||
{patient.alerts.marked_logs_count} notitie
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer with link indicator */}
|
||||
<div className="flex items-center justify-between text-sm text-slate-500 pt-2 border-t border-slate-100">
|
||||
<span>Bekijk overdracht</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
99
app/epd/overdracht/components/patient-grid.tsx
Normal file
99
app/epd/overdracht/components/patient-grid.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* PatientGrid Component
|
||||
* E4.S1: Grid van PatientCards met filter tabs
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PatientCard } from './patient-card';
|
||||
import type { PatientOverzicht } from '@/lib/types/overdracht';
|
||||
import { Users, AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface PatientGridProps {
|
||||
patients: PatientOverzicht[];
|
||||
}
|
||||
|
||||
type FilterType = 'all' | 'alerts';
|
||||
|
||||
export function PatientGrid({ patients }: PatientGridProps) {
|
||||
const [filter, setFilter] = useState<FilterType>('all');
|
||||
|
||||
const filteredPatients = filter === 'alerts'
|
||||
? patients.filter(p => p.alerts.total > 0)
|
||||
: patients;
|
||||
|
||||
const alertCount = patients.filter(p => p.alerts.total > 0).length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-2 mb-6">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`
|
||||
inline-flex items-center gap-2 px-4 py-2 rounded-lg font-medium text-sm
|
||||
transition-all
|
||||
${filter === 'all'
|
||||
? 'bg-teal-600 text-white shadow-sm'
|
||||
: 'bg-white text-slate-600 hover:bg-slate-50 border border-slate-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Users className="h-4 w-4" />
|
||||
Alle patiënten
|
||||
<span className={`
|
||||
px-2 py-0.5 rounded-full text-xs
|
||||
${filter === 'all' ? 'bg-teal-500 text-white' : 'bg-slate-100 text-slate-600'}
|
||||
`}>
|
||||
{patients.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setFilter('alerts')}
|
||||
className={`
|
||||
inline-flex items-center gap-2 px-4 py-2 rounded-lg font-medium text-sm
|
||||
transition-all
|
||||
${filter === 'alerts'
|
||||
? 'bg-teal-600 text-white shadow-sm'
|
||||
: 'bg-white text-slate-600 hover:bg-slate-50 border border-slate-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
Met alerts
|
||||
<span className={`
|
||||
px-2 py-0.5 rounded-full text-xs
|
||||
${filter === 'alerts' ? 'bg-teal-500 text-white' : 'bg-red-100 text-red-600'}
|
||||
`}>
|
||||
{alertCount}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Patient Grid */}
|
||||
{filteredPatients.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
|
||||
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Users className="h-8 w-8 text-slate-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-slate-900 mb-2">
|
||||
{filter === 'alerts' ? 'Geen patiënten met alerts' : 'Geen patiënten vandaag'}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
{filter === 'alerts'
|
||||
? 'Er zijn geen patiënten met hoog risico, afwijkende vitals of gemarkeerde notities.'
|
||||
: 'Er zijn geen patiënten met een encounter gepland voor vandaag.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredPatients.map((patient) => (
|
||||
<PatientCard key={patient.id} patient={patient} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user