- Adjusted date handling in the agenda API to ensure optional parameters for start, end, and label are correctly processed. - Enhanced chat input and action parser to support optional date labels for relative dates. - Updated agenda components to handle date ranges and loading states more effectively. - Improved error handling and loading indicators in the agenda block for better user experience. This commit ensures consistency in date handling across the application, aligning with the new requirements for relative date inputs.
307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Agenda View Component
|
|
*
|
|
* Client-side wrapper managing calendar state, view switching, and interactions.
|
|
*/
|
|
|
|
import React, { useState, useCallback, useRef, useTransition, useEffect } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { startOfWeek, endOfWeek, addDays, format } from 'date-fns';
|
|
import { toast } from '@/hooks/use-toast';
|
|
|
|
// Lazy load FullCalendar component (~150KB+ savings)
|
|
const AgendaCalendar = dynamic(
|
|
() => import('./agenda-calendar').then((mod) => mod.AgendaCalendar),
|
|
{
|
|
ssr: false,
|
|
loading: () => (
|
|
<div className="h-full bg-slate-50 rounded-lg flex items-center justify-center">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600" />
|
|
<span className="text-sm text-slate-500">Agenda laden...</span>
|
|
</div>
|
|
</div>
|
|
),
|
|
}
|
|
);
|
|
import { AgendaToolbar } from './agenda-toolbar';
|
|
import { AppointmentModal } from './appointment-modal';
|
|
import { RescheduleDialog } from './reschedule-dialog';
|
|
import { MiniCalendar } from './mini-calendar';
|
|
import { getEncounters, rescheduleEncounter } from '../actions';
|
|
import type { CalendarEvent, CalendarView } from '../types';
|
|
|
|
interface PendingReschedule {
|
|
event: CalendarEvent;
|
|
newStart: Date;
|
|
newEnd: Date | null;
|
|
}
|
|
|
|
interface AgendaViewProps {
|
|
initialEvents: CalendarEvent[];
|
|
initialDate?: Date;
|
|
highlightEncounterId?: string;
|
|
}
|
|
|
|
export function AgendaView({ initialEvents, initialDate, highlightEncounterId }: AgendaViewProps) {
|
|
const [events, setEvents] = useState<CalendarEvent[]>(initialEvents);
|
|
const [currentDate, setCurrentDate] = useState(initialDate || new Date());
|
|
const [currentView, setCurrentView] = useState<CalendarView>('timeGridWeek');
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
// Modal state
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [modalInitialDate, setModalInitialDate] = useState<Date | undefined>();
|
|
const [modalInitialStartTime, setModalInitialStartTime] = useState<string | undefined>();
|
|
const [modalInitialEndTime, setModalInitialEndTime] = useState<string | undefined>();
|
|
const [editingEvent, setEditingEvent] = useState<CalendarEvent | undefined>();
|
|
|
|
// Reschedule dialog state
|
|
const [pendingReschedule, setPendingReschedule] = useState<PendingReschedule | null>(null);
|
|
const [isRescheduling, setIsRescheduling] = useState(false);
|
|
|
|
const calendarRef = useRef<{ getApi: () => { prev: () => void; next: () => void; today: () => void; changeView: (view: string) => void; getDate: () => Date } } | null>(null);
|
|
|
|
// Auto-open appointment modal when navigating from a report
|
|
useEffect(() => {
|
|
if (highlightEncounterId && initialEvents.length > 0) {
|
|
const eventToOpen = initialEvents.find((e) => e.id === highlightEncounterId);
|
|
if (eventToOpen) {
|
|
setEditingEvent(eventToOpen);
|
|
setIsModalOpen(true);
|
|
}
|
|
}
|
|
}, [highlightEncounterId, initialEvents]);
|
|
|
|
// Fetch events when date range changes
|
|
const fetchEvents = useCallback(async (start: Date, end: Date) => {
|
|
startTransition(async () => {
|
|
const newEvents = await getEncounters({
|
|
startDate: start.toISOString(),
|
|
endDate: end.toISOString(),
|
|
});
|
|
setEvents(newEvents);
|
|
});
|
|
}, []);
|
|
|
|
// Handle view change
|
|
const handleViewChange = useCallback((view: CalendarView) => {
|
|
setCurrentView(view);
|
|
}, []);
|
|
|
|
// Handle navigation
|
|
const handleNavigate = useCallback((direction: 'prev' | 'next' | 'today') => {
|
|
let newDate = new Date(currentDate);
|
|
|
|
if (direction === 'today') {
|
|
newDate = new Date();
|
|
} else {
|
|
const days = currentView === 'timeGridDay' ? 1 : 7;
|
|
const offset = direction === 'prev' ? -days : days;
|
|
newDate = addDays(currentDate, offset);
|
|
}
|
|
|
|
setCurrentDate(newDate);
|
|
|
|
// Calculate date range for fetching
|
|
const start = currentView === 'timeGridDay'
|
|
? newDate
|
|
: startOfWeek(newDate, { weekStartsOn: 1 });
|
|
const end = currentView === 'timeGridDay'
|
|
? addDays(newDate, 1)
|
|
: endOfWeek(newDate, { weekStartsOn: 1 });
|
|
|
|
fetchEvents(start, end);
|
|
}, [currentDate, currentView, fetchEvents]);
|
|
|
|
// Handle date range change from calendar
|
|
const handleDateChange = useCallback((start: Date, end: Date) => {
|
|
setCurrentDate((prev) =>
|
|
prev.toDateString() === start.toDateString() ? prev : start
|
|
);
|
|
fetchEvents(start, end);
|
|
}, [fetchEvents]);
|
|
|
|
// Handle event click - open edit modal
|
|
const handleEventClick = useCallback((event: CalendarEvent) => {
|
|
setEditingEvent(event);
|
|
setIsModalOpen(true);
|
|
}, []);
|
|
|
|
// Handle date selection (for creating new appointment)
|
|
const handleDateSelect = useCallback((start: Date, end: Date) => {
|
|
setEditingEvent(undefined);
|
|
setModalInitialDate(start);
|
|
setModalInitialStartTime(format(start, 'HH:mm'));
|
|
setModalInitialEndTime(format(end, 'HH:mm'));
|
|
setIsModalOpen(true);
|
|
}, []);
|
|
|
|
// Handle event drag-and-drop - show confirmation dialog
|
|
const handleEventDrop = useCallback((
|
|
eventId: string,
|
|
newStart: Date,
|
|
newEnd: Date | null
|
|
) => {
|
|
const event = events.find((e) => e.id === eventId);
|
|
if (event) {
|
|
setPendingReschedule({ event, newStart, newEnd });
|
|
}
|
|
}, [events]);
|
|
|
|
// Confirm reschedule
|
|
const confirmReschedule = useCallback(async () => {
|
|
if (!pendingReschedule) return;
|
|
|
|
setIsRescheduling(true);
|
|
const { event, newStart, newEnd } = pendingReschedule;
|
|
|
|
const result = await rescheduleEncounter(
|
|
event.id,
|
|
newStart.toISOString(),
|
|
newEnd?.toISOString() || null
|
|
);
|
|
|
|
if (result.success) {
|
|
toast({ title: 'Afspraak verzet' });
|
|
// Update local state
|
|
setEvents((prev) =>
|
|
prev.map((e) =>
|
|
e.id === event.id
|
|
? { ...e, start: newStart, end: newEnd || undefined }
|
|
: e
|
|
)
|
|
);
|
|
} else {
|
|
toast({
|
|
variant: 'destructive',
|
|
title: 'Kon afspraak niet verzetten',
|
|
description: result.error,
|
|
});
|
|
// Refresh events to revert
|
|
const start = startOfWeek(currentDate, { weekStartsOn: 1 });
|
|
const end = endOfWeek(currentDate, { weekStartsOn: 1 });
|
|
fetchEvents(start, end);
|
|
}
|
|
|
|
setIsRescheduling(false);
|
|
setPendingReschedule(null);
|
|
}, [pendingReschedule, currentDate, fetchEvents]);
|
|
|
|
// Cancel reschedule - revert the visual change
|
|
const cancelReschedule = useCallback(() => {
|
|
// Refresh events to revert the visual drag
|
|
const start = startOfWeek(currentDate, { weekStartsOn: 1 });
|
|
const end = endOfWeek(currentDate, { weekStartsOn: 1 });
|
|
fetchEvents(start, end);
|
|
setPendingReschedule(null);
|
|
}, [currentDate, fetchEvents]);
|
|
|
|
// Handle new appointment button
|
|
const handleNewAppointment = useCallback(() => {
|
|
setEditingEvent(undefined);
|
|
setModalInitialDate(currentDate);
|
|
setModalInitialStartTime('09:00');
|
|
setModalInitialEndTime('10:00');
|
|
setIsModalOpen(true);
|
|
}, [currentDate]);
|
|
|
|
// Handle mini calendar date selection
|
|
const handleMiniCalendarDateSelect = useCallback((date: Date) => {
|
|
setCurrentDate(date);
|
|
|
|
// Calculate date range based on current view
|
|
const start = currentView === 'timeGridDay'
|
|
? date
|
|
: startOfWeek(date, { weekStartsOn: 1 });
|
|
const end = currentView === 'timeGridDay'
|
|
? addDays(date, 1)
|
|
: endOfWeek(date, { weekStartsOn: 1 });
|
|
|
|
fetchEvents(start, end);
|
|
}, [currentView, fetchEvents]);
|
|
|
|
// Handle modal success (refresh events)
|
|
const handleModalSuccess = useCallback(() => {
|
|
const start = currentView === 'timeGridDay'
|
|
? currentDate
|
|
: startOfWeek(currentDate, { weekStartsOn: 1 });
|
|
const end = currentView === 'timeGridDay'
|
|
? addDays(currentDate, 1)
|
|
: endOfWeek(currentDate, { weekStartsOn: 1 });
|
|
fetchEvents(start, end);
|
|
}, [currentDate, currentView, fetchEvents]);
|
|
|
|
return (
|
|
<div className="flex flex-col h-[calc(100vh-8rem)]">
|
|
<AgendaToolbar
|
|
currentDate={currentDate}
|
|
currentView={currentView}
|
|
onViewChange={handleViewChange}
|
|
onNavigate={handleNavigate}
|
|
onNewAppointment={handleNewAppointment}
|
|
/>
|
|
|
|
<div className="flex flex-1 gap-4 min-h-0">
|
|
{/* Mini calendar sidebar */}
|
|
<div className="w-64 flex-shrink-0">
|
|
<MiniCalendar
|
|
selectedDate={currentDate}
|
|
onDateSelect={handleMiniCalendarDateSelect}
|
|
/>
|
|
</div>
|
|
|
|
{/* Main calendar */}
|
|
<div className="flex-1 bg-white rounded-lg border border-slate-200 overflow-hidden relative">
|
|
{isPending && (
|
|
<div className="absolute inset-0 bg-white/50 flex items-center justify-center z-10">
|
|
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
|
</div>
|
|
)}
|
|
<AgendaCalendar
|
|
events={events}
|
|
initialView={currentView}
|
|
currentView={currentView}
|
|
currentDate={currentDate}
|
|
onEventClick={handleEventClick}
|
|
onDateSelect={handleDateSelect}
|
|
onEventDrop={handleEventDrop}
|
|
onDateChange={handleDateChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Appointment Modal */}
|
|
<AppointmentModal
|
|
open={isModalOpen}
|
|
onOpenChange={setIsModalOpen}
|
|
initialDate={modalInitialDate}
|
|
initialStartTime={modalInitialStartTime}
|
|
initialEndTime={modalInitialEndTime}
|
|
editingEvent={editingEvent}
|
|
onSuccess={handleModalSuccess}
|
|
/>
|
|
|
|
{/* Reschedule Confirmation Dialog */}
|
|
{pendingReschedule && (
|
|
<RescheduleDialog
|
|
open={!!pendingReschedule}
|
|
onOpenChange={(open) => {
|
|
if (!open) cancelReschedule();
|
|
}}
|
|
patientName={pendingReschedule.event.title}
|
|
appointmentType={pendingReschedule.event.extendedProps.encounter.type_display}
|
|
oldStart={new Date(pendingReschedule.event.start)}
|
|
oldEnd={pendingReschedule.event.end ? new Date(pendingReschedule.event.end) : null}
|
|
newStart={pendingReschedule.newStart}
|
|
newEnd={pendingReschedule.newEnd}
|
|
onConfirm={confirmReschedule}
|
|
isLoading={isRescheduling}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|