chore(strip): verwijder behandelplan-module
AI-generatie negeerde de geregistreerde diagnose en ernst — elk plan was generiek (reviewbevinding). Module komt terug bij de rebuild, gekoppeld aan het nieuwe datamodel. - app/epd/patients/[id]/behandelplan/ en /api/behandelplan/ verwijderd - components/behandelplan/ (view, list, forms) verwijderd - lib/ai/behandelplan-prompt.ts, lib/ai/intervention-mapping.ts, lib/types/behandelplan.ts verwijderd - behandelplan-sectie uit patientdashboard, dashboard-API en Cortex patient-dashboard-block - 'Doorzetten naar behandelplan' uit behandeladvies-formulier Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,327 +0,0 @@
|
||||
/**
|
||||
* Behandelplan Generate API
|
||||
*
|
||||
* POST /api/behandelplan/generate
|
||||
* Genereert een behandelplan met Claude AI
|
||||
*/
|
||||
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
BEHANDELPLAN_SYSTEM_PROMPT,
|
||||
buildUserPrompt,
|
||||
validatePlanContext,
|
||||
type PlanContext,
|
||||
} from '@/lib/ai/behandelplan-prompt';
|
||||
import { type Severity } from '@/lib/ai/intervention-mapping';
|
||||
import {
|
||||
GeneratedPlanSchema,
|
||||
type GeneratedPlan,
|
||||
} from '@/lib/types/behandelplan';
|
||||
import { createDefaultLifeDomainScores, type LifeDomainScore } from '@/lib/types/leefgebieden';
|
||||
|
||||
// Input validation schema
|
||||
const GenerateInputSchema = z.object({
|
||||
patientId: z.string().uuid('Patient ID moet een geldige UUID zijn'),
|
||||
intakeId: z.string().uuid('Intake ID moet een geldige UUID zijn'),
|
||||
conditionId: z.string().uuid().optional(),
|
||||
extraInstructions: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Map severity_code to Severity type
|
||||
*/
|
||||
function mapSeverity(severityCode: string | null | undefined): Severity {
|
||||
if (!severityCode) return 'middel';
|
||||
|
||||
const code = severityCode.toLowerCase();
|
||||
if (code.includes('mild') || code.includes('laag') || code.includes('light')) {
|
||||
return 'laag';
|
||||
}
|
||||
if (code.includes('severe') || code.includes('hoog') || code.includes('ernstig')) {
|
||||
return 'hoog';
|
||||
}
|
||||
return 'middel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load context from database
|
||||
*/
|
||||
async function loadPlanContext(
|
||||
supabase: Awaited<ReturnType<typeof createClient>>,
|
||||
patientId: string,
|
||||
intakeId: string,
|
||||
conditionId?: string
|
||||
): Promise<PlanContext> {
|
||||
// 1. Load intake
|
||||
const { data: intake, error: intakeError } = await supabase
|
||||
.from('intakes')
|
||||
.select('*')
|
||||
.eq('id', intakeId)
|
||||
.eq('patient_id', patientId)
|
||||
.single();
|
||||
|
||||
if (intakeError || !intake) {
|
||||
throw new Error(`Intake niet gevonden: ${intakeError?.message || 'niet gevonden'}`);
|
||||
}
|
||||
|
||||
// 2. Load condition (latest for patient, or specific one)
|
||||
let conditionQuery = supabase
|
||||
.from('conditions')
|
||||
.select('*')
|
||||
.eq('patient_id', patientId);
|
||||
|
||||
if (conditionId) {
|
||||
conditionQuery = conditionQuery.eq('id', conditionId);
|
||||
} else {
|
||||
conditionQuery = conditionQuery.order('recorded_date', { ascending: false }).limit(1);
|
||||
}
|
||||
|
||||
const { data: conditions } = await conditionQuery;
|
||||
const condition = conditions?.[0];
|
||||
|
||||
// 3. Build intake notes from available data
|
||||
const intakeNotes = buildIntakeNotes(intake);
|
||||
|
||||
// 4. Get life domains (from intake or default)
|
||||
const lifeDomains: LifeDomainScore[] =
|
||||
(intake.life_domains as LifeDomainScore[] | null) ||
|
||||
createDefaultLifeDomainScores();
|
||||
|
||||
// 5. Build context
|
||||
return {
|
||||
patientId,
|
||||
intakeNotes,
|
||||
dsmCategory: condition?.category || condition?.code_display || 'overig',
|
||||
severity: mapSeverity(condition?.severity_code),
|
||||
lifeDomains,
|
||||
extraInstructions: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build intake notes from intake data
|
||||
*/
|
||||
function buildIntakeNotes(intake: Record<string, unknown>): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (intake.notes) {
|
||||
parts.push(String(intake.notes));
|
||||
}
|
||||
|
||||
if (intake.treatment_advice) {
|
||||
const advice = intake.treatment_advice as Record<string, unknown>;
|
||||
if (advice.content) {
|
||||
parts.push(`Behandeladvies: ${String(advice.content)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (intake.kindcheck_data) {
|
||||
const kindcheck = intake.kindcheck_data as Record<string, unknown>;
|
||||
if (kindcheck.observations) {
|
||||
parts.push(`Observaties: ${String(kindcheck.observations)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('\n\n') || 'Geen intake notities beschikbaar.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Claude API
|
||||
*/
|
||||
async function callClaudeAPI(context: PlanContext): Promise<GeneratedPlan> {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('ANTHROPIC_API_KEY ontbreekt in environment');
|
||||
}
|
||||
|
||||
const userPrompt = buildUserPrompt(context);
|
||||
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 4096,
|
||||
temperature: 0.3,
|
||||
system: BEHANDELPLAN_SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
console.error('Claude API error:', errorBody);
|
||||
throw new Error(`Claude API fout: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const rawText = data?.content?.[0]?.text;
|
||||
|
||||
if (!rawText) {
|
||||
throw new Error('Geen response van Claude API');
|
||||
}
|
||||
|
||||
// Parse JSON from response (handle potential markdown code blocks)
|
||||
let jsonText = rawText.trim();
|
||||
if (jsonText.startsWith('```json')) {
|
||||
jsonText = jsonText.slice(7);
|
||||
}
|
||||
if (jsonText.startsWith('```')) {
|
||||
jsonText = jsonText.slice(3);
|
||||
}
|
||||
if (jsonText.endsWith('```')) {
|
||||
jsonText = jsonText.slice(0, -3);
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonText.trim());
|
||||
|
||||
// Validate with Zod schema
|
||||
const validated = GeneratedPlanSchema.parse(parsed);
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log AI event to database
|
||||
*/
|
||||
async function logAIEvent(
|
||||
supabase: Awaited<ReturnType<typeof createClient>>,
|
||||
kind: string,
|
||||
patientId: string,
|
||||
input: Record<string, unknown>,
|
||||
output: Record<string, unknown>,
|
||||
durationMs: number
|
||||
) {
|
||||
try {
|
||||
await supabase.from('ai_events').insert({
|
||||
kind,
|
||||
patient_id: patientId,
|
||||
input_data: input,
|
||||
output_data: output,
|
||||
duration_ms: durationMs,
|
||||
});
|
||||
} catch (error) {
|
||||
// Log but don't fail the request
|
||||
console.error('Failed to log AI event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/behandelplan/generate
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input
|
||||
const result = GenerateInputSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Validatiefout',
|
||||
details: result.error.issues.map((e) => ({
|
||||
field: e.path.join('.'),
|
||||
message: e.message,
|
||||
})),
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { patientId, intakeId, conditionId, extraInstructions } = result.data;
|
||||
const supabase = await createClient();
|
||||
|
||||
// Load context from database
|
||||
const context = await loadPlanContext(supabase, patientId, intakeId, conditionId);
|
||||
|
||||
// Add extra instructions if provided
|
||||
if (extraInstructions) {
|
||||
context.extraInstructions = extraInstructions;
|
||||
}
|
||||
|
||||
// Validate context
|
||||
const validation = validatePlanContext(context);
|
||||
if (!validation.valid) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Onvoldoende context voor behandelplan generatie',
|
||||
details: validation.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Call Claude API
|
||||
const plan = await callClaudeAPI(context);
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
|
||||
// Log AI event
|
||||
await logAIEvent(
|
||||
supabase,
|
||||
'behandelplan_generate',
|
||||
patientId,
|
||||
{ intakeId, conditionId, extraInstructions },
|
||||
{ goalCount: plan.doelen.length, interventionCount: plan.interventies.length },
|
||||
durationMs
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
meta: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
durationMs,
|
||||
context: {
|
||||
dsmCategory: context.dsmCategory,
|
||||
severity: context.severity,
|
||||
highPriorityDomains: context.lifeDomains
|
||||
.filter((d) => d.priority === 'hoog')
|
||||
.map((d) => d.domain),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating behandelplan:', error);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Onbekende fout';
|
||||
|
||||
// Check for specific error types
|
||||
if (errorMessage.includes('Intake niet gevonden')) {
|
||||
return NextResponse.json(
|
||||
{ error: errorMessage },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('ANTHROPIC_API_KEY')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI service niet geconfigureerd' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Claude API')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI service tijdelijk niet beschikbaar', details: errorMessage },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Fout bij genereren behandelplan',
|
||||
details: process.env.NODE_ENV === 'development' ? errorMessage : undefined,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,11 @@ import { createClient } from '@/lib/auth/server';
|
||||
import { getPatient } from '@/app/epd/patients/actions';
|
||||
import { getIntakesByPatientId } from '@/app/epd/patients/[id]/intakes/actions';
|
||||
import { getPatientEncounters } from '@/app/epd/agenda/actions';
|
||||
import { getActiveCarePlan } from '@/app/epd/patients/[id]/behandelplan/actions';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ patientId: string }>;
|
||||
}
|
||||
|
||||
function extractHulpvraag(notes: string | null): string | null {
|
||||
if (!notes) return null;
|
||||
const firstSentence = notes.split(/[.!?]/)[0];
|
||||
return firstSentence.length > 150 ? `${firstSentence.slice(0, 150)}...` : firstSentence;
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { patientId } = await params;
|
||||
@@ -38,26 +31,15 @@ export async function GET(_request: NextRequest, { params }: RouteParams) {
|
||||
return NextResponse.json({ error: 'Patiënt niet gevonden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const [intakes, encounters, carePlan] = await Promise.all([
|
||||
const [intakes, encounters] = await Promise.all([
|
||||
getIntakesByPatientId(patientId).catch(() => []),
|
||||
getPatientEncounters(patientId).catch(() => []),
|
||||
getActiveCarePlan(patientId).catch(() => null),
|
||||
]);
|
||||
|
||||
let hulpvraag: string | null = null;
|
||||
if (carePlan?.based_on_intake_id) {
|
||||
const linkedIntake = intakes.find((intake) => intake.id === carePlan.based_on_intake_id);
|
||||
if (linkedIntake?.notes) {
|
||||
hulpvraag = extractHulpvraag(linkedIntake.notes);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
patient,
|
||||
intakes,
|
||||
encounters,
|
||||
carePlan,
|
||||
hulpvraag,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in GET /api/patients/[patientId]/dashboard:', error);
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import type { GeneratedPlan, SmartGoal, Intervention, Behandelstructuur, Behandeldoel } from '@/lib/types/behandelplan';
|
||||
import { transformFromFlat } from '@/lib/types/behandelplan';
|
||||
import type { LifeDomainScore } from '@/lib/types/leefgebieden';
|
||||
import type { Json } from '@/lib/supabase/database.types';
|
||||
|
||||
/**
|
||||
* Get care plans for a patient
|
||||
*/
|
||||
export async function getCarePlans(patientId: string) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('care_plans')
|
||||
.select('*')
|
||||
.eq('patient_id', patientId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching care plans:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest active care plan for a patient
|
||||
*/
|
||||
export async function getActiveCarePlan(patientId: string) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('care_plans')
|
||||
.select('*')
|
||||
.eq('patient_id', patientId)
|
||||
.in('status', ['draft', 'active'])
|
||||
.order('version', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
console.error('Error fetching active care plan:', error);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get intakes for a patient (to select for plan generation)
|
||||
*/
|
||||
export async function getPatientIntakes(patientId: string) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('intakes')
|
||||
.select('id, title, status, start_date, life_domains, notes')
|
||||
.eq('patient_id', patientId)
|
||||
.order('start_date', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching intakes:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conditions for a patient
|
||||
*/
|
||||
export async function getPatientConditions(patientId: string) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('conditions')
|
||||
.select('id, category, code_display, severity_code, severity_display, recorded_date')
|
||||
.eq('patient_id', patientId)
|
||||
.order('recorded_date', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching conditions:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new care plan from generated plan
|
||||
*/
|
||||
export async function createCarePlan(
|
||||
patientId: string,
|
||||
intakeId: string,
|
||||
generatedPlan: GeneratedPlan,
|
||||
title: string = 'Behandelplan'
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current version
|
||||
const { data: existing } = await supabase
|
||||
.from('care_plans')
|
||||
.select('version')
|
||||
.eq('patient_id', patientId)
|
||||
.order('version', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const nextVersion = (existing?.version || 0) + 1;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('care_plans')
|
||||
.insert({
|
||||
patient_id: patientId,
|
||||
based_on_intake_id: intakeId,
|
||||
title: `${title} v${nextVersion}`,
|
||||
status: 'draft',
|
||||
intent: 'plan',
|
||||
version: nextVersion,
|
||||
goals: generatedPlan.doelen as unknown as Json,
|
||||
activities: generatedPlan.interventies as unknown as Json,
|
||||
behandelstructuur: generatedPlan.behandelstructuur as unknown as Json,
|
||||
evaluatiemomenten: generatedPlan.evaluatiemomenten as unknown as Json,
|
||||
sessie_planning: generatedPlan.sessiePlanning as unknown as Json,
|
||||
veiligheidsplan: (generatedPlan.veiligheidsplan || null) as unknown as Json,
|
||||
period_start: new Date().toISOString(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating care plan:', error);
|
||||
throw new Error('Kon behandelplan niet opslaan');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update care plan status
|
||||
*/
|
||||
export async function updateCarePlanStatus(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
status: 'draft' | 'active' | 'on-hold' | 'completed' | 'revoked'
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const updateData: Record<string, unknown> = { status };
|
||||
|
||||
// Set published_at when activating
|
||||
if (status === 'active') {
|
||||
updateData.published_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update(updateData)
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating care plan status:', error);
|
||||
throw new Error('Kon status niet bijwerken');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update care plan goals
|
||||
*/
|
||||
export async function updateCarePlanGoals(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
goals: GeneratedPlan['doelen']
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ goals: goals as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating goals:', error);
|
||||
throw new Error('Kon doelen niet bijwerken');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save life domains to intake
|
||||
*/
|
||||
export async function saveLifeDomains(
|
||||
intakeId: string,
|
||||
patientId: string,
|
||||
lifeDomains: LifeDomainScore[]
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('intakes')
|
||||
.update({ life_domains: lifeDomains as unknown as Json })
|
||||
.eq('id', intakeId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error saving life domains:', error);
|
||||
throw new Error('Kon leefgebieden niet opslaan');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a care plan
|
||||
*/
|
||||
export async function deleteCarePlan(carePlanId: string, patientId: string) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.delete()
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting care plan:', error);
|
||||
throw new Error('Kon behandelplan niet verwijderen');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new empty/manual care plan
|
||||
*/
|
||||
export async function createEmptyCarePlan(
|
||||
patientId: string,
|
||||
intakeId?: string,
|
||||
title: string = 'Behandelplan'
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current version
|
||||
const { data: existing } = await supabase
|
||||
.from('care_plans')
|
||||
.select('version')
|
||||
.eq('patient_id', patientId)
|
||||
.order('version', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
const nextVersion = (existing?.version || 0) + 1;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('care_plans')
|
||||
.insert({
|
||||
patient_id: patientId,
|
||||
based_on_intake_id: intakeId || null,
|
||||
title: `${title} v${nextVersion}`,
|
||||
status: 'draft',
|
||||
intent: 'plan',
|
||||
version: nextVersion,
|
||||
goals: [],
|
||||
activities: [],
|
||||
period_start: new Date().toISOString(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating empty care plan:', error);
|
||||
throw new Error('Kon behandelplan niet aanmaken');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
return data;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BEHANDELSTRUCTUUR
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Update behandelstructuur
|
||||
*/
|
||||
export async function updateBehandelstructuur(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
behandelstructuur: Behandelstructuur
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ behandelstructuur: behandelstructuur as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating behandelstructuur:', error);
|
||||
throw new Error('Kon behandelstructuur niet bijwerken');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GOALS (DOELEN)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Add a goal to a care plan
|
||||
*/
|
||||
export async function addGoal(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
goal: SmartGoal
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current goals
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('goals')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentGoals = (plan?.goals as unknown as SmartGoal[]) || [];
|
||||
const updatedGoals = [...currentGoals, goal];
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ goals: updatedGoals as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error adding goal:', error);
|
||||
throw new Error('Kon doel niet toevoegen');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single goal in a care plan
|
||||
*/
|
||||
export async function updateGoal(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
goalId: string,
|
||||
updatedGoal: SmartGoal
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current goals
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('goals')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentGoals = (plan?.goals as unknown as SmartGoal[]) || [];
|
||||
const updatedGoals = currentGoals.map((g) =>
|
||||
g.id === goalId ? updatedGoal : g
|
||||
);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ goals: updatedGoals as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating goal:', error);
|
||||
throw new Error('Kon doel niet bijwerken');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a goal from a care plan
|
||||
*/
|
||||
export async function deleteGoal(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
goalId: string
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current goals
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('goals')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentGoals = (plan?.goals as unknown as SmartGoal[]) || [];
|
||||
const updatedGoals = currentGoals.filter((g) => g.id !== goalId);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ goals: updatedGoals as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting goal:', error);
|
||||
throw new Error('Kon doel niet verwijderen');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INTERVENTIONS (INTERVENTIES)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Add an intervention to a care plan
|
||||
*/
|
||||
export async function addIntervention(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
intervention: Intervention
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current interventions
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('activities')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentInterventions = (plan?.activities as unknown as Intervention[]) || [];
|
||||
const updatedInterventions = [...currentInterventions, intervention];
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ activities: updatedInterventions as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error adding intervention:', error);
|
||||
throw new Error('Kon interventie niet toevoegen');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single intervention in a care plan
|
||||
*/
|
||||
export async function updateIntervention(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
interventionId: string,
|
||||
updatedIntervention: Intervention
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current interventions
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('activities')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentInterventions = (plan?.activities as unknown as Intervention[]) || [];
|
||||
const updatedInterventions = currentInterventions.map((i) =>
|
||||
i.id === interventionId ? updatedIntervention : i
|
||||
);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ activities: updatedInterventions as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating intervention:', error);
|
||||
throw new Error('Kon interventie niet bijwerken');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an intervention from a care plan
|
||||
*/
|
||||
export async function deleteIntervention(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
interventionId: string
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current interventions
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('activities')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentInterventions = (plan?.activities as unknown as Intervention[]) || [];
|
||||
const updatedInterventions = currentInterventions.filter((i) => i.id !== interventionId);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({ activities: updatedInterventions as unknown as Json })
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting intervention:', error);
|
||||
throw new Error('Kon interventie niet verwijderen');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BEHANDELDOEL (FLAT STRUCTURE - doel met embedded interventies)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Save a behandeldoel (creates or updates)
|
||||
* Transforms flat structure to goals + activities for backwards compatibility
|
||||
*/
|
||||
export async function saveBehandeldoel(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
behandeldoel: Behandeldoel
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current plan
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('goals, activities')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentGoals = (plan?.goals as unknown as SmartGoal[]) || [];
|
||||
const currentActivities = (plan?.activities as unknown as Intervention[]) || [];
|
||||
|
||||
// Check if this is an update or a new goal
|
||||
const existingGoalIndex = currentGoals.findIndex((g) => g.id === behandeldoel.id);
|
||||
|
||||
// Convert behandeldoel to SmartGoal
|
||||
const smartGoal: SmartGoal = {
|
||||
id: behandeldoel.id,
|
||||
title: behandeldoel.title,
|
||||
description: '', // Not used in flat structure
|
||||
clientVersion: behandeldoel.clientVersion,
|
||||
lifeDomain: behandeldoel.lifeDomain,
|
||||
priority: 'middel', // Default
|
||||
measurability: '', // Not used in flat structure
|
||||
timelineWeeks: behandeldoel.endWeek,
|
||||
status: behandeldoel.status,
|
||||
progress: behandeldoel.progress,
|
||||
};
|
||||
|
||||
// Update goals array
|
||||
let updatedGoals: SmartGoal[];
|
||||
if (existingGoalIndex >= 0) {
|
||||
updatedGoals = currentGoals.map((g, i) =>
|
||||
i === existingGoalIndex ? smartGoal : g
|
||||
);
|
||||
} else {
|
||||
updatedGoals = [...currentGoals, smartGoal];
|
||||
}
|
||||
|
||||
// Handle interventions: remove old ones for this goal and add new ones
|
||||
const otherActivities = currentActivities.filter(
|
||||
(a) => !a.linkedGoalIds.includes(behandeldoel.id)
|
||||
);
|
||||
|
||||
const newActivities: Intervention[] = behandeldoel.interventies.map((int) => ({
|
||||
id: int.id,
|
||||
name: int.name,
|
||||
description: int.description,
|
||||
rationale: '', // Not used in flat structure
|
||||
linkedGoalIds: [behandeldoel.id],
|
||||
}));
|
||||
|
||||
const updatedActivities = [...otherActivities, ...newActivities];
|
||||
|
||||
// Save to database
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({
|
||||
goals: updatedGoals as unknown as Json,
|
||||
activities: updatedActivities as unknown as Json,
|
||||
})
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error saving behandeldoel:', error);
|
||||
throw new Error('Kon behandeldoel niet opslaan');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a behandeldoel and its linked interventions
|
||||
*/
|
||||
export async function deleteBehandeldoel(
|
||||
carePlanId: string,
|
||||
patientId: string,
|
||||
doelId: string
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
// Get current plan
|
||||
const { data: plan } = await supabase
|
||||
.from('care_plans')
|
||||
.select('goals, activities')
|
||||
.eq('id', carePlanId)
|
||||
.single();
|
||||
|
||||
const currentGoals = (plan?.goals as unknown as SmartGoal[]) || [];
|
||||
const currentActivities = (plan?.activities as unknown as Intervention[]) || [];
|
||||
|
||||
// Remove the goal
|
||||
const updatedGoals = currentGoals.filter((g) => g.id !== doelId);
|
||||
|
||||
// Remove interventions linked to this goal
|
||||
const updatedActivities = currentActivities.filter(
|
||||
(a) => !a.linkedGoalIds.includes(doelId)
|
||||
);
|
||||
|
||||
// Save to database
|
||||
const { error } = await supabase
|
||||
.from('care_plans')
|
||||
.update({
|
||||
goals: updatedGoals as unknown as Json,
|
||||
activities: updatedActivities as unknown as Json,
|
||||
})
|
||||
.eq('id', carePlanId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting behandeldoel:', error);
|
||||
throw new Error('Kon behandeldoel niet verwijderen');
|
||||
}
|
||||
|
||||
revalidatePath(`/epd/patients/${patientId}/behandelplan`);
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BehandelplanView, BehandelplanList } from '@/components/behandelplan';
|
||||
import { BehandelplanFlat } from '@/components/behandelplan/flat';
|
||||
import {
|
||||
createCarePlan,
|
||||
updateCarePlanStatus,
|
||||
createEmptyCarePlan,
|
||||
updateBehandelstructuur,
|
||||
addGoal,
|
||||
updateGoal,
|
||||
deleteGoal,
|
||||
addIntervention,
|
||||
updateIntervention,
|
||||
deleteIntervention,
|
||||
saveBehandeldoel,
|
||||
deleteBehandeldoel,
|
||||
} from './actions';
|
||||
import type { GeneratedPlan, SmartGoal, Intervention, Sessie, Evaluatiemoment, Veiligheidsplan, Behandelstructuur, Behandeldoel } from '@/lib/types/behandelplan';
|
||||
import type { LifeDomainScore } from '@/lib/types/leefgebieden';
|
||||
import type { Json } from '@/lib/supabase/database.types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LayoutGrid, List } from 'lucide-react';
|
||||
|
||||
// Database row types (what we get from Supabase)
|
||||
interface DbCarePlan {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
version: number | null;
|
||||
goals: Json | null;
|
||||
activities: Json | null;
|
||||
behandelstructuur: Json | null;
|
||||
sessie_planning: Json | null;
|
||||
evaluatiemomenten: Json | null;
|
||||
veiligheidsplan: Json | null;
|
||||
created_at: string | null;
|
||||
published_at: string | null;
|
||||
period_start: string | null;
|
||||
}
|
||||
|
||||
interface DbIntake {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
start_date: string;
|
||||
life_domains: Json | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
interface DbCondition {
|
||||
id: string;
|
||||
category: string;
|
||||
code_display: string;
|
||||
severity_code: string | null;
|
||||
severity_display: string | null;
|
||||
}
|
||||
|
||||
// Mapped types for the view component
|
||||
interface ViewCarePlan {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'draft' | 'active' | 'on-hold' | 'completed' | 'revoked' | 'entered-in-error' | 'unknown';
|
||||
version: number | null;
|
||||
goals: SmartGoal[] | null;
|
||||
activities: Intervention[] | null;
|
||||
behandelstructuur: GeneratedPlan['behandelstructuur'] | null;
|
||||
sessie_planning: Sessie[] | null;
|
||||
evaluatiemomenten: Evaluatiemoment[] | null;
|
||||
veiligheidsplan: Veiligheidsplan | null;
|
||||
created_at: string | null;
|
||||
published_at: string | null;
|
||||
period_start: string | null;
|
||||
}
|
||||
|
||||
interface ViewIntake {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
start_date: string;
|
||||
life_domains: LifeDomainScore[] | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
interface ViewCondition {
|
||||
id: string;
|
||||
category: string;
|
||||
code_display: string;
|
||||
severity_code: string | null;
|
||||
severity_display: string | null;
|
||||
}
|
||||
|
||||
interface BehandelplanPageClientProps {
|
||||
patientId: string;
|
||||
allPlans: DbCarePlan[];
|
||||
intakes: DbIntake[];
|
||||
conditions: DbCondition[];
|
||||
}
|
||||
|
||||
// Helper to map database types to view types
|
||||
function mapCarePlan(dbPlan: DbCarePlan | null): ViewCarePlan | null {
|
||||
if (!dbPlan) return null;
|
||||
return {
|
||||
...dbPlan,
|
||||
status: dbPlan.status as ViewCarePlan['status'],
|
||||
goals: dbPlan.goals as SmartGoal[] | null,
|
||||
activities: dbPlan.activities as Intervention[] | null,
|
||||
behandelstructuur: dbPlan.behandelstructuur as GeneratedPlan['behandelstructuur'] | null,
|
||||
sessie_planning: dbPlan.sessie_planning as Sessie[] | null,
|
||||
evaluatiemomenten: dbPlan.evaluatiemomenten as Evaluatiemoment[] | null,
|
||||
veiligheidsplan: dbPlan.veiligheidsplan as Veiligheidsplan | null,
|
||||
};
|
||||
}
|
||||
|
||||
function mapIntakes(dbIntakes: DbIntake[]): ViewIntake[] {
|
||||
return dbIntakes.map((intake) => ({
|
||||
...intake,
|
||||
life_domains: intake.life_domains as LifeDomainScore[] | null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function BehandelplanPageClient({
|
||||
patientId,
|
||||
allPlans: initialPlans,
|
||||
intakes,
|
||||
conditions,
|
||||
}: BehandelplanPageClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// View mode toggle: 'flat' = nieuwe platte UI, 'detailed' = oude gedetailleerde UI
|
||||
const [viewMode, setViewMode] = useState<'flat' | 'detailed'>('flat');
|
||||
|
||||
// State voor alle plannen en selectie
|
||||
const [plans, setPlans] = useState<DbCarePlan[]>(initialPlans);
|
||||
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(
|
||||
// Selecteer standaard het nieuwste actieve/draft plan, of het eerste plan
|
||||
initialPlans.find(p => p.status === 'active')?.id ||
|
||||
initialPlans.find(p => p.status === 'draft')?.id ||
|
||||
initialPlans[0]?.id ||
|
||||
null
|
||||
);
|
||||
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
||||
const [showCreateView, setShowCreateView] = useState(false);
|
||||
|
||||
// Geselecteerd plan
|
||||
const selectedPlan = useMemo(() => {
|
||||
const plan = plans.find(p => p.id === selectedPlanId);
|
||||
return plan ? mapCarePlan(plan) : null;
|
||||
}, [plans, selectedPlanId]);
|
||||
|
||||
// Handler voor plan selectie
|
||||
const handleSelectPlan = useCallback((planId: string) => {
|
||||
setSelectedPlanId(planId);
|
||||
setShowCreateView(false);
|
||||
}, []);
|
||||
|
||||
// Handler voor nieuw plan aanmaken (toont create view)
|
||||
const handleShowCreateView = useCallback(() => {
|
||||
setSelectedPlanId(null);
|
||||
setShowCreateView(true);
|
||||
}, []);
|
||||
|
||||
const handleGenerate = useCallback(
|
||||
async (intakeId: string) => {
|
||||
// Get the first condition if available
|
||||
const conditionId = conditions[0]?.id;
|
||||
|
||||
// Call the generate API
|
||||
const response = await fetch('/api/behandelplan/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
patientId,
|
||||
intakeId,
|
||||
conditionId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Er ging iets mis bij het genereren');
|
||||
}
|
||||
|
||||
const generatedPlan: GeneratedPlan = await response.json();
|
||||
|
||||
// Save to database via server action
|
||||
const savedPlan = await createCarePlan(patientId, intakeId, generatedPlan);
|
||||
|
||||
// Update plans list en selecteer het nieuwe plan
|
||||
const newPlan = savedPlan as DbCarePlan;
|
||||
setPlans(prev => [newPlan, ...prev]);
|
||||
setSelectedPlanId(newPlan.id);
|
||||
setShowCreateView(false);
|
||||
|
||||
// Revalidate the page
|
||||
router.refresh();
|
||||
},
|
||||
[patientId, conditions, router]
|
||||
);
|
||||
|
||||
const handleStatusChange = useCallback(
|
||||
async (status: ViewCarePlan['status']) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
// Only allow valid status transitions
|
||||
if (!['draft', 'active', 'on-hold', 'completed', 'revoked'].includes(status)) {
|
||||
throw new Error('Ongeldige status');
|
||||
}
|
||||
|
||||
await updateCarePlanStatus(
|
||||
selectedPlan.id,
|
||||
patientId,
|
||||
status as 'draft' | 'active' | 'on-hold' | 'completed' | 'revoked'
|
||||
);
|
||||
|
||||
// Update plans list
|
||||
setPlans(prev => prev.map(p =>
|
||||
p.id === selectedPlan.id
|
||||
? {
|
||||
...p,
|
||||
status,
|
||||
published_at: status === 'active' ? new Date().toISOString() : p.published_at
|
||||
}
|
||||
: p
|
||||
));
|
||||
|
||||
// Revalidate
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleCreateManual = useCallback(
|
||||
async (intakeId?: string) => {
|
||||
setIsCreatingNew(true);
|
||||
try {
|
||||
// Create empty care plan via server action
|
||||
const savedPlan = await createEmptyCarePlan(patientId, intakeId);
|
||||
|
||||
// Update plans list en selecteer het nieuwe plan
|
||||
const newPlan = savedPlan as DbCarePlan;
|
||||
setPlans(prev => [newPlan, ...prev]);
|
||||
setSelectedPlanId(newPlan.id);
|
||||
setShowCreateView(false);
|
||||
|
||||
// Revalidate the page
|
||||
router.refresh();
|
||||
} finally {
|
||||
setIsCreatingNew(false);
|
||||
}
|
||||
},
|
||||
[patientId, router]
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// EDIT HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
const handleUpdateBehandelstructuur = useCallback(
|
||||
async (data: Behandelstructuur) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await updateBehandelstructuur(selectedPlan.id, patientId, data);
|
||||
|
||||
// Update local state
|
||||
setPlans(prev => prev.map(p =>
|
||||
p.id === selectedPlan.id
|
||||
? { ...p, behandelstructuur: data as unknown as Json }
|
||||
: p
|
||||
));
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleAddGoal = useCallback(
|
||||
async (goal: SmartGoal) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await addGoal(selectedPlan.id, patientId, goal);
|
||||
|
||||
// Update local state
|
||||
setPlans(prev => prev.map(p => {
|
||||
if (p.id !== selectedPlan.id) return p;
|
||||
const currentGoals = (p.goals as unknown as SmartGoal[]) || [];
|
||||
return { ...p, goals: [...currentGoals, goal] as unknown as Json };
|
||||
}));
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleUpdateGoal = useCallback(
|
||||
async (goalId: string, updatedGoal: SmartGoal) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await updateGoal(selectedPlan.id, patientId, goalId, updatedGoal);
|
||||
|
||||
// Update local state
|
||||
setPlans(prev => prev.map(p => {
|
||||
if (p.id !== selectedPlan.id) return p;
|
||||
const currentGoals = (p.goals as unknown as SmartGoal[]) || [];
|
||||
const newGoals = currentGoals.map(g => g.id === goalId ? updatedGoal : g);
|
||||
return { ...p, goals: newGoals as unknown as Json };
|
||||
}));
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleDeleteGoal = useCallback(
|
||||
async (goalId: string) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await deleteGoal(selectedPlan.id, patientId, goalId);
|
||||
|
||||
// Update local state
|
||||
setPlans(prev => prev.map(p => {
|
||||
if (p.id !== selectedPlan.id) return p;
|
||||
const currentGoals = (p.goals as unknown as SmartGoal[]) || [];
|
||||
const newGoals = currentGoals.filter(g => g.id !== goalId);
|
||||
return { ...p, goals: newGoals as unknown as Json };
|
||||
}));
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleAddIntervention = useCallback(
|
||||
async (intervention: Intervention) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await addIntervention(selectedPlan.id, patientId, intervention);
|
||||
|
||||
// Update local state
|
||||
setPlans(prev => prev.map(p => {
|
||||
if (p.id !== selectedPlan.id) return p;
|
||||
const currentInterventions = (p.activities as unknown as Intervention[]) || [];
|
||||
return { ...p, activities: [...currentInterventions, intervention] as unknown as Json };
|
||||
}));
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleUpdateIntervention = useCallback(
|
||||
async (interventionId: string, updatedIntervention: Intervention) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await updateIntervention(selectedPlan.id, patientId, interventionId, updatedIntervention);
|
||||
|
||||
// Update local state
|
||||
setPlans(prev => prev.map(p => {
|
||||
if (p.id !== selectedPlan.id) return p;
|
||||
const currentInterventions = (p.activities as unknown as Intervention[]) || [];
|
||||
const newInterventions = currentInterventions.map(i => i.id === interventionId ? updatedIntervention : i);
|
||||
return { ...p, activities: newInterventions as unknown as Json };
|
||||
}));
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleDeleteIntervention = useCallback(
|
||||
async (interventionId: string) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await deleteIntervention(selectedPlan.id, patientId, interventionId);
|
||||
|
||||
// Update local state
|
||||
setPlans(prev => prev.map(p => {
|
||||
if (p.id !== selectedPlan.id) return p;
|
||||
const currentInterventions = (p.activities as unknown as Intervention[]) || [];
|
||||
const newInterventions = currentInterventions.filter(i => i.id !== interventionId);
|
||||
return { ...p, activities: newInterventions as unknown as Json };
|
||||
}));
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// FLAT VIEW HANDLERS (Behandeldoel met embedded interventies)
|
||||
// =============================================================================
|
||||
|
||||
const handleSaveBehandeldoel = useCallback(
|
||||
async (doel: Behandeldoel) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await saveBehandeldoel(selectedPlan.id, patientId, doel);
|
||||
|
||||
// Update local state - we need to update both goals and activities
|
||||
// For now, just refresh the page to get fresh data
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
const handleDeleteBehandeldoel = useCallback(
|
||||
async (doelId: string) => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
await deleteBehandeldoel(selectedPlan.id, patientId, doelId);
|
||||
router.refresh();
|
||||
},
|
||||
[selectedPlan, patientId, router]
|
||||
);
|
||||
|
||||
// Get hulpvraag from first intake notes (first line/sentence)
|
||||
const hulpvraag = useMemo(() => {
|
||||
const firstIntake = intakes[0];
|
||||
if (!firstIntake?.notes) return null;
|
||||
// Get first sentence or first 150 chars
|
||||
const notes = firstIntake.notes;
|
||||
const firstSentence = notes.split(/[.!?]/)[0];
|
||||
return firstSentence.length > 150 ? firstSentence.slice(0, 150) + '...' : firstSentence;
|
||||
}, [intakes]);
|
||||
|
||||
// Get life domain scores from first intake
|
||||
const lifeDomainScores = useMemo(() => {
|
||||
const firstIntake = intakes[0];
|
||||
return firstIntake?.life_domains as LifeDomainScore[] | null;
|
||||
}, [intakes]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header met view toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<BehandelplanList
|
||||
plans={plans.map(p => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
status: p.status,
|
||||
version: p.version,
|
||||
created_at: p.created_at,
|
||||
published_at: p.published_at,
|
||||
}))}
|
||||
selectedPlanId={showCreateView ? null : selectedPlanId}
|
||||
onSelectPlan={handleSelectPlan}
|
||||
onCreateNew={handleShowCreateView}
|
||||
isCreating={isCreatingNew}
|
||||
/>
|
||||
|
||||
{/* View mode toggle */}
|
||||
<div className="flex items-center gap-1 border rounded-lg p-1 bg-slate-50">
|
||||
<Button
|
||||
variant={viewMode === 'flat' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('flat')}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4 mr-1.5" />
|
||||
Compact
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('detailed')}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<List className="h-4 w-4 mr-1.5" />
|
||||
Uitgebreid
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Geselecteerd plan - Flat view */}
|
||||
{viewMode === 'flat' && (
|
||||
<BehandelplanFlat
|
||||
patientId={patientId}
|
||||
carePlan={showCreateView ? null : selectedPlan}
|
||||
condition={conditions[0] || null}
|
||||
hulpvraag={hulpvraag}
|
||||
lifeDomainScores={lifeDomainScores}
|
||||
onGenerate={async () => {
|
||||
const intakeId = intakes[0]?.id;
|
||||
if (intakeId) await handleGenerate(intakeId);
|
||||
}}
|
||||
onCreateManual={async () => {
|
||||
await handleCreateManual(intakes[0]?.id);
|
||||
}}
|
||||
onStatusChange={handleStatusChange}
|
||||
onSaveBehandeldoel={handleSaveBehandeldoel}
|
||||
onDeleteBehandeldoel={handleDeleteBehandeldoel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Geselecteerd plan - Detailed view (oude UI) */}
|
||||
{viewMode === 'detailed' && (
|
||||
<BehandelplanView
|
||||
patientId={patientId}
|
||||
carePlan={showCreateView ? null : selectedPlan}
|
||||
intakes={mapIntakes(intakes)}
|
||||
conditions={conditions}
|
||||
onGenerate={handleGenerate}
|
||||
onStatusChange={handleStatusChange}
|
||||
onCreateManual={handleCreateManual}
|
||||
onUpdateBehandelstructuur={handleUpdateBehandelstructuur}
|
||||
onAddGoal={handleAddGoal}
|
||||
onUpdateGoal={handleUpdateGoal}
|
||||
onDeleteGoal={handleDeleteGoal}
|
||||
onAddIntervention={handleAddIntervention}
|
||||
onUpdateIntervention={handleUpdateIntervention}
|
||||
onDeleteIntervention={handleDeleteIntervention}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Behandelplan Page
|
||||
* E3.S1: Server component met data loading
|
||||
*/
|
||||
|
||||
import { getCarePlans, getPatientIntakes, getPatientConditions } from './actions';
|
||||
import { BehandelplanPageClient } from './page-client';
|
||||
|
||||
export default async function BehandelplanPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
|
||||
// Parallel data loading - haal ALLE plannen op
|
||||
const [allPlans, intakes, conditions] = await Promise.all([
|
||||
getCarePlans(id),
|
||||
getPatientIntakes(id),
|
||||
getPatientConditions(id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<BehandelplanPageClient
|
||||
patientId={id}
|
||||
allPlans={allPlans}
|
||||
intakes={intakes}
|
||||
conditions={conditions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useState, useTransition, useCallback } from 'react';
|
||||
import { saveTreatmentAdvice } from '../../actions';
|
||||
import { Loader2, Calendar, UserCircle, ClipboardList, Share2, CheckCircle2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Loader2, Calendar, UserCircle, ClipboardList, CheckCircle2 } from 'lucide-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const RichTextEditor = dynamic(
|
||||
@@ -238,20 +237,6 @@ export function TreatmentAdviceForm({ patientId, intakeId, initialData, initialD
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200 p-4 bg-slate-50 space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
<Share2 className="h-4 w-4" /> Doorzetten naar behandelplan
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
Gebruik dit advies als basis voor het behandelplan of koppel direct door.
|
||||
</p>
|
||||
<Link
|
||||
href={`/epd/patients/${patientId}/behandelplan`}
|
||||
className="inline-flex items-center justify-center rounded-md border border-slate-300 px-3 py-2 text-xs font-medium text-slate-700 hover:bg-white"
|
||||
>
|
||||
Open behandelplan
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -18,14 +18,6 @@ import type { Intake } from '@/lib/types/intake';
|
||||
import { format } from 'date-fns';
|
||||
import { nl } from 'date-fns/locale';
|
||||
import { getPatientEncounters } from '@/app/epd/agenda/actions';
|
||||
import { getActiveCarePlan } from './behandelplan/actions';
|
||||
import type { SmartGoal, Intervention, Behandelstructuur, Evaluatiemoment } from '@/lib/types/behandelplan';
|
||||
|
||||
function extractHulpvraag(notes: string | null): string | null {
|
||||
if (!notes) return null;
|
||||
const firstSentence = notes.split(/[.!?]/)[0];
|
||||
return firstSentence.length > 150 ? firstSentence.slice(0, 150) + '...' : firstSentence;
|
||||
}
|
||||
|
||||
export default async function PatientDashboardPage({
|
||||
params,
|
||||
@@ -35,10 +27,9 @@ export default async function PatientDashboardPage({
|
||||
const { id } = await params;
|
||||
|
||||
// Fetch all data in parallel for better performance
|
||||
const [intakesResult, encountersResult, carePlanResult] = await Promise.all([
|
||||
const [intakesResult, encountersResult] = await Promise.all([
|
||||
getIntakesByPatientId(id).catch(() => [] as Intake[]),
|
||||
getPatientEncounters(id).catch(() => []),
|
||||
getActiveCarePlan(id).catch(() => null),
|
||||
]);
|
||||
|
||||
// Process intakes
|
||||
@@ -58,16 +49,6 @@ export default async function PatientDashboardPage({
|
||||
recentEncounters = recent.slice(0, 5 - upcomingEncounters.length);
|
||||
}
|
||||
|
||||
// Process care plan and get hulpvraag from already-fetched intakes
|
||||
const activeCarePlan = carePlanResult;
|
||||
let hulpvraag: string | null = null;
|
||||
if (activeCarePlan?.based_on_intake_id) {
|
||||
const linkedIntake = intakesResult.find((i: Intake) => i.id === activeCarePlan.based_on_intake_id);
|
||||
if (linkedIntake?.notes) {
|
||||
hulpvraag = extractHulpvraag(linkedIntake.notes);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* Page Header */}
|
||||
@@ -266,154 +247,6 @@ export default async function PatientDashboardPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Behandelplan Section */}
|
||||
{activeCarePlan && (
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Actief Behandelplan</h3>
|
||||
<Link
|
||||
href={`/epd/patients/${id}/behandelplan`}
|
||||
className="text-sm text-teal-600 hover:text-teal-700 font-medium"
|
||||
>
|
||||
Bekijk volledig plan →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Hulpvraag */}
|
||||
{hulpvraag && (
|
||||
<div className="mb-4 p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-xs font-medium text-slate-600 mb-1">Hulpvraag</p>
|
||||
<p className="text-sm text-slate-700 italic">“{hulpvraag}”</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Behandelstructuur */}
|
||||
{activeCarePlan.behandelstructuur && (
|
||||
<div className="mb-4 p-3 bg-teal-50 rounded-lg">
|
||||
<p className="text-xs font-medium text-teal-700 mb-2">Behandelstructuur</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-teal-900">
|
||||
{(() => {
|
||||
const bs = activeCarePlan.behandelstructuur as unknown as Behandelstructuur;
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span className="font-medium">Duur:</span> {bs.duur}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Frequentie:</span> {bs.frequentie}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Aantal sessies:</span> {bs.aantalSessies}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Vorm:</span> {bs.vorm}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Doelen Overzicht */}
|
||||
{activeCarePlan.goals && Array.isArray(activeCarePlan.goals) && activeCarePlan.goals.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-xs font-medium text-slate-600 mb-2">Doelen ({activeCarePlan.goals.length})</p>
|
||||
<div className="space-y-2">
|
||||
{(activeCarePlan.goals as unknown as SmartGoal[]).slice(0, 3).map((goal) => (
|
||||
<div key={goal.id} className="p-2 bg-slate-50 rounded border border-slate-200">
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<p className="text-sm font-medium text-slate-900">{goal.title}</p>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
goal.status === 'bezig' ? 'bg-blue-50 text-blue-700' :
|
||||
goal.status === 'gehaald' ? 'bg-green-50 text-green-700' :
|
||||
'bg-slate-50 text-slate-700'
|
||||
}`}>
|
||||
{goal.status === 'bezig' ? 'Bezig' :
|
||||
goal.status === 'gehaald' ? 'Gehaald' :
|
||||
goal.status === 'niet_gestart' ? 'Niet gestart' : goal.status}
|
||||
</span>
|
||||
</div>
|
||||
{goal.progress > 0 && (
|
||||
<div className="mt-1">
|
||||
<div className="h-1.5 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-teal-500 transition-all"
|
||||
style={{ width: `${goal.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{goal.progress}% voltooid</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{activeCarePlan.goals.length > 3 && (
|
||||
<p className="text-xs text-slate-500 text-center">
|
||||
+{activeCarePlan.goals.length - 3} meer doelen
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interventies Overzicht */}
|
||||
{activeCarePlan.activities && Array.isArray(activeCarePlan.activities) && activeCarePlan.activities.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-xs font-medium text-slate-600 mb-2">Interventies ({activeCarePlan.activities.length})</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(activeCarePlan.activities as unknown as Intervention[]).slice(0, 5).map((intervention) => (
|
||||
<span
|
||||
key={intervention.id}
|
||||
className="px-2 py-1 bg-purple-50 text-purple-700 rounded text-xs font-medium"
|
||||
>
|
||||
{intervention.name}
|
||||
</span>
|
||||
))}
|
||||
{activeCarePlan.activities.length > 5 && (
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs">
|
||||
+{activeCarePlan.activities.length - 5} meer
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aankomende Evaluatiemomenten */}
|
||||
{activeCarePlan.evaluatiemomenten &&
|
||||
Array.isArray(activeCarePlan.evaluatiemomenten) &&
|
||||
activeCarePlan.evaluatiemomenten.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-600 mb-2">Aankomende evaluatiemomenten</p>
|
||||
<div className="space-y-2">
|
||||
{(activeCarePlan.evaluatiemomenten as unknown as Evaluatiemoment[])
|
||||
.filter((evaluatie) => evaluatie.status === 'gepland')
|
||||
.slice(0, 2)
|
||||
.map((evaluatie) => (
|
||||
<div key={evaluatie.id} className="p-2 bg-amber-50 rounded border border-amber-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-amber-900">
|
||||
{evaluatie.type === 'tussentijds' ? 'Tussentijdse evaluatie' :
|
||||
evaluatie.type === 'eind' ? 'Eindevaluatie' : 'Crisis evaluatie'}
|
||||
</p>
|
||||
{evaluatie.plannedDate && (
|
||||
<p className="text-xs text-amber-700 mt-0.5">
|
||||
Week {evaluatie.weekNumber} • {format(new Date(evaluatie.plannedDate), 'd MMM yyyy', { locale: nl })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="px-2 py-0.5 bg-amber-100 text-amber-800 rounded-full text-xs font-medium">
|
||||
Gepland
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Steps Section */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
|
||||
Reference in New Issue
Block a user