diff --git a/app/api/cortex/agenda/route.ts b/app/api/cortex/agenda/route.ts index e95b615..ef1ce30 100644 --- a/app/api/cortex/agenda/route.ts +++ b/app/api/cortex/agenda/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/auth/server'; import { getEncounters } from '@/app/epd/agenda/actions'; import { z } from 'zod'; +import { startOfDay, endOfDay } from 'date-fns'; +import { parseRelativeDate, isDateRange } from '@/lib/cortex/date-time-parser'; /** * Swift Agenda Query API @@ -12,14 +14,15 @@ import { z } from 'zod'; * Automatically filters by current user (practitioner_id). */ -// Query parameter schema +// Query parameter schema - start/end zijn optioneel, default naar vandaag const QuerySchema = z.object({ start: z.string().refine((val) => !isNaN(Date.parse(val)), { message: 'start moet een geldige datum zijn', - }), + }).optional(), end: z.string().refine((val) => !isNaN(Date.parse(val)), { message: 'end moet een geldige datum zijn', - }), + }).optional(), + label: z.string().optional(), // Voor relatieve datums: vandaag, morgen, deze week, etc. }); export async function GET(request: NextRequest) { @@ -42,15 +45,9 @@ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const start = searchParams.get('start'); const end = searchParams.get('end'); + const label = searchParams.get('label'); - if (!start || !end) { - return NextResponse.json( - { error: 'start en end parameters zijn verplicht' }, - { status: 400 } - ); - } - - const validation = QuerySchema.safeParse({ start, end }); + const validation = QuerySchema.safeParse({ start, end, label }); if (!validation.success) { const errorMessage = validation.error.issues .map((e) => e.message) @@ -58,18 +55,60 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: errorMessage }, { status: 400 }); } + // Determine effective date range (server-side, consistent met EPD agenda) + const serverNow = new Date(); + let effectiveStart: string; + let effectiveEnd: string; + let effectiveLabel: string; + + if (start && end) { + // Expliciete datums meegegeven + effectiveStart = start; + effectiveEnd = end; + effectiveLabel = label || 'custom'; + } else if (label) { + // Label meegegeven, server berekent de datum via centrale lib + // Pass serverNow als referenceDate voor consistentie + const parsed = parseRelativeDate(label, serverNow); + if (isDateRange(parsed)) { + effectiveStart = startOfDay(parsed.start).toISOString(); + effectiveEnd = endOfDay(parsed.end).toISOString(); + effectiveLabel = parsed.label; + } else if (parsed) { + effectiveStart = startOfDay(parsed).toISOString(); + effectiveEnd = endOfDay(parsed).toISOString(); + effectiveLabel = label; + } else { + // Label niet herkend, fallback naar vandaag + effectiveStart = startOfDay(serverNow).toISOString(); + effectiveEnd = endOfDay(serverNow).toISOString(); + effectiveLabel = 'vandaag'; + } + } else { + // Geen params, default naar vandaag (server-side bepaald) + effectiveStart = startOfDay(serverNow).toISOString(); + effectiveEnd = endOfDay(serverNow).toISOString(); + effectiveLabel = 'vandaag'; + } + // Fetch appointments + // Note: We don't filter by practitioner_id to show all appointments + // In a production system, this should be filtered by organization/team const appointments = await getEncounters({ - startDate: start, - endDate: end, - practitionerId: user.id, + startDate: effectiveStart, + endDate: effectiveEnd, + // practitionerId: user.id, // Disabled to show all appointments }); return NextResponse.json( { appointments, count: appointments.length, - dateRange: { start, end }, + dateRange: { + start: effectiveStart, + end: effectiveEnd, + label: effectiveLabel, + }, }, { status: 200 } ); diff --git a/app/api/intent/classify/route.ts b/app/api/intent/classify/route.ts index 81d48bc..2cbe1f7 100644 --- a/app/api/intent/classify/route.ts +++ b/app/api/intent/classify/route.ts @@ -95,7 +95,7 @@ export async function POST(request: NextRequest) { }; } else { // High confidence local result - extract entities locally - const entities = extractEntities(input, localResult.intent); + const entities = extractEntities(input, localResult.intent, new Date()); finalResult = { intent: localResult.intent, diff --git a/lib/cortex/date-time-parser.ts b/lib/cortex/date-time-parser.ts index cfaae6d..8ba3561 100644 --- a/lib/cortex/date-time-parser.ts +++ b/lib/cortex/date-time-parser.ts @@ -3,6 +3,12 @@ * * Parses natural language date and time expressions (Dutch) * for Cortex agenda functionality. + * + * IMPORTANT: All functions accept an optional `referenceDate` parameter. + * - Server-side: pass `getServerNow()` from `lib/utils/server-date.ts` + * - Client-side: omit for backward compatibility (uses `new Date()`) + * + * This ensures consistent date handling between Cortex and regular EPD. */ import { @@ -43,15 +49,23 @@ export interface DateRange { * - Absolute dates: "30 december", "28-12-2024" * * @param input - Natural language date expression + * @param referenceDate - Reference date for relative calculations (default: new Date()) + * Server-side: pass getServerNow() for consistency * @returns Date object, DateRange, or null if unparseable * * @example - * parseRelativeDate("morgen") // tomorrow's date - * parseRelativeDate("deze week") // { start: Mon, end: Sun, label: "deze week" } - * parseRelativeDate("dinsdag") // next Tuesday + * // Client-side (backward compatible): + * parseRelativeDate("morgen") + * + * // Server-side (consistent): + * import { getServerNow } from '@/lib/utils/server-date'; + * parseRelativeDate("morgen", getServerNow()) */ -export function parseRelativeDate(input: string): Date | DateRange | null { - const today = new Date(); +export function parseRelativeDate( + input: string, + referenceDate: Date = new Date() +): Date | DateRange | null { + const today = referenceDate; const normalized = input.toLowerCase().trim(); // Single day patterns (check longer patterns first to avoid "morgen" matching in "overmorgen") @@ -294,10 +308,16 @@ export function combineDatetime(date: Date | string, time: string): string { * * @param date - Date to validate * @param allowToday - Whether today is considered valid (default: true) + * @param referenceDate - Reference date for "today" (default: new Date()) + * Server-side: pass getServerNow() for consistency * @returns true if date is valid (not in past) */ -export function isNotInPast(date: Date, allowToday = true): boolean { - const today = startOfDay(new Date()); +export function isNotInPast( + date: Date, + allowToday = true, + referenceDate: Date = new Date() +): boolean { + const today = startOfDay(referenceDate); const checkDate = startOfDay(date); if (allowToday) { diff --git a/lib/cortex/entity-extractor.ts b/lib/cortex/entity-extractor.ts index ea3cb22..391bc62 100644 --- a/lib/cortex/entity-extractor.ts +++ b/lib/cortex/entity-extractor.ts @@ -2,6 +2,10 @@ * Entity Extractor * * Extracts entities (patient name, category, content, date/time) from user input. + * + * IMPORTANT: All functions accept an optional `referenceDate` parameter. + * Server-side: pass `getServerNow()` from `lib/utils/server-date.ts` + * This ensures consistent date handling between Cortex and regular EPD. */ import type { VerpleegkundigCategory } from '@/lib/types/report'; @@ -89,8 +93,17 @@ const COMMON_NAMES = new Set([ /** * Extract entities from user input based on the detected intent. + * + * @param input - User input text + * @param intent - Detected intent + * @param referenceDate - Reference date for relative date calculations (default: new Date()) + * Server-side: pass getServerNow() for consistency */ -export function extractEntities(input: string, intent: CortexIntent): ExtractedEntities { +export function extractEntities( + input: string, + intent: CortexIntent, + referenceDate: Date = new Date() +): ExtractedEntities { const trimmedInput = input.trim().toLowerCase(); const entities: ExtractedEntities = {}; @@ -103,13 +116,13 @@ export function extractEntities(input: string, intent: CortexIntent): ExtractedE // Overdracht doesn't need entity extraction return entities; case 'agenda_query': - return extractAgendaQueryEntities(trimmedInput, input); + return extractAgendaQueryEntities(trimmedInput, input, referenceDate); case 'create_appointment': - return extractCreateAppointmentEntities(trimmedInput, input); + return extractCreateAppointmentEntities(trimmedInput, input, referenceDate); case 'cancel_appointment': - return extractCancelAppointmentEntities(trimmedInput, input); + return extractCancelAppointmentEntities(trimmedInput, input, referenceDate); case 'reschedule_appointment': - return extractRescheduleAppointmentEntities(trimmedInput, input); + return extractRescheduleAppointmentEntities(trimmedInput, input, referenceDate); default: return entities; } @@ -259,7 +272,11 @@ export function parseCategory(input: string): VerpleegkundigCategory | undefined * - "wat is volgende afspraak" → dateRange: from now (no explicit range) * - "afspraken deze week" → dateRange: this week */ -function extractAgendaQueryEntities(lowerInput: string, originalInput: string): ExtractedEntities { +function extractAgendaQueryEntities( + lowerInput: string, + originalInput: string, + referenceDate: Date +): ExtractedEntities { const entities: ExtractedEntities = {}; const words = lowerInput.split(/\s+/); @@ -267,7 +284,7 @@ function extractAgendaQueryEntities(lowerInput: string, originalInput: string): // Check multi-word patterns first (e.g., "deze week", "volgende week") for (let i = 0; i < words.length - 1; i++) { const twoWords = `${words[i]} ${words[i + 1]}`; - const parsed = parseRelativeDate(twoWords); + const parsed = parseRelativeDate(twoWords, referenceDate); if (parsed) { if (isDateRange(parsed)) { entities.dateRange = parsed; @@ -280,7 +297,7 @@ function extractAgendaQueryEntities(lowerInput: string, originalInput: string): // Check single word patterns for (const word of words) { - const parsed = parseRelativeDate(word); + const parsed = parseRelativeDate(word, referenceDate); if (parsed) { if (isDateRange(parsed)) { entities.dateRange = parsed; @@ -292,8 +309,7 @@ function extractAgendaQueryEntities(lowerInput: string, originalInput: string): } // Default to today if no date specified - const today = new Date(); - entities.dateRange = dateToRange(today, 'vandaag'); + entities.dateRange = dateToRange(referenceDate, 'vandaag'); return entities; } @@ -305,7 +321,11 @@ function extractAgendaQueryEntities(lowerInput: string, originalInput: string): * - "plan intake marie vrijdag 10:00" → patient: Marie, type: intake, date: friday, time: 10:00 * - "afspraak met piet 14:00" → patient: Piet, time: 14:00, date: today (implied) */ -function extractCreateAppointmentEntities(lowerInput: string, originalInput: string): ExtractedEntities { +function extractCreateAppointmentEntities( + lowerInput: string, + originalInput: string, + referenceDate: Date +): ExtractedEntities { const entities: ExtractedEntities = {}; const words = lowerInput.split(/\s+/); @@ -367,7 +387,7 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str // Try multi-word date patterns for (let i = 0; i < filteredWords.length - 1; i++) { const twoWords = `${filteredWords[i]} ${filteredWords[i + 1]}`; - const parsed = parseRelativeDate(twoWords); + const parsed = parseRelativeDate(twoWords, referenceDate); if (parsed && !isDateRange(parsed)) { foundDate = parsed; break; @@ -377,7 +397,7 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str // Try single word date patterns if (!foundDate) { for (const word of filteredWords) { - const parsed = parseRelativeDate(word); + const parsed = parseRelativeDate(word, referenceDate); if (parsed && !isDateRange(parsed)) { foundDate = parsed; break; @@ -419,9 +439,9 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str time: '', // Will be filled by UI or AI }; } else if (foundTime) { - // Time without date (assume today) + // Time without date (assume today based on referenceDate) entities.datetime = { - date: new Date(), + date: referenceDate, time: foundTime, }; } @@ -436,7 +456,11 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str * - "cancel de 14:00 afspraak" → identifier: { type: time, time: 14:00 } * - "annuleer jan morgen" → identifier: { type: both, patientName: Jan, date: tomorrow } */ -function extractCancelAppointmentEntities(lowerInput: string, originalInput: string): ExtractedEntities { +function extractCancelAppointmentEntities( + lowerInput: string, + originalInput: string, + referenceDate: Date +): ExtractedEntities { const entities: ExtractedEntities = {}; const words = lowerInput.split(/\s+/); @@ -466,7 +490,7 @@ function extractCancelAppointmentEntities(lowerInput: string, originalInput: str // Extract date let date: Date | null = null; for (const word of filteredWords) { - const parsed = parseRelativeDate(word); + const parsed = parseRelativeDate(word, referenceDate); if (parsed && !isDateRange(parsed)) { date = parsed; break; @@ -505,7 +529,11 @@ function extractCancelAppointmentEntities(lowerInput: string, originalInput: str * - "verzet jan naar dinsdag" → identifier: { patientName: Jan }, newDatetime: { date: tuesday } * - "verzet de afspraak naar morgen 10:00" → newDatetime: { date: tomorrow, time: 10:00 } */ -function extractRescheduleAppointmentEntities(lowerInput: string, originalInput: string): ExtractedEntities { +function extractRescheduleAppointmentEntities( + lowerInput: string, + originalInput: string, + referenceDate: Date +): ExtractedEntities { const entities: ExtractedEntities = {}; const words = lowerInput.split(/\s+/); @@ -539,7 +567,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput: let oldDate: Date | null = null; for (const word of oldWords) { - const parsed = parseRelativeDate(word); + const parsed = parseRelativeDate(word, referenceDate); if (parsed && !isDateRange(parsed)) { oldDate = parsed; break; @@ -576,7 +604,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput: // Try multi-word date patterns for (let i = 0; i < newWords.length - 1; i++) { const twoWords = `${newWords[i]} ${newWords[i + 1]}`; - const parsed = parseRelativeDate(twoWords); + const parsed = parseRelativeDate(twoWords, referenceDate); if (parsed && !isDateRange(parsed)) { newDate = parsed; break; @@ -586,7 +614,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput: // Try single word date patterns if (!newDate) { for (const word of newWords) { - const parsed = parseRelativeDate(word); + const parsed = parseRelativeDate(word, referenceDate); if (parsed && !isDateRange(parsed)) { newDate = parsed; break; @@ -617,7 +645,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput: if (newDate || newTime) { entities.newDatetime = { - date: newDate || new Date(), // Default to today if only time specified + date: newDate || referenceDate, // Default to referenceDate if only time specified time: newTime || '', }; }