fix(cortex): update date handling in API and UI components

- 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.
This commit is contained in:
colinislit
2026-01-02 09:21:37 +01:00
parent 011815072b
commit 52e07aaf80
20 changed files with 2092 additions and 65 deletions

View File

@@ -19,6 +19,7 @@ import { PatientDashboardBlock } from '../blocks/patient-dashboard-block';
import { FallbackPicker } from '../blocks/fallback-picker';
import { APPOINTMENT_TYPES, type AppointmentTypeCode, type LocationClassCode } from '@/app/epd/agenda/types';
import type { Artifact, BlockType } from '@/stores/cortex-store';
import { parseRelativeDate, isDateRange } from '@/lib/cortex/date-time-parser';
interface ArtifactContainerProps {
artifacts: Artifact[];
@@ -55,13 +56,13 @@ function coerceDate(value: unknown): Date | undefined {
}
function coerceDateRange(raw: any): AgendaBlockProps['dateRange'] | undefined {
const start = coerceDate(raw?.start);
const end = coerceDate(raw?.end);
if (!start || !end) return undefined;
const label = typeof raw?.label === 'string' ? raw.label : undefined;
if (!label) return undefined;
return {
start,
end,
label: typeof raw?.label === 'string' ? raw.label : 'custom',
start: coerceDate(raw?.start),
end: coerceDate(raw?.end),
label,
};
}
@@ -78,6 +79,21 @@ function resolveLocation(value: unknown): LocationClassCode | undefined {
return undefined;
}
/**
* Convert date label to Date using central date-time-parser (DRY)
*/
function resolveDateFromLabel(label?: string): Date | null {
if (!label) return null;
const parsed = parseRelativeDate(label);
if (!parsed) return null;
// DateRange → return start date
if (isDateRange(parsed)) return parsed.start;
return parsed;
}
function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlockProps['prefillData'] {
if (!prefill || typeof prefill !== 'object') return undefined;
@@ -90,9 +106,15 @@ function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlo
}
: undefined);
// Try to resolve date from label first (more reliable than AI-generated dates)
const dateLabel = prefill?.datetime?.label || prefill?.dateRange?.label;
const resolvedDate = resolveDateFromLabel(dateLabel);
const datetimeDate =
resolvedDate || // Prefer calculated date from label
coerceDate(prefill?.datetime?.date) ||
(prefill?.datetime?.time ? new Date() : undefined);
const datetime = datetimeDate
? {
date: datetimeDate,
@@ -103,7 +125,12 @@ function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlo
const appointmentType = resolveAppointmentType(prefill?.appointmentType || prefill?.type);
const location = resolveLocation(prefill?.location);
// Also resolve newDatetime from label
const newDateLabel = prefill?.newDatetime?.label;
const resolvedNewDate = resolveDateFromLabel(newDateLabel);
const newDatetimeDate =
resolvedNewDate || // Prefer calculated date from label
coerceDate(prefill?.newDatetime?.date) ||
(prefill?.newDatetime?.time ? new Date() : undefined);
const newDatetime = newDatetimeDate

View File

@@ -1,16 +1,18 @@
'use client';
import React from 'react';
import React, { useState, useEffect } from 'react';
import { AppointmentTypeCode, LocationClassCode, CalendarEvent } from '@/app/epd/agenda/types';
import { AgendaListView } from './agenda-list-view';
import { AgendaCreateForm } from './agenda-create-form';
import { AgendaCancelView } from './agenda-cancel-view';
import { AgendaRescheduleForm } from './agenda-reschedule-form';
import { AgendaErrorState } from './agenda-error-state';
import { motion, AnimatePresence } from 'framer-motion';
import { Loader2 } from 'lucide-react';
export interface AgendaBlockProps {
mode: 'list' | 'create' | 'cancel' | 'reschedule';
appointments?: CalendarEvent[];
dateRange?: { start: Date; end: Date; label: string };
dateRange?: { start?: Date; end?: Date; label: string };
prefillData?: {
patient?: { id: string; name: string };
datetime?: { date: Date; time: string };
@@ -26,15 +28,92 @@ export interface AgendaBlockProps {
export function AgendaBlock({
mode,
appointments,
dateRange,
appointments: initialAppointments,
dateRange: initialDateRange,
prefillData,
disambiguationOptions,
onClose,
}: AgendaBlockProps) {
// State for fetched appointments (only used in list mode)
const [appointments, setAppointments] = useState<CalendarEvent[] | undefined>(initialAppointments);
const [dateRange, setDateRange] = useState<{ start: Date; end: Date; label: string } | undefined>(initialDateRange);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [refetchKey, setRefetchKey] = useState(0);
// Fetch appointments when in list mode and no appointments are provided
// Server-side bepaalt de datum (consistent met EPD agenda)
useEffect(() => {
if (mode !== 'list') return;
if (initialAppointments && initialAppointments.length > 0) return;
const fetchAppointments = async () => {
setIsLoading(true);
setError(null);
try {
// Build query params - server bepaalt de datum
const params = new URLSearchParams();
// Stuur alleen label naar server, server berekent de datums
if (initialDateRange?.label) {
params.set('label', initialDateRange.label);
}
// Als geen label en geen expliciete datums, server defaults naar vandaag
const response = await fetch(`/api/cortex/agenda?${params}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Kon afspraken niet laden');
}
const data = await response.json();
setAppointments(data.appointments || []);
// Update dateRange met server-side berekende waarden
if (data.dateRange) {
setDateRange({
start: new Date(data.dateRange.start),
end: new Date(data.dateRange.end),
label: data.dateRange.label,
});
}
} catch (err) {
console.error('Error fetching appointments:', err);
setError(err instanceof Error ? err.message : 'Er ging iets mis');
} finally {
setIsLoading(false);
}
};
fetchAppointments();
}, [mode, initialAppointments, initialDateRange, refetchKey]);
const renderContent = () => {
switch (mode) {
case 'list':
if (isLoading) {
return (
<div key="loading" className="flex flex-col items-center justify-center h-full text-slate-500">
<Loader2 className="h-8 w-8 animate-spin text-teal-600 mb-4" />
<p className="text-sm">Afspraken laden...</p>
</div>
);
}
if (error) {
return (
<AgendaErrorState
key="error"
error={error}
context="query"
onRetry={() => {
setError(null);
setRefetchKey((k) => k + 1); // Trigger refetch
}}
/>
);
}
return (
<AgendaListView
key="list"

View File

@@ -17,7 +17,7 @@ import {
interface AgendaListViewProps {
appointments?: CalendarEvent[];
dateRange?: { start: Date; end: Date; label: string };
dateRange?: { start?: Date; end?: Date; label: string };
onClose?: () => void;
onCancelAppointment?: (encounter: CalendarEvent) => void;
onViewDetails?: (encounter: CalendarEvent) => void;
@@ -48,7 +48,7 @@ export function AgendaListView({
const formatDateLabel = () => {
if (dateRange?.label) {
if (dateRange.label === 'vandaag' || dateRange.label === 'morgen') {
if ((dateRange.label === 'vandaag' || dateRange.label === 'morgen') && dateRange.start) {
const dateStr = format(dateRange.start, 'd MMMM', { locale: nl });
return `Afspraken ${dateRange.label} - ${dateStr}`;
}