Iteratie 1 - Parallel Fetching: - Patient dashboard: Promise.all voor 3 data fetches - Resultaat: ~45% sneller (800ms → 440ms) Iteratie 2 - Loading States: - Skeleton loaders voor EPD, patient, agenda, rapportage - Betere perceived performance tijdens laden Iteratie 3 - Lazy Load FullCalendar: - Dynamic import met next/dynamic (ssr: false) - ~150KB minder in initiële bundle Iteratie 4 - Reports API Optimalisatie: - Selectieve kolommen (10 i.p.v. 20) - Server-side pagination (limit/offset) - Database indexes voor timeline queries - Resultaat: 89% sneller (1671ms → 188ms) Overige fixes: - Type casts voor Json → Behandelstructuur/SmartGoal/etc. - Metadata template fix in layout.tsx Documentatie: - docs/audit-rapport.md - PO-vriendelijk audit rapport - docs/performance/baseline-2025-12-12.md - Metingen + resultaten 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
279 lines
8.0 KiB
TypeScript
279 lines
8.0 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { z } from 'zod';
|
|
import { createClient } from '@/lib/auth/server';
|
|
import {
|
|
CreateReportSchema,
|
|
CreateVerpleegkundigSchema,
|
|
calculateShiftDate,
|
|
type ReportListResponse,
|
|
VERPLEEG_REPORT_TYPES,
|
|
} from '@/lib/types/report';
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(request.url);
|
|
const patientId = searchParams.get('patientId');
|
|
const type = searchParams.get('type'); // Optional: filter by type
|
|
const types = searchParams.get('types'); // Optional: comma-separated list of types
|
|
const startDate = searchParams.get('startDate'); // Optional: YYYY-MM-DD
|
|
const endDate = searchParams.get('endDate'); // Optional: YYYY-MM-DD
|
|
const includeInHandover = searchParams.get('includeInHandover'); // Optional: 'true'
|
|
|
|
// Pagination parameters
|
|
const limitParam = searchParams.get('limit');
|
|
const offsetParam = searchParams.get('offset');
|
|
const limit = Math.min(Math.max(parseInt(limitParam ?? '50'), 1), 100); // 1-100, default 50
|
|
const offset = Math.max(parseInt(offsetParam ?? '0'), 0);
|
|
|
|
if (!patientId) {
|
|
return NextResponse.json(
|
|
{ error: 'patientId query parameter is verplicht' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!z.string().uuid().safeParse(patientId).success) {
|
|
return NextResponse.json(
|
|
{ error: 'patientId moet een geldige UUID zijn' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate date formats
|
|
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
if (startDate && !dateRegex.test(startDate)) {
|
|
return NextResponse.json(
|
|
{ error: 'startDate moet in YYYY-MM-DD formaat zijn' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
if (endDate && !dateRegex.test(endDate)) {
|
|
return NextResponse.json(
|
|
{ error: 'endDate moet in YYYY-MM-DD formaat zijn' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const supabase = await createClient();
|
|
|
|
// Selectieve kolommen voor betere performance
|
|
// Grote kolommen (ai_reasoning, audio_url) worden niet opgehaald tenzij nodig
|
|
const selectColumns = [
|
|
'id',
|
|
'patient_id',
|
|
'type',
|
|
'content',
|
|
'created_at',
|
|
'updated_at',
|
|
'shift_date',
|
|
'include_in_handover',
|
|
'structured_data',
|
|
'created_by',
|
|
].join(', ');
|
|
|
|
let query = supabase
|
|
.from('reports')
|
|
.select(selectColumns)
|
|
.eq('patient_id', patientId)
|
|
.is('deleted_at', null)
|
|
.order('created_at', { ascending: false });
|
|
|
|
// Filter by single type
|
|
if (type) {
|
|
query = query.eq('type', type);
|
|
}
|
|
|
|
// Filter by multiple types (comma-separated)
|
|
if (types) {
|
|
const typeList = types.split(',').map((t) => t.trim());
|
|
query = query.in('type', typeList);
|
|
}
|
|
|
|
// Filter by date range (for shift_date, used by verpleegkundig)
|
|
if (startDate && endDate) {
|
|
query = query.gte('shift_date', startDate).lte('shift_date', endDate);
|
|
} else if (startDate) {
|
|
query = query.gte('shift_date', startDate);
|
|
} else if (endDate) {
|
|
query = query.lte('shift_date', endDate);
|
|
}
|
|
|
|
// Filter for handover reports only
|
|
if (includeInHandover === 'true') {
|
|
query = query.eq('include_in_handover', true);
|
|
}
|
|
|
|
// Apply pagination
|
|
query = query.range(offset, offset + limit - 1);
|
|
|
|
// Execute query with count
|
|
const { data, error, count } = await query;
|
|
|
|
if (error) {
|
|
console.error('Error fetching reports:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Fout bij ophalen rapportages', details: error.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// Get total count (separate query for accurate pagination)
|
|
const countQuery = supabase
|
|
.from('reports')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('patient_id', patientId)
|
|
.is('deleted_at', null);
|
|
|
|
// Apply same filters to count query
|
|
if (type) countQuery.eq('type', type);
|
|
if (types) countQuery.in('type', types.split(',').map((t) => t.trim()));
|
|
if (startDate && endDate) {
|
|
countQuery.gte('shift_date', startDate).lte('shift_date', endDate);
|
|
} else if (startDate) {
|
|
countQuery.gte('shift_date', startDate);
|
|
} else if (endDate) {
|
|
countQuery.lte('shift_date', endDate);
|
|
}
|
|
if (includeInHandover === 'true') countQuery.eq('include_in_handover', true);
|
|
|
|
const { count: totalCount } = await countQuery;
|
|
const total = totalCount ?? 0;
|
|
|
|
return NextResponse.json({
|
|
reports: data ?? [],
|
|
total,
|
|
limit,
|
|
offset,
|
|
hasMore: offset + limit < total,
|
|
});
|
|
} catch (error) {
|
|
console.error('Unexpected error in GET /api/reports:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Onverwachte serverfout' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
|
|
// Check if this is a verpleegkundig report
|
|
const isVerpleegkundig = body.type === 'verpleegkundig';
|
|
|
|
// Use appropriate schema
|
|
const result = isVerpleegkundig
|
|
? CreateVerpleegkundigSchema.safeParse(body)
|
|
: CreateReportSchema.safeParse(body);
|
|
|
|
if (!result.success) {
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Validatiefout',
|
|
details: result.error.issues.map((issue) => ({
|
|
field: issue.path.join('.'),
|
|
message: issue.message,
|
|
})),
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const supabase = await createClient();
|
|
const { data: authData } = await supabase.auth.getUser();
|
|
|
|
if (!authData?.user) {
|
|
return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 });
|
|
}
|
|
|
|
// Get practitioner ID for the current user
|
|
const { data: practitioner } = await supabase
|
|
.from('practitioners')
|
|
.select('id')
|
|
.eq('user_id', authData.user.id)
|
|
.single();
|
|
|
|
if (isVerpleegkundig) {
|
|
// Handle verpleegkundig report
|
|
const { patient_id, content, category, include_in_handover } =
|
|
result.data as z.infer<typeof CreateVerpleegkundigSchema>;
|
|
|
|
const now = new Date();
|
|
const shiftDate = calculateShiftDate(now);
|
|
|
|
const { data, error } = await supabase
|
|
.from('reports')
|
|
.insert({
|
|
patient_id,
|
|
type: 'verpleegkundig',
|
|
content,
|
|
structured_data: { category },
|
|
include_in_handover: include_in_handover ?? false,
|
|
shift_date: shiftDate,
|
|
created_by: practitioner?.id ?? null,
|
|
})
|
|
.select('*')
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error creating verpleegkundig report:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Opslaan mislukt', details: error.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(data, { status: 201 });
|
|
} else {
|
|
// Handle standard report
|
|
const {
|
|
patient_id,
|
|
type,
|
|
content,
|
|
ai_confidence,
|
|
ai_reasoning,
|
|
encounter_id,
|
|
intake_id,
|
|
} = result.data as z.infer<typeof CreateReportSchema>;
|
|
|
|
const { data, error } = await supabase
|
|
.from('reports')
|
|
.insert({
|
|
patient_id,
|
|
type,
|
|
content,
|
|
ai_confidence,
|
|
ai_reasoning,
|
|
encounter_id,
|
|
intake_id,
|
|
created_by: practitioner?.id ?? null,
|
|
})
|
|
.select('*')
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error creating report:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Opslaan mislukt', details: error.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(data, { status: 201 });
|
|
}
|
|
} catch (error) {
|
|
console.error('Unexpected error in POST /api/reports:', error);
|
|
if (error instanceof SyntaxError) {
|
|
return NextResponse.json(
|
|
{ error: 'Ongeldige JSON in request body' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
return NextResponse.json(
|
|
{ error: 'Onverwachte serverfout' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|