feat(swift): implement agenda planning module (Epic 4 UI)
- Add AgendaBlock core component with list, create, cancel, reschedule modes - Implement AgendaListView with patient/type/location details and actions - Implement AgendaCreateForm with fuzzy patient search and validation - Implement AgendaCancelView with disambiguation support - Implement AgendaRescheduleForm with date/time picker - Integrate with server actions (create, cancel, reschedule) - Add radio-group UI component - Update documentation and status
This commit is contained in:
172
components/swift/artifacts/blocks/agenda-list-view.tsx
Normal file
172
components/swift/artifacts/blocks/agenda-list-view.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { nl } from 'date-fns/locale';
|
||||
import { Calendar, MapPin, Globe, Home, Clock, X, Info, ChevronRight, Ban } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
CalendarEvent,
|
||||
APPOINTMENT_TYPE_COLORS,
|
||||
APPOINTMENT_TYPES,
|
||||
LOCATION_CLASSES,
|
||||
LocationClassCode,
|
||||
AppointmentTypeCode
|
||||
} from '@/app/epd/agenda/types';
|
||||
|
||||
interface AgendaListViewProps {
|
||||
appointments?: CalendarEvent[];
|
||||
dateRange?: { start: Date; end: Date; label: string };
|
||||
onClose?: () => void;
|
||||
onCancelAppointment?: (encounter: CalendarEvent) => void;
|
||||
onViewDetails?: (encounter: CalendarEvent) => void;
|
||||
}
|
||||
|
||||
export function AgendaListView({
|
||||
appointments = [],
|
||||
dateRange,
|
||||
onClose,
|
||||
onCancelAppointment,
|
||||
onViewDetails
|
||||
}: AgendaListViewProps) {
|
||||
|
||||
const formatDateLabel = () => {
|
||||
if (dateRange?.label) {
|
||||
if (dateRange.label === 'vandaag' || dateRange.label === 'morgen') {
|
||||
const dateStr = format(dateRange.start, 'd MMMM', { locale: nl });
|
||||
return `Afspraken ${dateRange.label} - ${dateStr}`;
|
||||
}
|
||||
return `Afspraken ${dateRange.label}`;
|
||||
}
|
||||
// Fallback if no specific label
|
||||
if (appointments.length > 0) {
|
||||
return `Afspraken ${format(new Date(appointments[0].start), 'd MMMM', { locale: nl })}`;
|
||||
}
|
||||
return 'Afspraken';
|
||||
};
|
||||
|
||||
const getLocationIcon = (classCode: string) => {
|
||||
switch (classCode as LocationClassCode) {
|
||||
case 'AMB': return <MapPin className="h-3 w-3" />;
|
||||
case 'VR': return <Globe className="h-3 w-3" />;
|
||||
case 'HH': return <Home className="h-3 w-3" />;
|
||||
default: return <MapPin className="h-3 w-3" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<div className="flex items-center gap-2 text-teal-700">
|
||||
<Calendar className="h-5 w-5" />
|
||||
<h3 className="font-semibold text-lg capitalize">{formatDateLabel()}</h3>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8 hover:bg-gray-100 rounded-full">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{appointments.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center text-gray-500">
|
||||
<div className="bg-gray-50 p-4 rounded-full mb-3">
|
||||
<Calendar className="h-8 w-8 text-gray-400" />
|
||||
</div>
|
||||
<p className="font-medium">Geen afspraken gevonden</p>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
Er staan geen afspraken gepland voor deze periode.
|
||||
</p>
|
||||
<Button variant="outline" className="mt-4 gap-2 text-teal-600 border-teal-200 hover:bg-teal-50">
|
||||
<span className="text-lg leading-none">+</span> Maak nieuwe afspraak
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
appointments.map((evt) => {
|
||||
const encounter = evt.extendedProps.encounter;
|
||||
const patient = evt.extendedProps.patient;
|
||||
const typeCode = encounter.type_code as AppointmentTypeCode;
|
||||
const typeColor = APPOINTMENT_TYPE_COLORS[typeCode] || APPOINTMENT_TYPE_COLORS.overig;
|
||||
const classCode = encounter.class_code as LocationClassCode;
|
||||
|
||||
const startTime = format(new Date(evt.start), 'HH:mm');
|
||||
const endTime = evt.end ? format(new Date(evt.end), 'HH:mm') : '';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={evt.id}
|
||||
className="group border rounded-lg p-3 hover:border-teal-200 hover:shadow-sm transition-all bg-white"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
||||
<Clock className="h-3.5 w-3.5 text-gray-400" />
|
||||
<span>{startTime} {endTime && `- ${endTime}`}</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-left font-semibold text-teal-700 hover:underline mt-0.5"
|
||||
onClick={() => console.log('Open patient context', patient?.id)}
|
||||
>
|
||||
{evt.title}
|
||||
</button>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
style={{
|
||||
backgroundColor: typeColor.bg,
|
||||
color: typeColor.text,
|
||||
borderColor: typeColor.border
|
||||
}}
|
||||
className="whitespace-nowrap shadow-none"
|
||||
>
|
||||
{encounter.type_display || APPOINTMENT_TYPES[typeCode] || typeCode}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 mt-2">
|
||||
<div className="flex items-center gap-1.5" title={LOCATION_CLASSES[classCode]}>
|
||||
{getLocationIcon(classCode)}
|
||||
<span>{encounter.class_display || LOCATION_CLASSES[classCode] || classCode}</span>
|
||||
</div>
|
||||
{encounter.status === 'cancelled' && (
|
||||
<Badge variant="destructive" className="h-5 px-1.5">Geannuleerd</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 mt-3 pt-2 border-t border-gray-50 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-gray-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => onCancelAppointment?.(evt)}
|
||||
>
|
||||
Annuleren
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-teal-600 hover:bg-teal-50"
|
||||
onClick={() => onViewDetails?.(evt)}
|
||||
>
|
||||
Details <ChevronRight className="ml-1 h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-3 bg-gray-50 border-t text-center">
|
||||
<a
|
||||
href="/epd/agenda"
|
||||
className="text-xs font-medium text-teal-600 hover:text-teal-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
Open volledige agenda <ChevronRight className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user