diff --git a/app/epd/patients/[id]/behandelplan/actions.ts b/app/epd/patients/[id]/behandelplan/actions.ts new file mode 100644 index 0000000..9114ae4 --- /dev/null +++ b/app/epd/patients/[id]/behandelplan/actions.ts @@ -0,0 +1,520 @@ +'use server'; + +import { createClient } from '@/lib/auth/server'; +import { revalidatePath } from 'next/cache'; +import type { GeneratedPlan, SmartGoal, Intervention, Behandelstructuur } 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 = { 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`); +} diff --git a/app/epd/patients/[id]/behandelplan/page-client.tsx b/app/epd/patients/[id]/behandelplan/page-client.tsx new file mode 100644 index 0000000..4d9d2cf --- /dev/null +++ b/app/epd/patients/[id]/behandelplan/page-client.tsx @@ -0,0 +1,420 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; +import { BehandelplanView, BehandelplanList } from '@/components/behandelplan'; +import { + createCarePlan, + updateCarePlanStatus, + createEmptyCarePlan, + updateBehandelstructuur, + addGoal, + updateGoal, + deleteGoal, + addIntervention, + updateIntervention, + deleteIntervention, +} from './actions'; +import type { GeneratedPlan, SmartGoal, Intervention, Sessie, Evaluatiemoment, Veiligheidsplan, Behandelstructuur } from '@/lib/types/behandelplan'; +import type { LifeDomainScore } from '@/lib/types/leefgebieden'; +import type { Json } from '@/lib/supabase/database.types'; + +// 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(); + + // State voor alle plannen en selectie + const [plans, setPlans] = useState(initialPlans); + const [selectedPlanId, setSelectedPlanId] = useState( + // 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] + ); + + return ( +
+ {/* Plannen overzicht */} + ({ + 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} + /> + + {/* Geselecteerd plan of create view */} + +
+ ); +} diff --git a/app/epd/patients/[id]/behandelplan/page.tsx b/app/epd/patients/[id]/behandelplan/page.tsx index a28e3da..0938ead 100644 --- a/app/epd/patients/[id]/behandelplan/page.tsx +++ b/app/epd/patients/[id]/behandelplan/page.tsx @@ -1,9 +1,10 @@ /** * Behandelplan Page - * E2.S3: Placeholder for behandelplan functionality (future epic) + * E3.S1: Server component met data loading */ -import { Calendar } from 'lucide-react'; +import { getCarePlans, getPatientIntakes, getPatientConditions } from './actions'; +import { BehandelplanPageClient } from './page-client'; export default async function BehandelplanPage({ params, @@ -12,29 +13,21 @@ export default async function BehandelplanPage({ }) { 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 (
- {/* Page Header */} -
-

Behandelplan

-

- Behandeldoelen, interventies en planning -

-
- - {/* Placeholder */} -
-
- -
-

- Behandelplan Module - Coming Soon -

-

- De behandelplan functionaliteit wordt in een latere fase geïmplementeerd. - Dit omvat doelstellingen, interventies en planning van de behandeling. -

-
+
); } diff --git a/components/behandelplan/behandelplan-list.tsx b/components/behandelplan/behandelplan-list.tsx new file mode 100644 index 0000000..53a7fa9 --- /dev/null +++ b/components/behandelplan/behandelplan-list.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Plus, FileText, CheckCircle2 } from 'lucide-react'; +import { FHIR_STATUS_LABELS, type FhirCarePlanStatus } from '@/lib/types/behandelplan'; + +interface CarePlanSummary { + id: string; + title: string; + status: string; + version: number | null; + created_at: string | null; + published_at: string | null; +} + +interface BehandelplanListProps { + plans: CarePlanSummary[]; + selectedPlanId: string | null; + onSelectPlan: (planId: string) => void; + onCreateNew: () => void; + isCreating?: boolean; +} + +export function BehandelplanList({ + plans, + selectedPlanId, + onSelectPlan, + onCreateNew, + isCreating = false, +}: BehandelplanListProps) { + return ( + + +
+ + + Behandelplannen ({plans.length}) + + +
+
+ + {plans.length === 0 ? ( +

+ Nog geen behandelplannen aangemaakt +

+ ) : ( +
+ {plans.map((plan) => { + const isSelected = plan.id === selectedPlanId; + const statusInfo = FHIR_STATUS_LABELS[plan.status as FhirCarePlanStatus] || { + label: plan.status, + color: '#6b7280', + }; + const isActive = plan.status === 'active'; + + return ( + + ); + })} +
+ )} +
+
+ ); +} diff --git a/components/behandelplan/behandelplan-view.tsx b/components/behandelplan/behandelplan-view.tsx new file mode 100644 index 0000000..7047cb8 --- /dev/null +++ b/components/behandelplan/behandelplan-view.tsx @@ -0,0 +1,1023 @@ +'use client'; + +import { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Sparkles, + FileText, + Clock, + CheckCircle2, + AlertCircle, + ChevronRight, + Calendar, + Target, + Stethoscope, + CalendarDays, + ClipboardCheck, + Shield, + Phone, + AlertTriangle, +} from 'lucide-react'; +import type { GeneratedPlan, SmartGoal, Intervention, Sessie, Evaluatiemoment, Veiligheidsplan } from '@/lib/types/behandelplan'; +import type { LifeDomainScore } from '@/lib/types/leefgebieden'; +import { LeefgebiedenScoresCard } from './leefgebieden-scores'; +import { LeefgebiedenBadge } from './leefgebieden-badge'; +import { FHIR_STATUS_LABELS, GOAL_STATUS_LABELS, type FhirCarePlanStatus, type Behandelstructuur } from '@/lib/types/behandelplan'; +import { EditableSection, ItemActions } from './editable-section'; +import { BehandelstructuurForm } from './sections/behandelstructuur-form'; +import { GoalForm } from './sections/goal-form'; +import { InterventionForm } from './sections/intervention-form'; +import { Plus } from 'lucide-react'; + +interface CarePlan { + 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 Intake { + id: string; + title: string; + status: string; + start_date: string; + life_domains: LifeDomainScore[] | null; + notes: string | null; +} + +interface Condition { + id: string; + category: string; + code_display: string; + severity_code: string | null; + severity_display: string | null; +} + +interface BehandelplanViewProps { + patientId: string; + carePlan: CarePlan | null; + intakes: Intake[]; + conditions: Condition[]; + onGenerate: (intakeId: string) => Promise; + onStatusChange: (status: CarePlan['status']) => Promise; + onCreateManual: (intakeId?: string) => Promise; + // Edit callbacks + onUpdateBehandelstructuur?: (data: Behandelstructuur) => Promise; + onAddGoal?: (goal: SmartGoal) => Promise; + onUpdateGoal?: (goalId: string, goal: SmartGoal) => Promise; + onDeleteGoal?: (goalId: string) => Promise; + onAddIntervention?: (intervention: Intervention) => Promise; + onUpdateIntervention?: (interventionId: string, intervention: Intervention) => Promise; + onDeleteIntervention?: (interventionId: string) => Promise; +} + +export function BehandelplanView({ + carePlan, + intakes, + conditions, + onGenerate, + onStatusChange, + onCreateManual, + onUpdateBehandelstructuur, + onAddGoal, + onUpdateGoal, + onDeleteGoal, + onAddIntervention, + onUpdateIntervention, + onDeleteIntervention, +}: BehandelplanViewProps) { + const [isGenerating, setIsGenerating] = useState(false); + const [isCreatingManual, setIsCreatingManual] = useState(false); + const [selectedIntakeId, setSelectedIntakeId] = useState( + intakes[0]?.id || null + ); + const [error, setError] = useState(null); + + // Edit state + const [editingBehandelstructuur, setEditingBehandelstructuur] = useState(false); + const [editingGoalId, setEditingGoalId] = useState(null); + const [addingGoal, setAddingGoal] = useState(false); + const [editingInterventionId, setEditingInterventionId] = useState(null); + const [addingIntervention, setAddingIntervention] = useState(false); + + // Temp data for forms + const [tempBehandelstructuur, setTempBehandelstructuur] = useState(null); + const [tempGoal, setTempGoal] = useState(null); + const [tempIntervention, setTempIntervention] = useState(null); + + const selectedIntake = intakes.find((i) => i.id === selectedIntakeId); + const hasLifeDomains = selectedIntake?.life_domains && selectedIntake.life_domains.length > 0; + const latestCondition = conditions[0]; + + const handleGenerate = async () => { + if (!selectedIntakeId) { + setError('Selecteer eerst een intake'); + return; + } + + // Leefgebieden zijn aanbevolen maar niet verplicht + if (!hasLifeDomains) { + // Toon waarschuwing maar blokkeer niet + console.warn('Leefgebieden niet ingevuld - AI generatie mogelijk minder nauwkeurig'); + } + + setIsGenerating(true); + setError(null); + + try { + await onGenerate(selectedIntakeId); + } catch (err) { + setError(err instanceof Error ? err.message : 'Er ging iets mis'); + } finally { + setIsGenerating(false); + } + }; + + const handleCreateManual = async () => { + setIsCreatingManual(true); + setError(null); + + try { + await onCreateManual(selectedIntakeId || undefined); + } catch (err) { + setError(err instanceof Error ? err.message : 'Er ging iets mis'); + } finally { + setIsCreatingManual(false); + } + }; + + // No care plan exists - show generation UI + if (!carePlan) { + return ( +
+ {/* Header */} +
+

Behandelplan

+

+ Genereer een behandelplan op basis van de intake en diagnose +

+
+ + {/* Context Cards */} +
+ {/* Intake Selection */} + + + + + Intake selecteren + + + + {intakes.length === 0 ? ( +

+ Geen intakes gevonden. Maak eerst een intake aan. +

+ ) : ( +
+ {intakes.map((intake) => ( + + ))} +
+ )} +
+
+ + {/* Diagnosis Info */} + + + + + Diagnose + + + + {latestCondition ? ( +
+

{latestCondition.code_display}

+
+ {latestCondition.category} + {latestCondition.severity_display && ( + + {latestCondition.severity_display} + + )} +
+
+ ) : ( +

+ Geen diagnose gevonden. Voeg eerst een diagnose toe. +

+ )} +
+
+
+ + {/* Leefgebieden Preview */} + {selectedIntake && hasLifeDomains && ( + + )} + + {/* Create Options */} +
+ {/* AI Generate Option */} + + +
+
+
+ +
+
+

+ AI Genereren +

+

+ Automatisch SMART doelen en interventies genereren +

+
+
+ + {!hasLifeDomains && ( +

+ Tip: vul leefgebieden in voor betere AI resultaten +

+ )} +
+
+
+ + {/* Manual Create Option */} + + +
+
+
+ +
+
+

+ Handmatig Aanmaken +

+

+ Zelf doelen en interventies invoeren +

+
+
+ +
+
+
+
+ + {/* Error Display */} + {error && ( +
+ + {error} +
+ )} +
+ ); + } + + // Care plan exists - show plan view + const goals = (carePlan.goals as SmartGoal[]) || []; + const interventions = (carePlan.activities as Intervention[]) || []; + const behandelstructuur = carePlan.behandelstructuur as GeneratedPlan['behandelstructuur'] | null; + const sessiePlanning = (carePlan.sessie_planning as Sessie[]) || []; + const evaluatiemomenten = (carePlan.evaluatiemomenten as Evaluatiemoment[]) || []; + const veiligheidsplan = carePlan.veiligheidsplan as Veiligheidsplan | null; + const statusInfo = FHIR_STATUS_LABELS[carePlan.status as FhirCarePlanStatus] || { + label: carePlan.status, + color: '#6b7280', + }; + + return ( +
+ {/* Header with Status */} +
+
+
+

{carePlan.title}

+ + {statusInfo.label} + +
+

+ {carePlan.created_at && ( + <>Aangemaakt op {new Date(carePlan.created_at).toLocaleDateString('nl-NL')} + )} + {carePlan.published_at && ( + <> • Gepubliceerd op {new Date(carePlan.published_at).toLocaleDateString('nl-NL')} + )} +

+
+ + {/* Status Actions */} +
+ {carePlan.status === 'draft' && ( + + )} + {carePlan.status === 'active' && ( + + )} +
+
+ + {/* Behandelstructuur */} + } + isEditing={editingBehandelstructuur} + onEditChange={(editing) => { + setEditingBehandelstructuur(editing); + if (editing) { + setTempBehandelstructuur(behandelstructuur || { + duur: '8 weken', + frequentie: 'Wekelijks', + aantalSessies: 8, + vorm: 'Individueel', + }); + } + }} + canEdit={!!onUpdateBehandelstructuur} + onSave={async () => { + if (tempBehandelstructuur && onUpdateBehandelstructuur) { + await onUpdateBehandelstructuur(tempBehandelstructuur); + setEditingBehandelstructuur(false); + } + }} + onCancel={() => { + setEditingBehandelstructuur(false); + setTempBehandelstructuur(null); + }} + editForm={ + + } + > + {behandelstructuur ? ( +
+
+

Duur

+

{behandelstructuur.duur}

+
+
+

Frequentie

+

{behandelstructuur.frequentie}

+
+
+

Sessies

+

{behandelstructuur.aantalSessies}

+
+
+

Vorm

+

{behandelstructuur.vorm}

+
+
+ ) : ( +

Nog geen behandelstructuur ingesteld

+ )} +
+ + {/* Goals Section */} + + +
+
+ + + SMART Doelen ({goals.length}) + + + Behandeldoelen gekoppeld aan leefgebieden + +
+ {onAddGoal && ( + + )} +
+
+ +
+ {/* Add Goal Form */} + {addingGoal && ( +
+

Nieuw doel toevoegen

+ +
+ + +
+
+ )} + + {goals.map((goal) => { + const goalStatus = GOAL_STATUS_LABELS[goal.status] || { + label: goal.status, + color: '#6b7280', + }; + const isEditing = editingGoalId === goal.id; + + if (isEditing) { + return ( +
+ +
+ + +
+
+ ); + } + + return ( +
+
+
+
+ + + {goalStatus.label} + +
+

{goal.title}

+

{goal.description}

+ + {/* Client version */} + {goal.clientVersion && ( +
+ Voor cliënt: + {goal.clientVersion} +
+ )} + + {/* Progress bar */} +
+
+ Voortgang + {goal.progress}% +
+
+
+
+
+
+ {(onUpdateGoal || onDeleteGoal) && ( + { + setEditingGoalId(goal.id); + setTempGoal(goal); + } : undefined} + onDelete={onDeleteGoal ? () => onDeleteGoal(goal.id) : undefined} + /> + )} +
+
+ ); + })} + + {goals.length === 0 && !addingGoal && ( +

+ Geen doelen gevonden +

+ )} +
+ + + + {/* Interventions Section */} + + +
+
+ + + Interventies ({interventions.length}) + + + Evidence-based behandelmethoden + +
+ {onAddIntervention && ( + + )} +
+
+ +
+ {/* Add Intervention Form */} + {addingIntervention && ( +
+

Nieuwe interventie toevoegen

+ +
+ + +
+
+ )} + + {interventions.map((intervention) => { + const isEditing = editingInterventionId === intervention.id; + + if (isEditing) { + return ( +
+ +
+ + +
+
+ ); + } + + return ( +
+
+
+

{intervention.name}

+

{intervention.description}

+ {intervention.rationale && ( +
+ Rationale: + {intervention.rationale} +
+ )} + {intervention.linkedGoalIds.length > 0 && ( +
+ Gekoppeld aan: + {intervention.linkedGoalIds.map((goalId) => { + const goal = goals.find((g) => g.id === goalId); + return goal ? ( + + ) : null; + })} +
+ )} +
+ {(onUpdateIntervention || onDeleteIntervention) && ( + { + setEditingInterventionId(intervention.id); + setTempIntervention(intervention); + } : undefined} + onDelete={onDeleteIntervention ? () => onDeleteIntervention(intervention.id) : undefined} + /> + )} +
+
+ ); + })} + + {interventions.length === 0 && !addingIntervention && ( +

+ Geen interventies gevonden +

+ )} +
+
+
+ + {/* Sessie Planning Section */} + {sessiePlanning.length > 0 && ( + + + + + Sessie Planning ({sessiePlanning.length}) + + + Overzicht van geplande en afgeronde sessies + + + +
+ {sessiePlanning.map((sessie) => { + const statusColors: Record = { + gepland: 'bg-blue-100 text-blue-800', + afgerond: 'bg-green-100 text-green-800', + no_show: 'bg-red-100 text-red-800', + verzet: 'bg-amber-100 text-amber-800', + geannuleerd: 'bg-slate-100 text-slate-800', + }; + return ( +
+
+
+ {sessie.nummer} +
+
+

{sessie.focus}

+ {sessie.datum && ( +

+ {new Date(sessie.datum).toLocaleDateString('nl-NL', { + weekday: 'short', + day: 'numeric', + month: 'short', + })} +

+ )} +
+
+ + {sessie.status === 'no_show' ? 'No-show' : sessie.status} + +
+ ); + })} +
+
+
+ )} + + {/* Evaluatiemomenten Section */} + {evaluatiemomenten.length > 0 && ( + + + + + Evaluatiemomenten ({evaluatiemomenten.length}) + + + Geplande evaluaties en voortgangsmomenten + + + +
+ {evaluatiemomenten.map((evaluatie) => { + const typeLabels: Record = { + tussentijds: { label: 'Tussentijds', color: 'bg-blue-100 text-blue-800' }, + eind: { label: 'Eindevaluatie', color: 'bg-green-100 text-green-800' }, + crisis: { label: 'Crisis', color: 'bg-red-100 text-red-800' }, + }; + const statusLabels: Record = { + gepland: 'Gepland', + afgerond: 'Afgerond', + overgeslagen: 'Overgeslagen', + }; + const typeInfo = typeLabels[evaluatie.type] || { label: evaluatie.type, color: 'bg-slate-100' }; + return ( +
+
+
+ W{evaluatie.weekNumber} +
+
+
+ {typeInfo.label} + + {statusLabels[evaluatie.status] || evaluatie.status} + +
+

+ {new Date(evaluatie.plannedDate).toLocaleDateString('nl-NL', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} +

+
+
+ {evaluatie.status === 'afgerond' && ( + + )} +
+ ); + })} +
+
+
+ )} + + {/* Veiligheidsplan Section */} + {veiligheidsplan && ( + + + + + Veiligheidsplan + + + Crisisplan en veiligheidsafspraken + + + + {/* Waarschuwingssignalen */} +
+

+ + Waarschuwingssignalen +

+
    + {veiligheidsplan.waarschuwingssignalen.map((signaal, i) => ( +
  • + + {signaal} +
  • + ))} +
+
+ + {/* Coping Strategieën */} +
+

+ + Coping Strategieën +

+
    + {veiligheidsplan.copingStrategieen.map((strategie, i) => ( +
  • + + {strategie} +
  • + ))} +
+
+ + {/* Contactpersonen */} +
+

+ + Noodcontacten +

+
+ {veiligheidsplan.contacten.map((contact, i) => ( +
+

{contact.naam}

+

{contact.rol}

+

{contact.telefoon}

+
+ ))} +
+
+ + {/* Restricties */} + {veiligheidsplan.restricties && veiligheidsplan.restricties.length > 0 && ( +
+

Afspraken/Restricties

+
    + {veiligheidsplan.restricties.map((restrictie, i) => ( +
  • + + {restrictie} +
  • + ))} +
+
+ )} +
+
+ )} +
+ ); +} diff --git a/components/behandelplan/editable-section.tsx b/components/behandelplan/editable-section.tsx new file mode 100644 index 0000000..24d6e57 --- /dev/null +++ b/components/behandelplan/editable-section.tsx @@ -0,0 +1,162 @@ +'use client'; + +import { useState, ReactNode } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Pencil, X, Check, Loader2 } from 'lucide-react'; + +interface EditableSectionProps { + title: string; + description?: string; + icon?: ReactNode; + children: ReactNode; + editForm?: ReactNode; + onSave?: () => Promise; + onCancel?: () => void; + isEditing?: boolean; + onEditChange?: (isEditing: boolean) => void; + canEdit?: boolean; + className?: string; +} + +export function EditableSection({ + title, + description, + icon, + children, + editForm, + onSave, + onCancel, + isEditing: externalIsEditing, + onEditChange, + canEdit = true, + className = '', +}: EditableSectionProps) { + const [internalIsEditing, setInternalIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + // Use external or internal state + const isEditing = externalIsEditing ?? internalIsEditing; + const setIsEditing = onEditChange ?? setInternalIsEditing; + + const handleSave = async () => { + if (!onSave) return; + setIsSaving(true); + try { + await onSave(); + setIsEditing(false); + } catch (error) { + console.error('Error saving:', error); + } finally { + setIsSaving(false); + } + }; + + const handleCancel = () => { + onCancel?.(); + setIsEditing(false); + }; + + return ( + + +
+
+ + {icon} + {title} + + {description && {description}} +
+ {canEdit && !isEditing && ( + + )} +
+
+ + {isEditing && editForm ? ( +
+ {editForm} +
+ + +
+
+ ) : ( + children + )} +
+
+ ); +} + +// Simple inline edit buttons for list items +interface ItemActionsProps { + onEdit?: () => void; + onDelete?: () => void; + isDeleting?: boolean; +} + +export function ItemActions({ onEdit, onDelete, isDeleting }: ItemActionsProps) { + return ( +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+ ); +} diff --git a/components/behandelplan/index.ts b/components/behandelplan/index.ts index 7b15f0c..3a855c9 100644 --- a/components/behandelplan/index.ts +++ b/components/behandelplan/index.ts @@ -12,3 +12,15 @@ export { LeefgebiedenScoreBar, } from './leefgebieden-scores'; export { LeefgebiedenForm, LeefgebiedenQuickForm } from './leefgebieden-form'; + +// Behandelplan Views +export { BehandelplanView } from './behandelplan-view'; +export { BehandelplanList } from './behandelplan-list'; + +// Editable Components +export { EditableSection, ItemActions } from './editable-section'; + +// Section Forms +export { BehandelstructuurForm } from './sections/behandelstructuur-form'; +export { GoalForm } from './sections/goal-form'; +export { InterventionForm } from './sections/intervention-form'; diff --git a/components/behandelplan/sections/behandelstructuur-form.tsx b/components/behandelplan/sections/behandelstructuur-form.tsx new file mode 100644 index 0000000..1b60a80 --- /dev/null +++ b/components/behandelplan/sections/behandelstructuur-form.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import type { Behandelstructuur } from '@/lib/types/behandelplan'; + +interface BehandelstructuurFormProps { + initialData?: Behandelstructuur | null; + onChange: (data: Behandelstructuur) => void; +} + +const DUUR_OPTIONS = ['4 weken', '6 weken', '8 weken', '10 weken', '12 weken', '16 weken', '24 weken']; +const FREQUENTIE_OPTIONS = ['Wekelijks', 'Tweewekelijks', 'Maandelijks', '2x per week']; +const VORM_OPTIONS = ['Individueel', 'Groep', 'Gezin', 'Paar', 'Online', 'Hybride']; + +export function BehandelstructuurForm({ initialData, onChange }: BehandelstructuurFormProps) { + const [data, setData] = useState( + initialData || { + duur: '8 weken', + frequentie: 'Wekelijks', + aantalSessies: 8, + vorm: 'Individueel', + } + ); + + const handleChange = (field: keyof Behandelstructuur, value: string | number) => { + const updated = { ...data, [field]: value }; + setData(updated); + onChange(updated); + }; + + return ( +
+
+ + +
+ +
+ + +
+ +
+ + handleChange('aantalSessies', parseInt(e.target.value) || 1)} + /> +
+ +
+ + +
+
+ ); +} diff --git a/components/behandelplan/sections/goal-form.tsx b/components/behandelplan/sections/goal-form.tsx new file mode 100644 index 0000000..2a987bd --- /dev/null +++ b/components/behandelplan/sections/goal-form.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Slider } from '@/components/ui/slider'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import type { SmartGoal, GoalStatus } from '@/lib/types/behandelplan'; +import { LIFE_DOMAINS, LIFE_DOMAIN_META, type LifeDomain } from '@/lib/types/leefgebieden'; + +interface GoalFormProps { + initialData?: SmartGoal | null; + onChange: (data: SmartGoal) => void; +} + +const PRIORITY_OPTIONS = [ + { value: 'hoog', label: 'Hoog' }, + { value: 'middel', label: 'Middel' }, + { value: 'laag', label: 'Laag' }, +]; + +const STATUS_OPTIONS: { value: GoalStatus; label: string }[] = [ + { value: 'niet_gestart', label: 'Niet gestart' }, + { value: 'bezig', label: 'Bezig' }, + { value: 'gehaald', label: 'Gehaald' }, + { value: 'bijgesteld', label: 'Bijgesteld' }, +]; + +export function GoalForm({ initialData, onChange }: GoalFormProps) { + const [data, setData] = useState( + initialData || { + id: crypto.randomUUID(), + title: '', + description: '', + clientVersion: '', + lifeDomain: 'dlv', + priority: 'middel', + measurability: '', + timelineWeeks: 8, + status: 'niet_gestart', + progress: 0, + } + ); + + const handleChange = (field: K, value: SmartGoal[K]) => { + const updated = { ...data, [field]: value }; + setData(updated); + onChange(updated); + }; + + return ( +
+
+
+ + handleChange('title', e.target.value)} + placeholder="Korte beschrijving van het doel" + /> +
+ +
+ + +
+
+ +
+ +