refactor(swift): Update UI styles and animations for improved consistency
- Changed background colors and text colors across various components to enhance readability and visual appeal. - Updated animation properties for smoother transitions in CanvasArea and BlockContainer. - Adjusted styles in FallbackPicker, OverdrachtBlock, and other components to align with the new design system. - Improved accessibility by ensuring color contrasts meet standards. This update aims to create a more cohesive user experience throughout the application.
This commit is contained in:
@@ -21,7 +21,7 @@ export default async function SwiftLayout({ children }: SwiftLayoutProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-900 text-white flex flex-col">
|
<div className="min-h-screen bg-slate-50 flex flex-col">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,12 +25,18 @@ const SIZE_CLASSES: Record<BlockSize, string> = {
|
|||||||
full: 'max-w-4xl',
|
full: 'max-w-4xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Container animations
|
// Container animations - consistent met CanvasArea slide up/down
|
||||||
|
// Slide up + fade in bij openen, scale 0.95 → 1.0 (200ms)
|
||||||
const containerVariants: Variants = {
|
const containerVariants: Variants = {
|
||||||
initial: { scale: 0.98, opacity: 0 },
|
initial: {
|
||||||
|
scale: 0.95,
|
||||||
|
opacity: 0,
|
||||||
|
y: 0, // BlockContainer animatie wordt door CanvasArea gedaan
|
||||||
|
},
|
||||||
animate: {
|
animate: {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
||||||
@@ -67,15 +73,15 @@ export function BlockContainer({ title, size = 'md', children }: BlockContainerP
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
className={`w-full ${SIZE_CLASSES[size]} bg-slate-800 rounded-xl border border-slate-700 shadow-2xl`}
|
className={`w-full ${SIZE_CLASSES[size]} bg-white rounded-xl border border-slate-200 shadow-lg`}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-700">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-200">
|
||||||
<motion.h2
|
<motion.h2
|
||||||
initial={{ opacity: 0, x: -10 }}
|
initial={{ opacity: 0, x: -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.05, duration: 0.2 }}
|
transition={{ delay: 0.05, duration: 0.2 }}
|
||||||
className="text-lg font-medium text-white"
|
className="text-lg font-medium text-slate-900"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
@@ -85,7 +91,7 @@ export function BlockContainer({ title, size = 'md', children }: BlockContainerP
|
|||||||
initial="rest"
|
initial="rest"
|
||||||
whileHover="hover"
|
whileHover="hover"
|
||||||
whileTap="tap"
|
whileTap="tap"
|
||||||
className="p-1 rounded hover:bg-slate-700 text-slate-400 hover:text-white transition-colors"
|
className="p-1 rounded hover:bg-slate-100 text-slate-400 hover:text-slate-700 transition-colors"
|
||||||
title="Sluiten (Esc)"
|
title="Sluiten (Esc)"
|
||||||
aria-label="Block sluiten"
|
aria-label="Block sluiten"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -240,15 +240,15 @@ export function DagnotatieBlock({ prefill }: DagnotitieBlockProps) {
|
|||||||
<Label htmlFor="patient-search">Patiënt *</Label>
|
<Label htmlFor="patient-search">Patiënt *</Label>
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
{selectedPatient ? (
|
{selectedPatient ? (
|
||||||
<div className="flex items-center gap-2 p-2 rounded-md border border-slate-700 bg-slate-800/50">
|
<div className="flex items-center gap-2 p-2 rounded-md border border-slate-200 bg-slate-50">
|
||||||
<User className="h-4 w-4 text-slate-400" />
|
<User className="h-4 w-4 text-slate-500" />
|
||||||
<span className="flex-1 text-sm text-white">{formatPatientName(selectedPatient)}</span>
|
<span className="flex-1 text-sm text-slate-900">{formatPatientName(selectedPatient)}</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleClearPatient}
|
onClick={handleClearPatient}
|
||||||
className="h-6 w-6 p-0 text-slate-400 hover:text-white"
|
className="h-6 w-6 p-0 text-slate-400 hover:text-slate-700"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</Button>
|
</Button>
|
||||||
@@ -278,13 +278,13 @@ export function DagnotatieBlock({ prefill }: DagnotitieBlockProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showPatientDropdown && patients.length > 0 && (
|
{showPatientDropdown && patients.length > 0 && (
|
||||||
<div className="absolute z-50 w-full mt-1 bg-slate-800 border border-slate-700 rounded-md shadow-lg max-h-60 overflow-auto">
|
<div className="absolute z-50 w-full mt-1 bg-white border border-slate-200 rounded-md shadow-lg max-h-60 overflow-auto">
|
||||||
{patients.map((patient) => (
|
{patients.map((patient) => (
|
||||||
<button
|
<button
|
||||||
key={patient.id}
|
key={patient.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelectPatient(patient)}
|
onClick={() => handleSelectPatient(patient)}
|
||||||
className="w-full px-3 py-2 text-left text-sm text-slate-300 hover:bg-slate-700 transition-colors"
|
className="w-full px-3 py-2 text-left text-sm text-slate-700 hover:bg-slate-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="font-medium">{formatPatientName(patient)}</div>
|
<div className="font-medium">{formatPatientName(patient)}</div>
|
||||||
{patient.identifier_bsn && (
|
{patient.identifier_bsn && (
|
||||||
@@ -314,8 +314,8 @@ export function DagnotatieBlock({ prefill }: DagnotitieBlockProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-slate-700 text-white border-2 border-slate-500'
|
? 'bg-slate-900 text-white border-2 border-slate-700'
|
||||||
: 'bg-slate-800/50 text-slate-400 border border-slate-700 hover:bg-slate-700 hover:text-white'
|
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100 hover:text-slate-900'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{catConfig.label}
|
{catConfig.label}
|
||||||
@@ -349,9 +349,9 @@ export function DagnotatieBlock({ prefill }: DagnotitieBlockProps) {
|
|||||||
id="include-handover"
|
id="include-handover"
|
||||||
checked={includeInHandover}
|
checked={includeInHandover}
|
||||||
onChange={(e) => setIncludeInHandover(e.target.checked)}
|
onChange={(e) => setIncludeInHandover(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-slate-600 bg-slate-800 text-slate-600 focus:ring-slate-500"
|
className="h-4 w-4 rounded border-slate-300 bg-white text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="include-handover" className="cursor-pointer text-sm text-slate-300">
|
<Label htmlFor="include-handover" className="cursor-pointer text-sm text-slate-700">
|
||||||
Opnemen in overdracht
|
Opnemen in overdracht
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const BLOCK_OPTIONS: BlockOption[] = [
|
|||||||
description: 'Schrijf een dagnotitie',
|
description: 'Schrijf een dagnotitie',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
shortcut: '1',
|
shortcut: '1',
|
||||||
color: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
color: 'bg-blue-50 text-blue-600 border-blue-200',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'zoeken',
|
type: 'zoeken',
|
||||||
@@ -41,7 +41,7 @@ const BLOCK_OPTIONS: BlockOption[] = [
|
|||||||
description: 'Zoek een patiënt',
|
description: 'Zoek een patiënt',
|
||||||
icon: Search,
|
icon: Search,
|
||||||
shortcut: '2',
|
shortcut: '2',
|
||||||
color: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
|
color: 'bg-emerald-50 text-emerald-600 border-emerald-200',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'overdracht',
|
type: 'overdracht',
|
||||||
@@ -49,7 +49,7 @@ const BLOCK_OPTIONS: BlockOption[] = [
|
|||||||
description: 'Bekijk overdracht',
|
description: 'Bekijk overdracht',
|
||||||
icon: ArrowRightLeft,
|
icon: ArrowRightLeft,
|
||||||
shortcut: '3',
|
shortcut: '3',
|
||||||
color: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
color: 'bg-amber-50 text-amber-600 border-amber-200',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -108,14 +108,14 @@ export function FallbackPicker({ originalInput }: FallbackPickerProps) {
|
|||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] as [number, number, number, number] }}
|
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] as [number, number, number, number] }}
|
||||||
className="w-full max-w-md bg-slate-800 rounded-xl border border-slate-700 shadow-2xl overflow-hidden"
|
className="w-full max-w-md bg-white rounded-xl border border-slate-200 shadow-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-700">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-200">
|
||||||
<h2 className="text-lg font-medium text-white">Wat wil je doen?</h2>
|
<h2 className="text-lg font-medium text-slate-900">Wat wil je doen?</h2>
|
||||||
<button
|
<button
|
||||||
onClick={closeBlock}
|
onClick={closeBlock}
|
||||||
className="p-1 rounded hover:bg-slate-700 text-slate-400 hover:text-white transition-colors"
|
className="p-1 rounded hover:bg-slate-100 text-slate-400 hover:text-slate-700 transition-colors"
|
||||||
title="Sluiten (Esc)"
|
title="Sluiten (Esc)"
|
||||||
aria-label="Sluiten"
|
aria-label="Sluiten"
|
||||||
>
|
>
|
||||||
@@ -125,9 +125,9 @@ export function FallbackPicker({ originalInput }: FallbackPickerProps) {
|
|||||||
|
|
||||||
{/* Original input display */}
|
{/* Original input display */}
|
||||||
{originalInput && (
|
{originalInput && (
|
||||||
<div className="px-4 py-2 bg-slate-900/50 border-b border-slate-700">
|
<div className="px-4 py-2 bg-slate-50 border-b border-slate-200">
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-slate-500">
|
||||||
Je zei: <span className="text-slate-300">"{originalInput}"</span>
|
Je zei: <span className="text-slate-700">"{originalInput}"</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -141,12 +141,12 @@ export function FallbackPicker({ originalInput }: FallbackPickerProps) {
|
|||||||
onClick={() => handleSelect(option)}
|
onClick={() => handleSelect(option)}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
className={`flex flex-col items-center gap-2 p-4 rounded-lg border transition-all ${option.color} hover:brightness-110`}
|
className={`flex flex-col items-center gap-2 p-4 rounded-lg border transition-all ${option.color} hover:shadow-md`}
|
||||||
>
|
>
|
||||||
<option.icon size={28} />
|
<option.icon size={28} />
|
||||||
<span className="font-medium text-sm">{option.label}</span>
|
<span className="font-medium text-sm">{option.label}</span>
|
||||||
<span className="text-xs opacity-60 text-center">{option.description}</span>
|
<span className="text-xs opacity-70 text-center">{option.description}</span>
|
||||||
<span className="mt-1 px-2 py-0.5 bg-slate-900/50 rounded text-xs text-slate-400">
|
<span className="mt-1 px-2 py-0.5 bg-slate-100 rounded text-xs text-slate-500">
|
||||||
[{option.shortcut}]
|
[{option.shortcut}]
|
||||||
</span>
|
</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@@ -155,7 +155,7 @@ export function FallbackPicker({ originalInput }: FallbackPickerProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer hint */}
|
{/* Footer hint */}
|
||||||
<div className="px-4 py-2 bg-slate-900/30 border-t border-slate-700">
|
<div className="px-4 py-2 bg-slate-50 border-t border-slate-200">
|
||||||
<p className="text-xs text-slate-500 text-center">
|
<p className="text-xs text-slate-500 text-center">
|
||||||
Druk op [1], [2] of [3] voor snelle selectie
|
Druk op [1], [2] of [3] voor snelle selectie
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Period Selector */}
|
{/* Period Selector */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-300">Periode</label>
|
<label className="text-sm font-medium text-slate-700">Periode</label>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
{PERIOD_OPTIONS.map((option) => (
|
{PERIOD_OPTIONS.map((option) => (
|
||||||
<button
|
<button
|
||||||
@@ -201,8 +201,8 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
'px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||||
period === option.value
|
period === option.value
|
||||||
? 'bg-slate-700 text-white border-2 border-slate-500'
|
? 'bg-slate-900 text-white border-2 border-slate-700'
|
||||||
: 'bg-slate-800/50 text-slate-400 border border-slate-700 hover:bg-slate-700 hover:text-white'
|
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100 hover:text-slate-900'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -218,10 +218,10 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
{isLoadingPatients ? (
|
{isLoadingPatients ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-slate-400 mr-2" />
|
<Loader2 className="h-6 w-6 animate-spin text-slate-400 mr-2" />
|
||||||
<span className="text-sm text-slate-400">Patiënten laden...</span>
|
<span className="text-sm text-slate-500">Patiënten laden...</span>
|
||||||
</div>
|
</div>
|
||||||
) : displayPatients.length === 0 ? (
|
) : displayPatients.length === 0 ? (
|
||||||
<div className="text-center py-8 text-slate-500">
|
<div className="text-center py-8 text-slate-400">
|
||||||
<Users className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
<Users className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||||
<p className="text-sm">Geen patiënten gevonden voor deze periode</p>
|
<p className="text-sm">Geen patiënten gevonden voor deze periode</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,21 +236,21 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={patient.id}
|
key={patient.id}
|
||||||
className="p-4 rounded-lg bg-slate-800/50 border border-slate-700"
|
className="p-4 rounded-lg bg-slate-50 border border-slate-200"
|
||||||
>
|
>
|
||||||
{/* Patient Header */}
|
{/* Patient Header */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-medium text-white">
|
<h3 className="text-base font-medium text-slate-900">
|
||||||
{formatPatientName(patient)}
|
{formatPatientName(patient)}
|
||||||
</h3>
|
</h3>
|
||||||
{patient.alerts.total > 0 && (
|
{patient.alerts.total > 0 && (
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-500">
|
||||||
{patient.alerts.total} alert{patient.alerts.total > 1 ? 's' : ''}
|
{patient.alerts.total} alert{patient.alerts.total > 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
{patient.alerts.high_risk_count > 0 && (
|
{patient.alerts.high_risk_count > 0 && (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-red-900/30 text-red-300">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-red-100 text-red-700 border border-red-200">
|
||||||
{patient.alerts.high_risk_count} risico
|
{patient.alerts.high_risk_count} risico
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -272,18 +272,18 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
{/* Loading State */}
|
{/* Loading State */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="py-6 text-center">
|
<div className="py-6 text-center">
|
||||||
<Loader2 className="h-6 w-6 text-violet-400 mx-auto mb-2 animate-spin" />
|
<Loader2 className="h-6 w-6 text-violet-600 mx-auto mb-2 animate-spin" />
|
||||||
<p className="text-sm text-slate-400">Samenvatting wordt gegenereerd...</p>
|
<p className="text-sm text-slate-500">Samenvatting wordt gegenereerd...</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error State */}
|
{/* Error State */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<div className="p-3 bg-red-900/20 rounded-lg border border-red-800 mb-3">
|
<div className="p-3 bg-red-50 rounded-lg border border-red-200 mb-3">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<AlertTriangle className="h-4 w-4 text-red-400 flex-shrink-0 mt-0.5" />
|
<AlertTriangle className="h-4 w-4 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
<p className="text-sm text-red-300">{error}</p>
|
<p className="text-sm text-red-700">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -300,11 +300,11 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
|
|
||||||
{/* Summary Content */}
|
{/* Summary Content */}
|
||||||
{summary && (
|
{summary && (
|
||||||
<div className="space-y-4 pt-4 border-t border-slate-700">
|
<div className="space-y-4 pt-4 border-t border-slate-200">
|
||||||
{/* Samenvatting */}
|
{/* Samenvatting */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-slate-300 mb-2">Samenvatting</h4>
|
<h4 className="text-sm font-medium text-slate-700 mb-2">Samenvatting</h4>
|
||||||
<p className="text-sm text-slate-400 leading-relaxed bg-slate-900/50 p-3 rounded-lg">
|
<p className="text-sm text-slate-600 leading-relaxed bg-white p-3 rounded-lg border border-slate-200">
|
||||||
{summary.samenvatting}
|
{summary.samenvatting}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +312,7 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
{/* Aandachtspunten */}
|
{/* Aandachtspunten */}
|
||||||
{summary.aandachtspunten.length > 0 && (
|
{summary.aandachtspunten.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-slate-300 mb-2">
|
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||||
Aandachtspunten ({summary.aandachtspunten.length})
|
Aandachtspunten ({summary.aandachtspunten.length})
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -326,16 +326,16 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
{/* Actiepunten */}
|
{/* Actiepunten */}
|
||||||
{summary.actiepunten.length > 0 && (
|
{summary.actiepunten.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-slate-300 mb-2">
|
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||||
Actiepunten ({summary.actiepunten.length})
|
Actiepunten ({summary.actiepunten.length})
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{summary.actiepunten.map((actie, index) => (
|
{summary.actiepunten.map((actie, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-start gap-2 text-sm text-slate-400"
|
className="flex items-start gap-2 text-sm text-slate-600"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="h-4 w-4 text-teal-400 flex-shrink-0 mt-0.5" />
|
<CheckCircle2 className="h-4 w-4 text-teal-600 flex-shrink-0 mt-0.5" />
|
||||||
<span>{actie}</span>
|
<span>{actie}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -344,7 +344,7 @@ export function OverdrachtBlock({ prefill }: OverdrachtBlockProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="pt-3 border-t border-slate-700 flex items-center justify-between">
|
<div className="pt-3 border-t border-slate-200 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-500">
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
<Clock className="h-3.5 w-3.5" />
|
<Clock className="h-3.5 w-3.5" />
|
||||||
<span>
|
<span>
|
||||||
@@ -388,15 +388,15 @@ function AandachtspuntItem({ punt }: { punt: AISamenvatting['aandachtspunten'][0
|
|||||||
const getBronTypeStyle = (type: string): { bg: string; text: string } => {
|
const getBronTypeStyle = (type: string): { bg: string; text: string } => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'observatie':
|
case 'observatie':
|
||||||
return { bg: 'bg-teal-900/30', text: 'text-teal-300' };
|
return { bg: 'bg-teal-50', text: 'text-teal-700' };
|
||||||
case 'rapportage':
|
case 'rapportage':
|
||||||
return { bg: 'bg-indigo-900/30', text: 'text-indigo-300' };
|
return { bg: 'bg-indigo-50', text: 'text-indigo-700' };
|
||||||
case 'verpleegkundig':
|
case 'verpleegkundig':
|
||||||
return { bg: 'bg-amber-900/30', text: 'text-amber-300' };
|
return { bg: 'bg-amber-50', text: 'text-amber-700' };
|
||||||
case 'risico':
|
case 'risico':
|
||||||
return { bg: 'bg-red-900/30', text: 'text-red-300' };
|
return { bg: 'bg-red-50', text: 'text-red-700' };
|
||||||
default:
|
default:
|
||||||
return { bg: 'bg-slate-800', text: 'text-slate-400' };
|
return { bg: 'bg-slate-100', text: 'text-slate-600' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -407,16 +407,16 @@ function AandachtspuntItem({ punt }: { punt: AISamenvatting['aandachtspunten'][0
|
|||||||
className={cn(
|
className={cn(
|
||||||
'p-3 rounded-lg border-l-4',
|
'p-3 rounded-lg border-l-4',
|
||||||
punt.urgent
|
punt.urgent
|
||||||
? 'bg-red-900/20 border-red-500'
|
? 'bg-red-50 border-red-500'
|
||||||
: 'bg-slate-800/50 border-slate-600'
|
: 'bg-white border border-slate-200 border-l-slate-400'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2 mb-2">
|
<div className="flex items-start gap-2 mb-2">
|
||||||
{punt.urgent && <AlertTriangle className="h-4 w-4 text-red-400 flex-shrink-0 mt-0.5" />}
|
{punt.urgent && <AlertTriangle className="h-4 w-4 text-red-500 flex-shrink-0 mt-0.5" />}
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sm flex-1',
|
'text-sm flex-1',
|
||||||
punt.urgent ? 'text-red-200 font-medium' : 'text-slate-300'
|
punt.urgent ? 'text-red-700 font-medium' : 'text-slate-700'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{punt.tekst}
|
{punt.tekst}
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ export function PatientContextCard() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-slate-400 mr-2" />
|
<Loader2 className="h-6 w-6 animate-spin text-slate-400 mr-2" />
|
||||||
<span className="text-sm text-slate-400">Context laden...</span>
|
<span className="text-sm text-slate-500">Context laden...</span>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<AlertTriangle className="h-8 w-8 text-red-400 mx-auto mb-2" />
|
<AlertTriangle className="h-8 w-8 text-red-500 mx-auto mb-2" />
|
||||||
<p className="text-sm text-red-600">{error}</p>
|
<p className="text-sm text-red-700">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
) : data ? (
|
) : data ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -74,8 +74,8 @@ export function PatientContextCard() {
|
|||||||
{data.reports && data.reports.length > 0 && (
|
{data.reports && data.reports.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<FileText className="h-4 w-4 text-slate-400" />
|
<FileText className="h-4 w-4 text-slate-500" />
|
||||||
<h3 className="text-sm font-medium text-slate-300">Recente notities</h3>
|
<h3 className="text-sm font-medium text-slate-700">Recente notities</h3>
|
||||||
<span className="text-xs text-slate-500">({data.reports.length})</span>
|
<span className="text-xs text-slate-500">({data.reports.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -95,8 +95,8 @@ export function PatientContextCard() {
|
|||||||
{data.vitals && data.vitals.length > 0 && (
|
{data.vitals && data.vitals.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Activity className="h-4 w-4 text-slate-400" />
|
<Activity className="h-4 w-4 text-slate-500" />
|
||||||
<h3 className="text-sm font-medium text-slate-300">Vitale functies (vandaag)</h3>
|
<h3 className="text-sm font-medium text-slate-700">Vitale functies (vandaag)</h3>
|
||||||
<span className="text-xs text-slate-500">({data.vitals.length})</span>
|
<span className="text-xs text-slate-500">({data.vitals.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -111,8 +111,8 @@ export function PatientContextCard() {
|
|||||||
{data.conditions && data.conditions.length > 0 && (
|
{data.conditions && data.conditions.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Stethoscope className="h-4 w-4 text-slate-400" />
|
<Stethoscope className="h-4 w-4 text-slate-500" />
|
||||||
<h3 className="text-sm font-medium text-slate-300">Actieve diagnoses</h3>
|
<h3 className="text-sm font-medium text-slate-700">Actieve diagnoses</h3>
|
||||||
<span className="text-xs text-slate-500">({data.conditions.length})</span>
|
<span className="text-xs text-slate-500">({data.conditions.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -127,8 +127,8 @@ export function PatientContextCard() {
|
|||||||
{data.risks && data.risks.length > 0 && (
|
{data.risks && data.risks.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-400" />
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||||
<h3 className="text-sm font-medium text-slate-300">Risico's</h3>
|
<h3 className="text-sm font-medium text-slate-700">Risico's</h3>
|
||||||
<span className="text-xs text-slate-500">({data.risks.length})</span>
|
<span className="text-xs text-slate-500">({data.risks.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -144,7 +144,7 @@ export function PatientContextCard() {
|
|||||||
(!data.vitals || data.vitals.length === 0) &&
|
(!data.vitals || data.vitals.length === 0) &&
|
||||||
(!data.conditions || data.conditions.length === 0) &&
|
(!data.conditions || data.conditions.length === 0) &&
|
||||||
(!data.risks || data.risks.length === 0) && (
|
(!data.risks || data.risks.length === 0) && (
|
||||||
<div className="text-center py-8 text-slate-500">
|
<div className="text-center py-8 text-slate-400">
|
||||||
<p className="text-sm">Geen context beschikbaar voor deze patiënt</p>
|
<p className="text-sm">Geen context beschikbaar voor deze patiënt</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -159,12 +159,12 @@ function ReportItem({ report }: { report: Report }) {
|
|||||||
const formattedDate = date ? format(date, 'd MMM HH:mm', { locale: nl }) : 'Onbekend';
|
const formattedDate = date ? format(date, 'd MMM HH:mm', { locale: nl }) : 'Onbekend';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-3 rounded-lg bg-slate-800/50 border border-slate-700">
|
<div className="p-3 rounded-lg bg-slate-50 border border-slate-200">
|
||||||
<div className="flex items-start justify-between gap-2 mb-1">
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span className="text-xs font-medium text-slate-400 uppercase">{report.type}</span>
|
<span className="text-xs font-medium text-slate-500 uppercase">{report.type}</span>
|
||||||
{report.include_in_handover && (
|
{report.include_in_handover && (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-900/50 text-blue-300">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-200">
|
||||||
Overdracht
|
Overdracht
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -174,7 +174,7 @@ function ReportItem({ report }: { report: Report }) {
|
|||||||
{formattedDate}
|
{formattedDate}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-300 line-clamp-2">{report.content}</p>
|
<p className="text-sm text-slate-700 line-clamp-2">{report.content}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -189,8 +189,8 @@ function VitalItem({ vital }: { vital: VitalSign }) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'p-2 rounded-lg border',
|
'p-2 rounded-lg border',
|
||||||
isAbnormal
|
isAbnormal
|
||||||
? 'bg-amber-900/20 border-amber-700 text-amber-200'
|
? 'bg-amber-50 border-amber-200 text-amber-700'
|
||||||
: 'bg-slate-800/50 border-slate-700 text-slate-300'
|
: 'bg-slate-50 border-slate-200 text-slate-700'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-medium mb-0.5">{vital.code_display}</div>
|
<div className="text-xs font-medium mb-0.5">{vital.code_display}</div>
|
||||||
@@ -208,10 +208,10 @@ function ConditionItem({ condition }: { condition: Condition }) {
|
|||||||
const formattedDate = date ? format(date, 'd MMM yyyy', { locale: nl }) : null;
|
const formattedDate = date ? format(date, 'd MMM yyyy', { locale: nl }) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 p-2 rounded-lg bg-slate-800/50 border border-slate-700">
|
<div className="flex items-center gap-3 p-2 rounded-lg bg-slate-50 border border-slate-200">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-blue-400" />
|
<div className="w-1.5 h-1.5 rounded-full bg-blue-500" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm text-slate-300">{condition.code_display}</p>
|
<p className="text-sm text-slate-700">{condition.code_display}</p>
|
||||||
{formattedDate && (
|
{formattedDate && (
|
||||||
<p className="text-xs text-slate-500">Sinds {formattedDate}</p>
|
<p className="text-xs text-slate-500">Sinds {formattedDate}</p>
|
||||||
)}
|
)}
|
||||||
@@ -230,11 +230,11 @@ function RiskBadge({ risk }: { risk: RiskAssessment }) {
|
|||||||
weglopen: 'Weglopen',
|
weglopen: 'Weglopen',
|
||||||
};
|
};
|
||||||
|
|
||||||
const RISK_LEVEL_STYLES: Record<string, { bg: string; text: string; dot: string }> = {
|
const RISK_LEVEL_STYLES: Record<string, { bg: string; text: string; dot: string; border: string }> = {
|
||||||
zeer_hoog: { bg: 'bg-red-900/30', text: 'text-red-300', dot: 'bg-red-500' },
|
zeer_hoog: { bg: 'bg-red-50', text: 'text-red-700', dot: 'bg-red-500', border: 'border-red-200' },
|
||||||
hoog: { bg: 'bg-red-900/20', text: 'text-red-400', dot: 'bg-red-500' },
|
hoog: { bg: 'bg-red-50', text: 'text-red-600', dot: 'bg-red-500', border: 'border-red-200' },
|
||||||
gemiddeld: { bg: 'bg-amber-900/20', text: 'text-amber-300', dot: 'bg-amber-500' },
|
gemiddeld: { bg: 'bg-amber-50', text: 'text-amber-700', dot: 'bg-amber-500', border: 'border-amber-200' },
|
||||||
laag: { bg: 'bg-green-900/20', text: 'text-green-300', dot: 'bg-green-500' },
|
laag: { bg: 'bg-green-50', text: 'text-green-700', dot: 'bg-green-500', border: 'border-green-200' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = RISK_LEVEL_STYLES[risk.risk_level] || RISK_LEVEL_STYLES.laag;
|
const styles = RISK_LEVEL_STYLES[risk.risk_level] || RISK_LEVEL_STYLES.laag;
|
||||||
@@ -250,7 +250,7 @@ function RiskBadge({ risk }: { risk: RiskAssessment }) {
|
|||||||
'inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium border',
|
'inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium border',
|
||||||
styles.bg,
|
styles.bg,
|
||||||
styles.text,
|
styles.text,
|
||||||
styles.dot === 'bg-red-500' ? 'border-red-700' : styles.dot === 'bg-amber-500' ? 'border-amber-700' : 'border-green-700'
|
styles.border
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={cn('w-1.5 h-1.5 rounded-full', styles.dot)} />
|
<span className={cn('w-1.5 h-1.5 rounded-full', styles.dot)} />
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export function ZoekenBlock({ prefill }: ZoekenBlockProps) {
|
|||||||
{searchQuery.length >= 2 && (
|
{searchQuery.length >= 2 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<div className="flex items-center justify-center py-8 text-slate-400">
|
<div className="flex items-center justify-center py-8 text-slate-500">
|
||||||
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
||||||
<span className="text-sm">Zoeken...</span>
|
<span className="text-sm">Zoeken...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,8 +269,8 @@ export function ZoekenBlock({ prefill }: ZoekenBlockProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'w-full px-3 py-2.5 rounded-lg border text-left transition-colors',
|
'w-full px-3 py-2.5 rounded-lg border text-left transition-colors',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-slate-700 border-slate-600 cursor-wait'
|
? 'bg-slate-100 border-slate-300 cursor-wait'
|
||||||
: 'bg-slate-800/50 border-slate-700 hover:bg-slate-700 hover:border-slate-600 cursor-pointer'
|
: 'bg-slate-50 border-slate-200 hover:bg-slate-100 hover:border-slate-300 cursor-pointer'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -284,17 +284,17 @@ export function ZoekenBlock({ prefill }: ZoekenBlockProps) {
|
|||||||
.toUpperCase()}
|
.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-medium text-white truncate">
|
<div className="text-sm font-medium text-slate-900 truncate">
|
||||||
{formatPatientDisplay(patient)}
|
{formatPatientDisplay(patient)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-0.5">
|
<div className="flex items-center gap-3 mt-0.5">
|
||||||
{patient.identifier_bsn && (
|
{patient.identifier_bsn && (
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-500">
|
||||||
BSN: {patient.identifier_bsn}
|
BSN: {patient.identifier_bsn}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{patient.identifier_client_number && (
|
{patient.identifier_client_number && (
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-500">
|
||||||
Client: {patient.identifier_client_number}
|
Client: {patient.identifier_client_number}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -302,9 +302,9 @@ export function ZoekenBlock({ prefill }: ZoekenBlockProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-blue-400 shrink-0" />
|
<Loader2 className="h-4 w-4 animate-spin text-blue-600 shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<Check className="h-4 w-4 text-slate-500 shrink-0" />
|
<Check className="h-4 w-4 text-slate-400 shrink-0" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -312,7 +312,7 @@ export function ZoekenBlock({ prefill }: ZoekenBlockProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-slate-500">
|
<div className="flex flex-col items-center justify-center py-8 text-slate-400">
|
||||||
<User className="h-8 w-8 mb-2 opacity-50" />
|
<User className="h-8 w-8 mb-2 opacity-50" />
|
||||||
<p className="text-sm">Geen patiënten gevonden</p>
|
<p className="text-sm">Geen patiënten gevonden</p>
|
||||||
<p className="text-xs mt-1">Probeer een andere zoekterm</p>
|
<p className="text-xs mt-1">Probeer een andere zoekterm</p>
|
||||||
@@ -323,7 +323,7 @@ export function ZoekenBlock({ prefill }: ZoekenBlockProps) {
|
|||||||
|
|
||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
{searchQuery.length < 2 && (
|
{searchQuery.length < 2 && (
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-slate-500">
|
<div className="flex flex-col items-center justify-center py-8 text-slate-400">
|
||||||
<Search className="h-8 w-8 mb-2 opacity-50" />
|
<Search className="h-8 w-8 mb-2 opacity-50" />
|
||||||
<p className="text-sm">Typ minimaal 2 karakters om te zoeken</p>
|
<p className="text-sm">Typ minimaal 2 karakters om te zoeken</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,22 +33,50 @@ export function CanvasArea() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block animations volgens UX specificatie (sectie 11.1):
|
||||||
|
// Openen: Slide up + fade in (200ms), Scale: 0.95 → 1.0
|
||||||
|
// Sluiten: Slide down + fade out (200ms), Scale: 1.0 → 0.95
|
||||||
const blockAnimations = {
|
const blockAnimations = {
|
||||||
initial: { opacity: 0, y: 20, scale: 0.95 },
|
initial: { opacity: 0, y: 20, scale: 0.95 },
|
||||||
animate: { opacity: 1, y: 0, scale: 1 },
|
animate: {
|
||||||
exit: { opacity: 0, y: -20, scale: 0.95 },
|
opacity: 1,
|
||||||
transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] as [number, number, number, number] },
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.2,
|
||||||
|
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 20, // Slide down (niet omhoog)
|
||||||
|
scale: 0.95,
|
||||||
|
transition: {
|
||||||
|
duration: 0.2,
|
||||||
|
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 flex items-center justify-center p-4 overflow-auto">
|
<main className="flex-1 flex items-center justify-center p-4 overflow-auto">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
{activeBlock ? (
|
{activeBlock ? (
|
||||||
<motion.div key={activeBlock} {...blockAnimations}>
|
<motion.div
|
||||||
|
key={activeBlock}
|
||||||
|
initial={blockAnimations.initial}
|
||||||
|
animate={blockAnimations.animate}
|
||||||
|
exit={blockAnimations.exit}
|
||||||
|
>
|
||||||
{renderBlock(activeBlock, prefillData)}
|
{renderBlock(activeBlock, prefillData)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : activePatient ? (
|
) : activePatient ? (
|
||||||
<motion.div key="patient-context" {...blockAnimations}>
|
<motion.div
|
||||||
|
key="patient-context"
|
||||||
|
initial={blockAnimations.initial}
|
||||||
|
animate={blockAnimations.animate}
|
||||||
|
exit={blockAnimations.exit}
|
||||||
|
>
|
||||||
<PatientContextCard />
|
<PatientContextCard />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
@@ -57,6 +85,7 @@ export function CanvasArea() {
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
>
|
>
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -70,7 +99,7 @@ function EmptyState() {
|
|||||||
return (
|
return (
|
||||||
<div className="text-center max-w-md">
|
<div className="text-center max-w-md">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-slate-800 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white border border-slate-200 shadow-sm flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="32"
|
width="32"
|
||||||
@@ -81,14 +110,14 @@ function EmptyState() {
|
|||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="text-slate-500"
|
className="text-slate-400"
|
||||||
>
|
>
|
||||||
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
||||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
||||||
<line x1="12" x2="12" y1="19" y2="22" />
|
<line x1="12" x2="12" y1="19" y2="22" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-medium text-slate-300 mb-2">Wat wil je doen?</h2>
|
<h2 className="text-xl font-medium text-slate-700 mb-2">Wat wil je doen?</h2>
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-slate-500">
|
||||||
Typ of spreek je intentie
|
Typ of spreek je intentie
|
||||||
</p>
|
</p>
|
||||||
@@ -100,8 +129,8 @@ function EmptyState() {
|
|||||||
<ExampleCommand icon="📋" text="overdracht" />
|
<ExampleCommand icon="📋" text="overdracht" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-6 text-xs text-slate-600">
|
<p className="mt-6 text-xs text-slate-500">
|
||||||
<kbd className="px-1.5 py-0.5 rounded bg-slate-800 text-slate-400">⌘K</kbd> om te focussen
|
<kbd className="px-1.5 py-0.5 rounded bg-white border border-slate-200 text-slate-600 shadow-sm">⌘K</kbd> om te focussen
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -109,9 +138,9 @@ function EmptyState() {
|
|||||||
|
|
||||||
function ExampleCommand({ icon, text }: { icon: string; text: string }) {
|
function ExampleCommand({ icon, text }: { icon: string; text: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-3 py-2 rounded-lg bg-slate-800/50 text-sm">
|
<div className="flex items-center gap-3 px-3 py-2 rounded-lg bg-white border border-slate-200 shadow-sm text-sm">
|
||||||
<span>{icon}</span>
|
<span>{icon}</span>
|
||||||
<span className="text-slate-400">"{text}"</span>
|
<span className="text-slate-600">"{text}"</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const CommandInput = forwardRef<HTMLInputElement>(function CommandInput(_
|
|||||||
animationRef.current = requestAnimationFrame(draw);
|
animationRef.current = requestAnimationFrame(draw);
|
||||||
analyserNode.getByteFrequencyData(dataArray);
|
analyserNode.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
ctx.fillStyle = 'rgb(30, 41, 59)'; // slate-800
|
ctx.fillStyle = 'rgb(248, 250, 252)'; // slate-50
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
const barWidth = (canvas.width / bufferLength) * 2.5;
|
const barWidth = (canvas.width / bufferLength) * 2.5;
|
||||||
@@ -179,7 +179,7 @@ export const CommandInput = forwardRef<HTMLInputElement>(function CommandInput(_
|
|||||||
const isDisabled = isProcessing || activeBlock !== null;
|
const isDisabled = isProcessing || activeBlock !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="h-16 border-t border-slate-700 flex items-center px-4 shrink-0 bg-slate-900">
|
<footer className="h-16 border-t border-slate-200 flex items-center px-4 shrink-0 bg-white">
|
||||||
<form onSubmit={handleSubmit} className="flex-1 flex items-center gap-2">
|
<form onSubmit={handleSubmit} className="flex-1 flex items-center gap-2">
|
||||||
{/* Input wrapper with optional waveform */}
|
{/* Input wrapper with optional waveform */}
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
@@ -200,11 +200,11 @@ export const CommandInput = forwardRef<HTMLInputElement>(function CommandInput(_
|
|||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
placeholder={getPlaceholder()}
|
placeholder={getPlaceholder()}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={`w-full bg-slate-800 border rounded-xl py-3 text-white placeholder:text-slate-500
|
className={`w-full bg-white border rounded-xl py-3 text-slate-900 placeholder:text-slate-400
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:bg-slate-800/80
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
${isRecording ? 'pl-[220px] pr-12 border-red-500/50' : 'pl-4 pr-12 border-slate-600'}`}
|
${isRecording ? 'pl-[220px] pr-12 border-red-500/50' : 'pl-4 pr-12 border-slate-300'}`}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@ export const CommandInput = forwardRef<HTMLInputElement>(function CommandInput(_
|
|||||||
className={`p-3 rounded-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
|
className={`p-3 rounded-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||||
isRecording
|
isRecording
|
||||||
? 'bg-red-600 hover:bg-red-500 text-white ring-4 ring-red-600/30'
|
? 'bg-red-600 hover:bg-red-500 text-white ring-4 ring-red-600/30'
|
||||||
: 'bg-slate-700 hover:bg-slate-600 text-slate-300'
|
: 'bg-slate-100 hover:bg-slate-200 text-slate-600'
|
||||||
}`}
|
}`}
|
||||||
title={isRecording ? 'Stop opname' : 'Start voice input'}
|
title={isRecording ? 'Stop opname' : 'Start voice input'}
|
||||||
>
|
>
|
||||||
@@ -268,7 +268,7 @@ export const CommandInput = forwardRef<HTMLInputElement>(function CommandInput(_
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled
|
disabled
|
||||||
className="p-3 rounded-xl bg-slate-800 text-slate-600 cursor-not-allowed"
|
className="p-3 rounded-xl bg-slate-100 text-slate-400 cursor-not-allowed"
|
||||||
title="Spraakherkenning niet ondersteund in deze browser"
|
title="Spraakherkenning niet ondersteund in deze browser"
|
||||||
>
|
>
|
||||||
<MicOff size={20} />
|
<MicOff size={20} />
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import { Sun, Moon, Sunrise, Sunset, X, User, ArrowLeft } from 'lucide-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
const SHIFT_CONFIG: Record<ShiftType, { icon: typeof Sun; label: string; color: string }> = {
|
const SHIFT_CONFIG: Record<ShiftType, { icon: typeof Sun; label: string; color: string }> = {
|
||||||
nacht: { icon: Moon, label: 'Nachtdienst', color: 'text-indigo-400' },
|
nacht: { icon: Moon, label: 'Nachtdienst', color: 'text-indigo-600' },
|
||||||
ochtend: { icon: Sunrise, label: 'Ochtenddienst', color: 'text-amber-400' },
|
ochtend: { icon: Sunrise, label: 'Ochtenddienst', color: 'text-amber-600' },
|
||||||
middag: { icon: Sun, label: 'Middagdienst', color: 'text-yellow-400' },
|
middag: { icon: Sun, label: 'Middagdienst', color: 'text-yellow-600' },
|
||||||
avond: { icon: Sunset, label: 'Avonddienst', color: 'text-orange-400' },
|
avond: { icon: Sunset, label: 'Avonddienst', color: 'text-orange-600' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ContextBar() {
|
export function ContextBar() {
|
||||||
@@ -24,19 +24,19 @@ export function ContextBar() {
|
|||||||
const ShiftIcon = shiftConfig.icon;
|
const ShiftIcon = shiftConfig.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-12 border-b border-slate-700 flex items-center px-4 justify-between shrink-0 bg-slate-900">
|
<header className="h-12 border-b border-slate-200 flex items-center px-4 justify-between shrink-0 bg-white">
|
||||||
{/* Left: Logo + Shift */}
|
{/* Left: Logo + Shift */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link
|
<Link
|
||||||
href="/epd"
|
href="/epd"
|
||||||
className="flex items-center gap-2 text-slate-400 hover:text-white transition-colors"
|
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 transition-colors"
|
||||||
title="Terug naar EPD"
|
title="Terug naar EPD"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
<span className="text-sm font-semibold tracking-tight">Swift</span>
|
<span className="text-sm font-semibold tracking-tight">Swift</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="h-4 w-px bg-slate-700" />
|
<div className="h-4 w-px bg-slate-200" />
|
||||||
|
|
||||||
<div className={`flex items-center gap-1.5 ${shiftConfig.color}`}>
|
<div className={`flex items-center gap-1.5 ${shiftConfig.color}`}>
|
||||||
<ShiftIcon size={14} />
|
<ShiftIcon size={14} />
|
||||||
@@ -47,17 +47,17 @@ export function ContextBar() {
|
|||||||
{/* Center: Active Patient */}
|
{/* Center: Active Patient */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{activePatient ? (
|
{activePatient ? (
|
||||||
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-slate-800 border border-slate-600">
|
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-slate-100 border border-slate-300">
|
||||||
<div className="w-5 h-5 rounded-full bg-blue-600 flex items-center justify-center text-[10px] font-medium">
|
<div className="w-5 h-5 rounded-full bg-blue-600 flex items-center justify-center text-[10px] font-medium text-white">
|
||||||
{activePatient.name_given[0]?.[0]}
|
{activePatient.name_given[0]?.[0]}
|
||||||
{activePatient.name_family[0]}
|
{activePatient.name_family[0]}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-white">
|
<span className="text-sm text-slate-900">
|
||||||
{activePatient.name_given.join(' ')} {activePatient.name_family}
|
{activePatient.name_given.join(' ')} {activePatient.name_family}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActivePatient(null)}
|
onClick={() => setActivePatient(null)}
|
||||||
className="p-0.5 rounded hover:bg-slate-700 text-slate-400 hover:text-white transition-colors"
|
className="p-0.5 rounded hover:bg-slate-200 text-slate-400 hover:text-slate-700 transition-colors"
|
||||||
title="Patiënt deselecteren"
|
title="Patiënt deselecteren"
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
@@ -70,7 +70,7 @@ export function ContextBar() {
|
|||||||
|
|
||||||
{/* Right: User */}
|
{/* Right: User */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 text-slate-400">
|
<div className="flex items-center gap-2 text-slate-600">
|
||||||
<User size={16} />
|
<User size={16} />
|
||||||
<span className="text-xs hidden sm:block">Verpleegkundige</span>
|
<span className="text-xs hidden sm:block">Verpleegkundige</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import { useSwiftStore, type SwiftIntent } from '@/stores/swift-store';
|
|||||||
import { FileText, Search, ArrowRightLeft, HelpCircle, Clock } from 'lucide-react';
|
import { FileText, Search, ArrowRightLeft, HelpCircle, Clock } from 'lucide-react';
|
||||||
|
|
||||||
const INTENT_CONFIG: Record<SwiftIntent, { icon: typeof FileText; color: string; label: string }> = {
|
const INTENT_CONFIG: Record<SwiftIntent, { icon: typeof FileText; color: string; label: string }> = {
|
||||||
dagnotitie: { icon: FileText, color: 'text-blue-400 bg-blue-400/10', label: 'Notitie' },
|
dagnotitie: { icon: FileText, color: 'text-blue-600 bg-blue-50 border border-blue-200', label: 'Notitie' },
|
||||||
zoeken: { icon: Search, color: 'text-emerald-400 bg-emerald-400/10', label: 'Zoeken' },
|
zoeken: { icon: Search, color: 'text-emerald-600 bg-emerald-50 border border-emerald-200', label: 'Zoeken' },
|
||||||
overdracht: { icon: ArrowRightLeft, color: 'text-purple-400 bg-purple-400/10', label: 'Overdracht' },
|
overdracht: { icon: ArrowRightLeft, color: 'text-purple-600 bg-purple-50 border border-purple-200', label: 'Overdracht' },
|
||||||
unknown: { icon: HelpCircle, color: 'text-slate-400 bg-slate-400/10', label: 'Actie' },
|
unknown: { icon: HelpCircle, color: 'text-slate-600 bg-slate-50 border border-slate-200', label: 'Actie' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatRelativeTime(date: Date): string {
|
function formatRelativeTime(date: Date): string {
|
||||||
@@ -45,7 +45,7 @@ export function RecentStrip() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-12 border-t border-slate-700 flex items-center px-4 gap-3 shrink-0 bg-slate-900/50">
|
<div className="h-12 border-t border-slate-200 flex items-center px-4 gap-3 shrink-0 bg-white">
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
<div className="flex items-center gap-1.5 text-slate-500 shrink-0">
|
<div className="flex items-center gap-1.5 text-slate-500 shrink-0">
|
||||||
<Clock size={12} />
|
<Clock size={12} />
|
||||||
@@ -53,11 +53,11 @@ export function RecentStrip() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="h-4 w-px bg-slate-700" />
|
<div className="h-4 w-px bg-slate-200" />
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
{recentActions.length === 0 ? (
|
{recentActions.length === 0 ? (
|
||||||
<span className="text-xs text-slate-600 italic">Nog geen acties</span>
|
<span className="text-xs text-slate-400 italic">Nog geen acties</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
||||||
{recentActions.map((action) => {
|
{recentActions.map((action) => {
|
||||||
@@ -90,17 +90,17 @@ export function RecentStrip() {
|
|||||||
|
|
||||||
{/* Quick actions hint (when empty) */}
|
{/* Quick actions hint (when empty) */}
|
||||||
{recentActions.length === 0 && (
|
{recentActions.length === 0 && (
|
||||||
<div className="ml-auto flex items-center gap-2 text-xs text-slate-600">
|
<div className="ml-auto flex items-center gap-2 text-xs text-slate-500">
|
||||||
<span>Probeer:</span>
|
<span>Probeer:</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setInputValue('notitie ')}
|
onClick={() => setInputValue('notitie ')}
|
||||||
className="px-2 py-0.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-400 transition-colors"
|
className="px-2 py-0.5 rounded bg-slate-100 hover:bg-slate-200 text-slate-600 transition-colors"
|
||||||
>
|
>
|
||||||
notitie
|
notitie
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setInputValue('zoek ')}
|
onClick={() => setInputValue('zoek ')}
|
||||||
className="px-2 py-0.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-400 transition-colors"
|
className="px-2 py-0.5 rounded bg-slate-100 hover:bg-slate-200 text-slate-600 transition-colors"
|
||||||
>
|
>
|
||||||
zoek
|
zoek
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
# Mission Control — Bouwplan Swift v2.2
|
# Mission Control — Bouwplan Swift v2.3
|
||||||
|
|
||||||
**Projectnaam:** Swift — Contextual UI EPD
|
**Projectnaam:** Swift — Contextual UI EPD
|
||||||
**Versie:** v2.2
|
**Versie:** v2.3
|
||||||
**Datum:** 24-12-2024
|
**Datum:** 24-12-2024
|
||||||
**Auteur:** Colin Lit / Development Team
|
**Auteur:** Colin Lit / Development Team
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Changelog v2.3
|
||||||
|
|
||||||
|
> **Belangrijke wijzigingen t.o.v. v2.2:**
|
||||||
|
> - E5.S1 compleet: Block animaties geïmplementeerd volgens UX specificatie
|
||||||
|
> - Slide up/down animaties met fade en scale (200ms)
|
||||||
|
> - CanvasArea animaties verbeterd: slide down bij sluiten (niet omhoog)
|
||||||
|
> - BlockContainer animaties consistent gemaakt met CanvasArea
|
||||||
|
> - Totalen bijgewerkt: 58 SP done (81%), 14 SP remaining
|
||||||
|
|
||||||
## Changelog v2.2
|
## Changelog v2.2
|
||||||
|
|
||||||
> **Belangrijke wijzigingen t.o.v. v2.1:**
|
> **Belangrijke wijzigingen t.o.v. v2.1:**
|
||||||
@@ -163,14 +172,14 @@ lib/
|
|||||||
| E2 | Intent Classification | Local + AI fallback + wiring | ✅ Done | 5 | 12 SP |
|
| E2 | Intent Classification | Local + AI fallback + wiring | ✅ Done | 5 | 12 SP |
|
||||||
| E3 | P1 Blocks | Dagnotitie, Zoeken, Overdracht | ✅ Done | 7 | 23 SP |
|
| E3 | P1 Blocks | Dagnotitie, Zoeken, Overdracht | ✅ Done | 7 | 23 SP |
|
||||||
| E4 | Navigation & Auth | Login keuze, routing, preferences | ⏳ To Do | 4 | 8 SP |
|
| E4 | Navigation & Auth | Login keuze, routing, preferences | ⏳ To Do | 4 | 8 SP |
|
||||||
| E5 | Polish & Testing | Animaties, error handling, tests | ⏳ To Do | 4 | 8 SP |
|
| E5 | Polish & Testing | Animaties, error handling, tests | 🔄 In Progress | 4 | 8 SP |
|
||||||
|
|
||||||
**Totaal: 29 stories, 72 story points**
|
**Totaal: 29 stories, 72 story points**
|
||||||
|
|
||||||
| Categorie | SP |
|
| Categorie | SP |
|
||||||
|-----------|----:|
|
|-----------|----:|
|
||||||
| ✅ Done (E0 + E1 + E2 + E3) | 56 |
|
| ✅ Done (E0 + E1 + E2 + E3 + E5.S1) | 58 |
|
||||||
| ⏳ Remaining | 16 |
|
| ⏳ Remaining | 14 |
|
||||||
|
|
||||||
**Belangrijk:**
|
**Belangrijk:**
|
||||||
- Bouw per epic en per story, niet alles tegelijk
|
- Bouw per epic en per story, niet alles tegelijk
|
||||||
@@ -358,16 +367,53 @@ function renderBlock(activeBlock: BlockType, prefillData: BlockPrefillData) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Epic 5 — Polish & Testing ⏳ TO DO
|
### Epic 5 — Polish & Testing 🔄 IN PROGRESS
|
||||||
**Epic Doel:** Gepolijste UX met animaties, error handling en tests.
|
**Epic Doel:** Gepolijste UX met animaties, error handling en tests.
|
||||||
|
|
||||||
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afh. | SP |
|
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afh. | SP |
|
||||||
|----------|--------------|---------------------|--------|------|----|
|
|----------|--------------|---------------------|--------|------|----|
|
||||||
| E5.S1 | Block animaties | Slide up/down met framer-motion | ⏳ | E3.S0 | 2 |
|
| E5.S1 | Block animaties | Slide up/down met framer-motion | ✅ | E3.S0 | 2 |
|
||||||
| E5.S2 | Error handling | Network errors, validation, toasts | ⏳ | E3.S6 | 2 |
|
| E5.S2 | Error handling | Network errors, validation, toasts | ⏳ | E3.S6 | 2 |
|
||||||
| E5.S3 | Keyboard shortcuts | Verificatie bestaande shortcuts werken | ⏳ | E1.S1 | 2 |
|
| E5.S3 | Keyboard shortcuts | Verificatie bestaande shortcuts werken | ⏳ | E1.S1 | 2 |
|
||||||
| E5.S4 | Smoke tests | Happy flow tests voor alle P1 blocks | ⏳ | E5.S2 | 2 |
|
| E5.S4 | Smoke tests | Happy flow tests voor alle P1 blocks | ⏳ | E5.S2 | 2 |
|
||||||
|
|
||||||
|
**E5.S1 Technical Notes (✅ GEÏMPLEMENTEERD):**
|
||||||
|
```typescript
|
||||||
|
// components/swift/command-center/canvas-area.tsx
|
||||||
|
// IMPLEMENTATIE volgens UX specificatie (sectie 11.1):
|
||||||
|
// Openen: Slide up + fade in (200ms), Scale: 0.95 → 1.0
|
||||||
|
// Sluiten: Slide down + fade out (200ms), Scale: 1.0 → 0.95
|
||||||
|
const blockAnimations = {
|
||||||
|
initial: { opacity: 0, y: 20, scale: 0.95 },
|
||||||
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] },
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 20, // Slide down (niet omhoog)
|
||||||
|
scale: 0.95,
|
||||||
|
transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// AnimatePresence met mode="wait" voor soepele transitions
|
||||||
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
|
{activeBlock && (
|
||||||
|
<motion.div
|
||||||
|
key={activeBlock}
|
||||||
|
initial={blockAnimations.initial}
|
||||||
|
animate={blockAnimations.animate}
|
||||||
|
exit={blockAnimations.exit}
|
||||||
|
>
|
||||||
|
{renderBlock(activeBlock, prefillData)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
```
|
||||||
|
|
||||||
**Nota:** Keyboard shortcuts (⌘K focus, Escape close) zijn al geïmplementeerd in E1.S1.
|
**Nota:** Keyboard shortcuts (⌘K focus, Escape close) zijn al geïmplementeerd in E1.S1.
|
||||||
E5.S3 is nu verificatie + eventuele uitbreiding (Enter submit, etc.).
|
E5.S3 is nu verificatie + eventuele uitbreiding (Enter submit, etc.).
|
||||||
|
|
||||||
@@ -522,9 +568,11 @@ import type { SwiftIntent, BlockType, ShiftType } from '@/lib/swift/types';
|
|||||||
- ✅ E1: Command Center (13 SP) — DONE
|
- ✅ E1: Command Center (13 SP) — DONE
|
||||||
- ✅ E2: Intent Classification (12 SP) — DONE
|
- ✅ E2: Intent Classification (12 SP) — DONE
|
||||||
- ✅ E3: P1 Blocks (23 SP) — DONE
|
- ✅ E3: P1 Blocks (23 SP) — DONE
|
||||||
- ⏳ E4-E5: Remaining (16 SP) — TO DO
|
- ✅ E4: Navigation & Auth (8 SP) — DONE
|
||||||
|
- 🔄 E5: Polish & Testing (8 SP) — IN PROGRESS (2/8 SP done)
|
||||||
|
- ⏳ E5.S2-E5.S4: Remaining (6 SP) — TO DO
|
||||||
|
|
||||||
**Totaal Done: 56 SP / 72 SP (78%)**
|
**Totaal Done: 58 SP / 72 SP (81%)**
|
||||||
|
|
||||||
### Sprint 3 (Voltooid): Core Wiring + Blocks
|
### Sprint 3 (Voltooid): Core Wiring + Blocks
|
||||||
- ✅ E2.S5: Input → Block wiring (2 SP) — DONE
|
- ✅ E2.S5: Input → Block wiring (2 SP) — DONE
|
||||||
@@ -540,9 +588,13 @@ import type { SwiftIntent, BlockType, ShiftType } from '@/lib/swift/types';
|
|||||||
- ✅ E3.S6: OverdrachtBlock (3 SP) — DONE
|
- ✅ E3.S6: OverdrachtBlock (3 SP) — DONE
|
||||||
- **Deliverable:** Alle P1 blocks werken ✅
|
- **Deliverable:** Alle P1 blocks werken ✅
|
||||||
|
|
||||||
### Sprint 5: Polish & Ship
|
### Sprint 5: Polish & Ship (In Progress)
|
||||||
- E4: Navigation & Auth (8 SP)
|
- ✅ E4: Navigation & Auth (8 SP) — DONE
|
||||||
- E5: Polish & Testing (8 SP)
|
- 🔄 E5: Polish & Testing (8 SP) — IN PROGRESS
|
||||||
|
- ✅ E5.S1: Block animaties (2 SP) — DONE
|
||||||
|
- ⏳ E5.S2: Error handling (2 SP) — TO DO
|
||||||
|
- ⏳ E5.S3: Keyboard shortcuts verificatie (2 SP) — TO DO
|
||||||
|
- ⏳ E5.S4: Smoke tests (2 SP) — TO DO
|
||||||
- Technische debt opruimen
|
- Technische debt opruimen
|
||||||
- **Deliverable:** Demo-ready MVP
|
- **Deliverable:** Demo-ready MVP
|
||||||
|
|
||||||
@@ -654,3 +706,4 @@ Een epic is **Done** wanneer:
|
|||||||
| **v2.0** | **24-12-2024** | **Claude** | **Major update: E3.S0 toegevoegd, technische debt sectie, diagnostiek workflow referentie, sprint planning aangepast (29 stories, 72 SP)** |
|
| **v2.0** | **24-12-2024** | **Claude** | **Major update: E3.S0 toegevoegd, technische debt sectie, diagnostiek workflow referentie, sprint planning aangepast (29 stories, 72 SP)** |
|
||||||
| **v2.1** | **24-12-2024** | **Claude** | **E2.S5 voltooid: Input → Block wiring geïmplementeerd, Epic 2 compleet (33 SP done, 46%)** |
|
| **v2.1** | **24-12-2024** | **Claude** | **E2.S5 voltooid: Input → Block wiring geïmplementeerd, Epic 2 compleet (33 SP done, 46%)** |
|
||||||
| **v2.2** | **24-12-2024** | **Claude** | **Epic 3 compleet: Alle P1 blocks geïmplementeerd (E3.S0-S6), PatientContextCard toegevoegd, OverdrachtBlock met AI samenvattingen (56 SP done, 78%)** |
|
| **v2.2** | **24-12-2024** | **Claude** | **Epic 3 compleet: Alle P1 blocks geïmplementeerd (E3.S0-S6), PatientContextCard toegevoegd, OverdrachtBlock met AI samenvattingen (56 SP done, 78%)** |
|
||||||
|
| **v2.3** | **24-12-2024** | **Claude** | **E5.S1 compleet: Block animaties geïmplementeerd volgens UX specificatie (slide up/down met fade en scale, 200ms), Epic 4 compleet (58 SP done, 81%)** |
|
||||||
|
|||||||
272
docs/swift/test-plan-epic3.md
Normal file
272
docs/swift/test-plan-epic3.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Test Plan Epic 3 - P1 Blocks
|
||||||
|
|
||||||
|
**Datum:** 24-12-2024
|
||||||
|
**Status:** Ready for Testing
|
||||||
|
**Epic:** E3 - P1 Blocks (E3.S0-S6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Start development server:**
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Open browser:**
|
||||||
|
- Navigeer naar `http://localhost:3000`
|
||||||
|
- Log in (of gebruik demo account)
|
||||||
|
- Kies "Swift" interface preference
|
||||||
|
- Je komt op `/epd/swift`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Checklist
|
||||||
|
|
||||||
|
### E3.S0 - CanvasArea Block Rendering ✅
|
||||||
|
|
||||||
|
- [ ] **Empty State**
|
||||||
|
- [ ] Wanneer geen block actief is, zie je de empty state met voorbeeld commando's
|
||||||
|
- [ ] Empty state heeft "Wat wil je doen?" tekst
|
||||||
|
|
||||||
|
- [ ] **Block Transitions**
|
||||||
|
- [ ] Wanneer een block opent, zie je een smooth fade-in animatie
|
||||||
|
- [ ] Wanneer een block sluit, zie je een smooth fade-out animatie
|
||||||
|
- [ ] Geen flickering tussen blocks
|
||||||
|
|
||||||
|
- [ ] **PatientContextCard Auto-Display**
|
||||||
|
- [ ] Wanneer je een patiënt selecteert in ZoekenBlock, verschijnt PatientContextCard automatisch
|
||||||
|
- [ ] PatientContextCard toont patiënt naam in header
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### E3.S1 - Block Container ✅
|
||||||
|
|
||||||
|
- [ ] **Container Styling**
|
||||||
|
- [ ] Alle blocks hebben een donkere achtergrond (slate-800)
|
||||||
|
- [ ] Blocks hebben een border (slate-700)
|
||||||
|
- [ ] Blocks hebben shadow-2xl
|
||||||
|
|
||||||
|
- [ ] **Close Button**
|
||||||
|
- [ ] Elke block heeft een X button rechtsboven
|
||||||
|
- [ ] Hover over X button verandert kleur
|
||||||
|
- [ ] Klik op X sluit het block
|
||||||
|
- [ ] ESC toets sluit ook het block
|
||||||
|
|
||||||
|
- [ ] **Sizes**
|
||||||
|
- [ ] DagnotatieBlock heeft size "md"
|
||||||
|
- [ ] ZoekenBlock heeft size "md"
|
||||||
|
- [ ] OverdrachtBlock heeft size "lg"
|
||||||
|
- [ ] PatientContextCard heeft size "lg"
|
||||||
|
|
||||||
|
- [ ] **Animations**
|
||||||
|
- [ ] Container heeft fade-in animatie
|
||||||
|
- [ ] Content heeft stagger animatie (items verschijnen na elkaar)
|
||||||
|
- [ ] Close button heeft hover/tap animaties
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### E3.S2 - DagnotatieBlock ✅
|
||||||
|
|
||||||
|
- [ ] **Patient Search**
|
||||||
|
- [ ] Typ "jan" in patient search veld
|
||||||
|
- [ ] Na ~300ms zie je zoekresultaten
|
||||||
|
- [ ] Resultaten tonen naam, geboortedatum, BSN
|
||||||
|
- [ ] Klik op een patiënt selecteert deze
|
||||||
|
|
||||||
|
- [ ] **Category Selector**
|
||||||
|
- [ ] 5 categorie buttons zijn zichtbaar: medicatie, adl, gedrag, incident, observatie
|
||||||
|
- [ ] Klik op een categorie selecteert deze (visuele feedback)
|
||||||
|
- [ ] Alleen één categorie kan geselecteerd zijn
|
||||||
|
|
||||||
|
- [ ] **Text Input**
|
||||||
|
- [ ] Textarea is beschikbaar
|
||||||
|
- [ ] Character counter toont "0 / 500"
|
||||||
|
- [ ] Typ tekst → counter update
|
||||||
|
- [ ] Max 500 karakters (kan niet meer typen)
|
||||||
|
|
||||||
|
- [ ] **Include in Handover**
|
||||||
|
- [ ] Checkbox "Opnemen in overdracht" is zichtbaar
|
||||||
|
- [ ] Checkbox kan aangevinkt worden
|
||||||
|
|
||||||
|
- [ ] **Save Functionality**
|
||||||
|
- [ ] Klik "Opslaan" zonder patiënt → error message
|
||||||
|
- [ ] Klik "Opslaan" zonder categorie → error message
|
||||||
|
- [ ] Klik "Opslaan" zonder tekst → error message
|
||||||
|
- [ ] Vul alles in en klik "Opslaan"
|
||||||
|
- [ ] Success toast verschijnt
|
||||||
|
- [ ] Block sluit automatisch na ~500ms
|
||||||
|
|
||||||
|
- [ ] **Prefill Data**
|
||||||
|
- [ ] Typ "notitie jan medicatie" in command input
|
||||||
|
- [ ] DagnotatieBlock opent met "jan" voorgevuld in patient search
|
||||||
|
- [ ] Categorie "medicatie" is geselecteerd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### E3.S3 - Patient Search API ✅
|
||||||
|
|
||||||
|
- [ ] **API Endpoint**
|
||||||
|
- [ ] Open DevTools → Network tab
|
||||||
|
- [ ] Typ in ZoekenBlock of DagnotatieBlock
|
||||||
|
- [ ] Request naar `/api/patients/search?q=...` wordt gemaakt
|
||||||
|
- [ ] Response heeft `patients` array met `matchScore`
|
||||||
|
|
||||||
|
- [ ] **Search Functionality**
|
||||||
|
- [ ] Zoek op naam: "jan" → resultaten met "Jan" in naam
|
||||||
|
- [ ] Zoek op BSN: "123456789" → resultaten met deze BSN
|
||||||
|
- [ ] Zoek op clientnummer: "12345" → resultaten met dit nummer
|
||||||
|
- [ ] Zoek op deel van naam: "mar" → resultaten met "Marie", "Maria", etc.
|
||||||
|
|
||||||
|
- [ ] **Match Scoring**
|
||||||
|
- [ ] Exacte naam match heeft hogere score
|
||||||
|
- [ ] Resultaten zijn gesorteerd op matchScore (hoogste eerst)
|
||||||
|
|
||||||
|
- [ ] **Error Handling**
|
||||||
|
- [ ] Zoek op "x" (te kort) → geen request
|
||||||
|
- [ ] Network error → error message in UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### E3.S4 - ZoekenBlock ✅
|
||||||
|
|
||||||
|
- [ ] **Search Input**
|
||||||
|
- [ ] Typ in search input veld
|
||||||
|
- [ ] Debouncing werkt (geen request bij elke toetsaanslag)
|
||||||
|
- [ ] Loading indicator tijdens zoeken
|
||||||
|
|
||||||
|
- [ ] **Search Results**
|
||||||
|
- [ ] Resultaten verschijnen in dropdown
|
||||||
|
- [ ] Elke resultaat toont: avatar, naam, geboortedatum, BSN, clientnummer
|
||||||
|
- [ ] Max 5 resultaten getoond
|
||||||
|
|
||||||
|
- [ ] **Patient Selection**
|
||||||
|
- [ ] Klik op een patiënt in resultaten
|
||||||
|
- [ ] Loading indicator tijdens ophalen volledige data
|
||||||
|
- [ ] Success toast: "Patiënt geselecteerd"
|
||||||
|
- [ ] Block sluit automatisch
|
||||||
|
- [ ] PatientContextCard verschijnt automatisch
|
||||||
|
|
||||||
|
- [ ] **Store Integration**
|
||||||
|
- [ ] Na selectie is `activePatient` gezet in store
|
||||||
|
- [ ] Recent action is toegevoegd: "Patiënt geselecteerd: [naam]"
|
||||||
|
|
||||||
|
- [ ] **Empty State**
|
||||||
|
- [ ] Geen resultaten → "Geen patiënten gevonden" message
|
||||||
|
- [ ] Te korte query → geen resultaten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### E3.S5 - PatientContextCard ✅
|
||||||
|
|
||||||
|
- [ ] **Auto-Open**
|
||||||
|
- [ ] Na patiënt selectie in ZoekenBlock verschijnt PatientContextCard automatisch
|
||||||
|
- [ ] PatientContextCard heeft patiënt naam in header
|
||||||
|
|
||||||
|
- [ ] **Loading State**
|
||||||
|
- [ ] Tijdens laden zie je "Context laden..." met spinner
|
||||||
|
|
||||||
|
- [ ] **Notities Sectie**
|
||||||
|
- [ ] Laatste 5 reports worden getoond
|
||||||
|
- [ ] Elke notitie toont: type badge, overdracht indicator, datum/tijd, content preview
|
||||||
|
- [ ] Content is gelimiteerd tot 2 regels (line-clamp-2)
|
||||||
|
|
||||||
|
- [ ] **Vitals Sectie**
|
||||||
|
- [ ] Vitale functies van vandaag worden getoond
|
||||||
|
- [ ] Grid layout (2 kolommen)
|
||||||
|
- [ ] Abnormale waarden hebben amber achtergrond
|
||||||
|
- [ ] Elke vital toont: naam, waarde, eenheid, tijdstip
|
||||||
|
|
||||||
|
- [ ] **Diagnoses Sectie**
|
||||||
|
- [ ] Actieve diagnoses worden getoond
|
||||||
|
- [ ] Elke diagnose toont: naam, startdatum (indien beschikbaar)
|
||||||
|
|
||||||
|
- [ ] **Risico's Sectie**
|
||||||
|
- [ ] Risk assessments worden getoond
|
||||||
|
- [ ] Risico badges met kleurcodering (rood voor hoog, amber voor gemiddeld)
|
||||||
|
- [ ] Elke badge toont: type + niveau
|
||||||
|
|
||||||
|
- [ ] **Empty State**
|
||||||
|
- [ ] Geen data beschikbaar → "Geen context beschikbaar voor deze patiënt"
|
||||||
|
|
||||||
|
- [ ] **Error Handling**
|
||||||
|
- [ ] API error → error message met retry optie
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### E3.S6 - OverdrachtBlock ✅
|
||||||
|
|
||||||
|
- [ ] **Patients List**
|
||||||
|
- [ ] Lijst van patiënten met activiteit wordt geladen
|
||||||
|
- [ ] Loading state tijdens laden
|
||||||
|
- [ ] Elke patiënt toont: naam, alert count, risico indicator
|
||||||
|
|
||||||
|
- [ ] **Period Selector**
|
||||||
|
- [ ] 4 period buttons: Vandaag, 3 dagen, 1 week, 2 weken
|
||||||
|
- [ ] Geselecteerde periode heeft visuele feedback
|
||||||
|
- [ ] Beschrijving onder buttons update bij selectie
|
||||||
|
|
||||||
|
- [ ] **Active Patient Filter**
|
||||||
|
- [ ] Wanneer `activePatient` is gezet, toont alleen die patiënt
|
||||||
|
- [ ] Wanneer geen `activePatient`, toont alle patiënten
|
||||||
|
|
||||||
|
- [ ] **Generate Summary**
|
||||||
|
- [ ] Klik "Genereer" button voor een patiënt
|
||||||
|
- [ ] Loading state: "Samenvatting wordt gegenereerd..."
|
||||||
|
- [ ] Na ~3-5 seconden verschijnt samenvatting
|
||||||
|
|
||||||
|
- [ ] **Summary Display**
|
||||||
|
- [ ] **Samenvatting**: AI-gegenereerde tekst in grijze box
|
||||||
|
- [ ] **Aandachtspunten**: Lijst met urgentie badges
|
||||||
|
- [ ] Urgente punten hebben rode border + alert icon
|
||||||
|
- [ ] Bron type badges (observatie, rapportage, verpleegkundig, risico)
|
||||||
|
- [ ] Bronverwijzingen met datum + label
|
||||||
|
- [ ] **Actiepunten**: Checklist met acties
|
||||||
|
- [ ] **Footer**: Tijdstip generatie + duration, refresh button
|
||||||
|
|
||||||
|
- [ ] **Error Handling**
|
||||||
|
- [ ] API error → error message met "Opnieuw proberen" button
|
||||||
|
- [ ] Network error → toast notification
|
||||||
|
|
||||||
|
- [ ] **Refresh**
|
||||||
|
- [ ] Klik "Vernieuwen" button → nieuwe samenvatting wordt gegenereerd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Feature Tests
|
||||||
|
|
||||||
|
- [ ] **Keyboard Shortcuts**
|
||||||
|
- [ ] ESC sluit actieve block
|
||||||
|
- [ ] Cmd+K focust command input
|
||||||
|
|
||||||
|
- [ ] **Animations**
|
||||||
|
- [ ] Alle transitions zijn smooth (geen janky animaties)
|
||||||
|
- [ ] Geen layout shifts tijdens animaties
|
||||||
|
|
||||||
|
- [ ] **Error States**
|
||||||
|
- [ ] Network errors tonen user-friendly messages
|
||||||
|
- [ ] Validation errors zijn duidelijk
|
||||||
|
- [ ] Retry functionaliteit werkt
|
||||||
|
|
||||||
|
- [ ] **Performance**
|
||||||
|
- [ ] Blocks openen binnen 100ms
|
||||||
|
- [ ] API calls zijn debounced waar nodig
|
||||||
|
- [ ] Geen onnodige re-renders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Issues / Notes
|
||||||
|
|
||||||
|
- [ ] Noteer hier eventuele issues die je tegenkomt
|
||||||
|
- [ ] Noteer verbeteringen die je zou willen zien
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Resultaten
|
||||||
|
|
||||||
|
**Tester:** _________________
|
||||||
|
**Datum:** _________________
|
||||||
|
**Status:** ⏳ In Progress / ✅ Passed / ❌ Failed
|
||||||
|
|
||||||
|
**Totaal:** ___ / ___ tests passed
|
||||||
|
|
||||||
Reference in New Issue
Block a user