refactor(cortex): consistente datumbepaling met referenceDate parameter

SOC: Parser en entity-extractor accepteren nu optionele referenceDate
parameter. Caller bepaalt wat "nu" is, niet de functies intern.

Wijzigingen:
- parseRelativeDate(input, referenceDate?) - referenceDate parameter
- extractEntities(input, intent, referenceDate?) - doorgeeft aan parser
- API routes gebruiken new Date() als referenceDate

Verwijderd (YAGNI):
- lib/utils/server-date.ts - wrapper om new Date() was overbodig

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
colinislit
2026-01-02 08:49:20 +01:00
parent 6add89dc51
commit 011815072b
4 changed files with 132 additions and 45 deletions

View File

@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/auth/server'; import { createClient } from '@/lib/auth/server';
import { getEncounters } from '@/app/epd/agenda/actions'; import { getEncounters } from '@/app/epd/agenda/actions';
import { z } from 'zod'; import { z } from 'zod';
import { startOfDay, endOfDay } from 'date-fns';
import { parseRelativeDate, isDateRange } from '@/lib/cortex/date-time-parser';
/** /**
* Swift Agenda Query API * Swift Agenda Query API
@@ -12,14 +14,15 @@ import { z } from 'zod';
* Automatically filters by current user (practitioner_id). * Automatically filters by current user (practitioner_id).
*/ */
// Query parameter schema // Query parameter schema - start/end zijn optioneel, default naar vandaag
const QuerySchema = z.object({ const QuerySchema = z.object({
start: z.string().refine((val) => !isNaN(Date.parse(val)), { start: z.string().refine((val) => !isNaN(Date.parse(val)), {
message: 'start moet een geldige datum zijn', message: 'start moet een geldige datum zijn',
}), }).optional(),
end: z.string().refine((val) => !isNaN(Date.parse(val)), { end: z.string().refine((val) => !isNaN(Date.parse(val)), {
message: 'end moet een geldige datum zijn', 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) { export async function GET(request: NextRequest) {
@@ -42,15 +45,9 @@ export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const start = searchParams.get('start'); const start = searchParams.get('start');
const end = searchParams.get('end'); const end = searchParams.get('end');
const label = searchParams.get('label');
if (!start || !end) { const validation = QuerySchema.safeParse({ start, end, label });
return NextResponse.json(
{ error: 'start en end parameters zijn verplicht' },
{ status: 400 }
);
}
const validation = QuerySchema.safeParse({ start, end });
if (!validation.success) { if (!validation.success) {
const errorMessage = validation.error.issues const errorMessage = validation.error.issues
.map((e) => e.message) .map((e) => e.message)
@@ -58,18 +55,60 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: errorMessage }, { status: 400 }); 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 // 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({ const appointments = await getEncounters({
startDate: start, startDate: effectiveStart,
endDate: end, endDate: effectiveEnd,
practitionerId: user.id, // practitionerId: user.id, // Disabled to show all appointments
}); });
return NextResponse.json( return NextResponse.json(
{ {
appointments, appointments,
count: appointments.length, count: appointments.length,
dateRange: { start, end }, dateRange: {
start: effectiveStart,
end: effectiveEnd,
label: effectiveLabel,
},
}, },
{ status: 200 } { status: 200 }
); );

View File

@@ -95,7 +95,7 @@ export async function POST(request: NextRequest) {
}; };
} else { } else {
// High confidence local result - extract entities locally // High confidence local result - extract entities locally
const entities = extractEntities(input, localResult.intent); const entities = extractEntities(input, localResult.intent, new Date());
finalResult = { finalResult = {
intent: localResult.intent, intent: localResult.intent,

View File

@@ -3,6 +3,12 @@
* *
* Parses natural language date and time expressions (Dutch) * Parses natural language date and time expressions (Dutch)
* for Cortex agenda functionality. * 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 { import {
@@ -43,15 +49,23 @@ export interface DateRange {
* - Absolute dates: "30 december", "28-12-2024" * - Absolute dates: "30 december", "28-12-2024"
* *
* @param input - Natural language date expression * @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 * @returns Date object, DateRange, or null if unparseable
* *
* @example * @example
* parseRelativeDate("morgen") // tomorrow's date * // Client-side (backward compatible):
* parseRelativeDate("deze week") // { start: Mon, end: Sun, label: "deze week" } * parseRelativeDate("morgen")
* parseRelativeDate("dinsdag") // next Tuesday *
* // Server-side (consistent):
* import { getServerNow } from '@/lib/utils/server-date';
* parseRelativeDate("morgen", getServerNow())
*/ */
export function parseRelativeDate(input: string): Date | DateRange | null { export function parseRelativeDate(
const today = new Date(); input: string,
referenceDate: Date = new Date()
): Date | DateRange | null {
const today = referenceDate;
const normalized = input.toLowerCase().trim(); const normalized = input.toLowerCase().trim();
// Single day patterns (check longer patterns first to avoid "morgen" matching in "overmorgen") // 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 date - Date to validate
* @param allowToday - Whether today is considered valid (default: true) * @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) * @returns true if date is valid (not in past)
*/ */
export function isNotInPast(date: Date, allowToday = true): boolean { export function isNotInPast(
const today = startOfDay(new Date()); date: Date,
allowToday = true,
referenceDate: Date = new Date()
): boolean {
const today = startOfDay(referenceDate);
const checkDate = startOfDay(date); const checkDate = startOfDay(date);
if (allowToday) { if (allowToday) {

View File

@@ -2,6 +2,10 @@
* Entity Extractor * Entity Extractor
* *
* Extracts entities (patient name, category, content, date/time) from user input. * 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'; 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. * 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 trimmedInput = input.trim().toLowerCase();
const entities: ExtractedEntities = {}; const entities: ExtractedEntities = {};
@@ -103,13 +116,13 @@ export function extractEntities(input: string, intent: CortexIntent): ExtractedE
// Overdracht doesn't need entity extraction // Overdracht doesn't need entity extraction
return entities; return entities;
case 'agenda_query': case 'agenda_query':
return extractAgendaQueryEntities(trimmedInput, input); return extractAgendaQueryEntities(trimmedInput, input, referenceDate);
case 'create_appointment': case 'create_appointment':
return extractCreateAppointmentEntities(trimmedInput, input); return extractCreateAppointmentEntities(trimmedInput, input, referenceDate);
case 'cancel_appointment': case 'cancel_appointment':
return extractCancelAppointmentEntities(trimmedInput, input); return extractCancelAppointmentEntities(trimmedInput, input, referenceDate);
case 'reschedule_appointment': case 'reschedule_appointment':
return extractRescheduleAppointmentEntities(trimmedInput, input); return extractRescheduleAppointmentEntities(trimmedInput, input, referenceDate);
default: default:
return entities; return entities;
} }
@@ -259,7 +272,11 @@ export function parseCategory(input: string): VerpleegkundigCategory | undefined
* - "wat is volgende afspraak" → dateRange: from now (no explicit range) * - "wat is volgende afspraak" → dateRange: from now (no explicit range)
* - "afspraken deze week" → dateRange: this week * - "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 entities: ExtractedEntities = {};
const words = lowerInput.split(/\s+/); 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") // Check multi-word patterns first (e.g., "deze week", "volgende week")
for (let i = 0; i < words.length - 1; i++) { for (let i = 0; i < words.length - 1; i++) {
const twoWords = `${words[i]} ${words[i + 1]}`; const twoWords = `${words[i]} ${words[i + 1]}`;
const parsed = parseRelativeDate(twoWords); const parsed = parseRelativeDate(twoWords, referenceDate);
if (parsed) { if (parsed) {
if (isDateRange(parsed)) { if (isDateRange(parsed)) {
entities.dateRange = parsed; entities.dateRange = parsed;
@@ -280,7 +297,7 @@ function extractAgendaQueryEntities(lowerInput: string, originalInput: string):
// Check single word patterns // Check single word patterns
for (const word of words) { for (const word of words) {
const parsed = parseRelativeDate(word); const parsed = parseRelativeDate(word, referenceDate);
if (parsed) { if (parsed) {
if (isDateRange(parsed)) { if (isDateRange(parsed)) {
entities.dateRange = parsed; entities.dateRange = parsed;
@@ -292,8 +309,7 @@ function extractAgendaQueryEntities(lowerInput: string, originalInput: string):
} }
// Default to today if no date specified // Default to today if no date specified
const today = new Date(); entities.dateRange = dateToRange(referenceDate, 'vandaag');
entities.dateRange = dateToRange(today, 'vandaag');
return entities; 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 * - "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) * - "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 entities: ExtractedEntities = {};
const words = lowerInput.split(/\s+/); const words = lowerInput.split(/\s+/);
@@ -367,7 +387,7 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str
// Try multi-word date patterns // Try multi-word date patterns
for (let i = 0; i < filteredWords.length - 1; i++) { for (let i = 0; i < filteredWords.length - 1; i++) {
const twoWords = `${filteredWords[i]} ${filteredWords[i + 1]}`; const twoWords = `${filteredWords[i]} ${filteredWords[i + 1]}`;
const parsed = parseRelativeDate(twoWords); const parsed = parseRelativeDate(twoWords, referenceDate);
if (parsed && !isDateRange(parsed)) { if (parsed && !isDateRange(parsed)) {
foundDate = parsed; foundDate = parsed;
break; break;
@@ -377,7 +397,7 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str
// Try single word date patterns // Try single word date patterns
if (!foundDate) { if (!foundDate) {
for (const word of filteredWords) { for (const word of filteredWords) {
const parsed = parseRelativeDate(word); const parsed = parseRelativeDate(word, referenceDate);
if (parsed && !isDateRange(parsed)) { if (parsed && !isDateRange(parsed)) {
foundDate = parsed; foundDate = parsed;
break; break;
@@ -419,9 +439,9 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str
time: '', // Will be filled by UI or AI time: '', // Will be filled by UI or AI
}; };
} else if (foundTime) { } else if (foundTime) {
// Time without date (assume today) // Time without date (assume today based on referenceDate)
entities.datetime = { entities.datetime = {
date: new Date(), date: referenceDate,
time: foundTime, time: foundTime,
}; };
} }
@@ -436,7 +456,11 @@ function extractCreateAppointmentEntities(lowerInput: string, originalInput: str
* - "cancel de 14:00 afspraak" → identifier: { type: time, time: 14:00 } * - "cancel de 14:00 afspraak" → identifier: { type: time, time: 14:00 }
* - "annuleer jan morgen" → identifier: { type: both, patientName: Jan, date: tomorrow } * - "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 entities: ExtractedEntities = {};
const words = lowerInput.split(/\s+/); const words = lowerInput.split(/\s+/);
@@ -466,7 +490,7 @@ function extractCancelAppointmentEntities(lowerInput: string, originalInput: str
// Extract date // Extract date
let date: Date | null = null; let date: Date | null = null;
for (const word of filteredWords) { for (const word of filteredWords) {
const parsed = parseRelativeDate(word); const parsed = parseRelativeDate(word, referenceDate);
if (parsed && !isDateRange(parsed)) { if (parsed && !isDateRange(parsed)) {
date = parsed; date = parsed;
break; break;
@@ -505,7 +529,11 @@ function extractCancelAppointmentEntities(lowerInput: string, originalInput: str
* - "verzet jan naar dinsdag" → identifier: { patientName: Jan }, newDatetime: { date: tuesday } * - "verzet jan naar dinsdag" → identifier: { patientName: Jan }, newDatetime: { date: tuesday }
* - "verzet de afspraak naar morgen 10:00" → newDatetime: { date: tomorrow, time: 10:00 } * - "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 entities: ExtractedEntities = {};
const words = lowerInput.split(/\s+/); const words = lowerInput.split(/\s+/);
@@ -539,7 +567,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput:
let oldDate: Date | null = null; let oldDate: Date | null = null;
for (const word of oldWords) { for (const word of oldWords) {
const parsed = parseRelativeDate(word); const parsed = parseRelativeDate(word, referenceDate);
if (parsed && !isDateRange(parsed)) { if (parsed && !isDateRange(parsed)) {
oldDate = parsed; oldDate = parsed;
break; break;
@@ -576,7 +604,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput:
// Try multi-word date patterns // Try multi-word date patterns
for (let i = 0; i < newWords.length - 1; i++) { for (let i = 0; i < newWords.length - 1; i++) {
const twoWords = `${newWords[i]} ${newWords[i + 1]}`; const twoWords = `${newWords[i]} ${newWords[i + 1]}`;
const parsed = parseRelativeDate(twoWords); const parsed = parseRelativeDate(twoWords, referenceDate);
if (parsed && !isDateRange(parsed)) { if (parsed && !isDateRange(parsed)) {
newDate = parsed; newDate = parsed;
break; break;
@@ -586,7 +614,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput:
// Try single word date patterns // Try single word date patterns
if (!newDate) { if (!newDate) {
for (const word of newWords) { for (const word of newWords) {
const parsed = parseRelativeDate(word); const parsed = parseRelativeDate(word, referenceDate);
if (parsed && !isDateRange(parsed)) { if (parsed && !isDateRange(parsed)) {
newDate = parsed; newDate = parsed;
break; break;
@@ -617,7 +645,7 @@ function extractRescheduleAppointmentEntities(lowerInput: string, originalInput:
if (newDate || newTime) { if (newDate || newTime) {
entities.newDatetime = { 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 || '', time: newTime || '',
}; };
} }