Files
triqura-ecd/app/api/cortex/intake/status/route.ts
colinislit 601ad8c3f0 fix(cortex): integrate intake intents into full chat + artifact flow
Fixes 4 bugs preventing intake blocks from working:

1. Chat API validation: Add 'nudge' to message type enum, allow empty
   content for streaming messages
2. AI chat recognition: Add P3 intake intents (intake_status,
   risico_query, diagnose_query, intake_navigeer) to system prompt
   with triggers, entities, and JSON examples
3. Artifact rendering: Add intake block imports and switch cases
   to artifact-container.tsx
4. API 400 error: Convert null to undefined for optional Zod params
   (searchParams.get returns null, Zod .optional() expects undefined)

Also adds:
- Testplan for E2E testing (60+ test cases)
- Session log with lessons learned and new intent checklist

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 00:07:10 +01:00

231 lines
7.2 KiB
TypeScript

/**
* Intake Status API
*
* GET /api/cortex/intake/status?patientId=xxx&intakeId=xxx
*
* Returns intake completion status: percentage, completed/missing sections.
* Used by IntakeStatusBlock in Cortex Command Center.
*/
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/auth/server';
import { z } from 'zod';
// Query parameter schema
const QuerySchema = z.object({
patientId: z.string().uuid({ message: 'patientId moet een geldige UUID zijn' }),
intakeId: z.string().uuid({ message: 'intakeId moet een geldige UUID zijn' }).optional(),
});
// Section definition
interface IntakeSection {
id: string;
label: string;
required: boolean;
}
const INTAKE_SECTIONS: IntakeSection[] = [
{ id: 'contacts', label: 'Contactmomenten', required: false },
{ id: 'anamnese', label: 'Anamnese', required: true },
{ id: 'risk', label: 'Risicotaxatie', required: true },
{ id: 'kindcheck', label: 'Kindcheck', required: true },
{ id: 'examination', label: 'Onderzoek', required: false },
{ id: 'rom', label: 'Meetinstrumenten (ROM)', required: false },
{ id: 'diagnosis', label: 'Diagnose', required: true },
{ id: 'behandeladvies', label: 'Behandeladvies', required: true },
];
export interface IntakeStatusResponse {
intakeId: string;
patientId: string;
completionPercentage: number;
status: 'bezig' | 'afgerond';
sections: {
id: string;
label: string;
required: boolean;
completed: boolean;
count: number;
}[];
completedCount: number;
totalRequired: number;
lastUpdated: string | null;
}
export async function GET(request: NextRequest) {
try {
// Auth check
const supabase = await createClient();
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ error: 'Niet geautoriseerd. Log opnieuw in.' },
{ status: 401 }
);
}
// Parse and validate query parameters
const searchParams = request.nextUrl.searchParams;
const patientId = searchParams.get('patientId');
const intakeId = searchParams.get('intakeId') || undefined; // Convert null to undefined for Zod
console.log('[Intake Status API] Request params:', { patientId, intakeId });
if (!patientId) {
console.log('[Intake Status API] Missing patientId');
return NextResponse.json(
{ error: 'Query parameter "patientId" is verplicht' },
{ status: 400 }
);
}
const validation = QuerySchema.safeParse({ patientId, intakeId });
if (!validation.success) {
const errorMessage = validation.error.issues
.map((e) => e.message)
.join(', ');
console.log('[Intake Status API] Validation failed:', errorMessage);
return NextResponse.json({ error: errorMessage }, { status: 400 });
}
// If no intakeId provided, get the most recent active intake for this patient
let targetIntakeId = validation.data.intakeId;
if (!targetIntakeId) {
const { data: latestIntake, error: intakeError } = await supabase
.from('intakes')
.select('id')
.eq('patient_id', patientId)
.eq('status', 'bezig')
.order('start_date', { ascending: false })
.limit(1)
.maybeSingle();
if (intakeError) {
console.error('Error fetching latest intake:', intakeError);
return NextResponse.json(
{ error: 'Fout bij ophalen intake' },
{ status: 500 }
);
}
if (!latestIntake) {
console.log('[Intake Status API] No active intake found for patient:', patientId);
return NextResponse.json(
{ error: 'Geen actieve intake gevonden voor deze patiënt. Start eerst een intake.' },
{ status: 404 }
);
}
targetIntakeId = latestIntake.id;
}
// Fetch intake details
const { data: intake, error: intakeDetailError } = await supabase
.from('intakes')
.select('id, patient_id, status, kindcheck_data, treatment_advice, updated_at')
.eq('id', targetIntakeId)
.single();
if (intakeDetailError || !intake) {
return NextResponse.json(
{ error: 'Intake niet gevonden' },
{ status: 404 }
);
}
// Fetch counts for each section in parallel
const [
contactsResult,
anamneseResult,
riskResult,
examinationResult,
romResult,
diagnosisResult,
] = await Promise.all([
// Contacts (encounters)
supabase
.from('encounters')
.select('id', { count: 'exact', head: true })
.eq('intake_id', targetIntakeId),
// Anamnese
supabase
.from('anamneses')
.select('id', { count: 'exact', head: true })
.eq('intake_id', targetIntakeId),
// Risk assessments
supabase
.from('risk_assessments')
.select('id', { count: 'exact', head: true })
.eq('intake_id', targetIntakeId),
// Examinations (non-ROM)
supabase
.from('examinations')
.select('id', { count: 'exact', head: true })
.eq('intake_id', targetIntakeId)
.neq('examination_type', 'ROM'),
// ROM examinations
supabase
.from('examinations')
.select('id', { count: 'exact', head: true })
.eq('intake_id', targetIntakeId)
.eq('examination_type', 'ROM'),
// Diagnoses (conditions linked to intake via encounter_id)
supabase
.from('conditions')
.select('id', { count: 'exact', head: true })
.eq('encounter_id', targetIntakeId),
]);
// Build section status
const sectionCounts: Record<string, number> = {
contacts: contactsResult.count || 0,
anamnese: anamneseResult.count || 0,
risk: riskResult.count || 0,
kindcheck: intake.kindcheck_data && Object.keys(intake.kindcheck_data as object).length > 0 ? 1 : 0,
examination: examinationResult.count || 0,
rom: romResult.count || 0,
diagnosis: diagnosisResult.count || 0,
behandeladvies: intake.treatment_advice && Object.keys(intake.treatment_advice as object).length > 0 ? 1 : 0,
};
const sections = INTAKE_SECTIONS.map((section) => ({
id: section.id,
label: section.label,
required: section.required,
completed: sectionCounts[section.id] > 0,
count: sectionCounts[section.id],
}));
// Calculate completion percentage (based on required sections)
const requiredSections = sections.filter((s) => s.required);
const completedRequired = requiredSections.filter((s) => s.completed).length;
const completionPercentage = Math.round(
(completedRequired / requiredSections.length) * 100
);
const response: IntakeStatusResponse = {
intakeId: targetIntakeId,
patientId: intake.patient_id,
completionPercentage,
status: intake.status as 'bezig' | 'afgerond',
sections,
completedCount: completedRequired,
totalRequired: requiredSections.length,
lastUpdated: intake.updated_at,
};
return NextResponse.json(response, { status: 200 });
} catch (error) {
console.error('Error in intake status API:', error);
return NextResponse.json(
{ error: 'Er ging iets mis bij het ophalen van de intake status.' },
{ status: 500 }
);
}
}