feat(behandelplan): E3 UI componenten en documentatie updates

Behandelplan UI (E3):
- page-client.tsx: Client-side behandelplan pagina
- actions.ts: Server actions voor CRUD operaties
- behandelplan-view.tsx: Volledige behandelplan weergave
- behandelplan-list.tsx: Lijst van behandelplannen
- editable-section.tsx: Herbruikbare edit sectie component
- sections/: Goal, intervention en behandelstructuur forms

UI Componenten:
- components/ui/checkbox.tsx (shadcn)
- components/ui/input.tsx (shadcn)
- components/ui/select.tsx (shadcn)

Documentatie:
- agenda-systeem.mdx toegevoegd
- _index.json en metadata.json bijgewerkt

Dependencies:
- @radix-ui/react-checkbox toegevoegd

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
colinislit
2025-12-06 00:35:00 +01:00
parent d965f69a9b
commit 9e4e16035b
20 changed files with 3243 additions and 47 deletions

View File

@@ -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<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`);
}

View File

@@ -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<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]
);
return (
<div className="space-y-6">
{/* Plannen overzicht */}
<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}
/>
{/* Geselecteerd plan of create view */}
<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>
);
}

View File

@@ -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 (
<div className="p-6">
{/* Page Header */}
<div className="mb-6">
<h2 className="text-lg font-semibold text-slate-900">Behandelplan</h2>
<p className="text-sm text-slate-600 mt-1">
Behandeldoelen, interventies en planning
</p>
</div>
{/* Placeholder */}
<div className="bg-white rounded-lg border border-slate-200 p-12 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-indigo-50 mb-4">
<Calendar className="h-8 w-8 text-indigo-500" />
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">
Behandelplan Module - Coming Soon
</h3>
<p className="text-sm text-slate-600 max-w-md mx-auto">
De behandelplan functionaliteit wordt in een latere fase geïmplementeerd.
Dit omvat doelstellingen, interventies en planning van de behandeling.
</p>
</div>
<BehandelplanPageClient
patientId={id}
allPlans={allPlans}
intakes={intakes}
conditions={conditions}
/>
</div>
);
}

View File

@@ -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 (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<FileText className="h-4 w-4" />
Behandelplannen ({plans.length})
</CardTitle>
<Button
onClick={onCreateNew}
disabled={isCreating}
size="sm"
className="bg-indigo-600 hover:bg-indigo-700"
>
<Plus className="h-4 w-4 mr-1" />
Nieuw
</Button>
</div>
</CardHeader>
<CardContent>
{plans.length === 0 ? (
<p className="text-sm text-slate-500 text-center py-4">
Nog geen behandelplannen aangemaakt
</p>
) : (
<div className="space-y-2">
{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 (
<button
key={plan.id}
onClick={() => onSelectPlan(plan.id)}
className={`w-full text-left p-3 rounded-lg border transition-all ${
isSelected
? 'border-indigo-500 bg-indigo-50 ring-1 ring-indigo-500'
: 'border-slate-200 hover:border-slate-300 hover:bg-slate-50'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isActive && (
<CheckCircle2 className="h-4 w-4 text-green-500" />
)}
<span className={`font-medium text-sm ${isSelected ? 'text-indigo-900' : 'text-slate-900'}`}>
{plan.title}
</span>
</div>
<Badge
style={{
backgroundColor: statusInfo.color,
color: 'white',
}}
className="text-xs"
>
{statusInfo.label}
</Badge>
</div>
<div className="flex items-center gap-2 mt-1 text-xs text-slate-500">
{plan.version && <span>v{plan.version}</span>}
{plan.created_at && (
<>
<span></span>
<span>
{new Date(plan.created_at).toLocaleDateString('nl-NL', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</span>
</>
)}
{plan.published_at && (
<>
<span></span>
<span className="text-green-600">Gepubliceerd</span>
</>
)}
</div>
</button>
);
})}
</div>
)}
</CardContent>
</Card>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<void>;
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 (
<Card className={className}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base flex items-center gap-2">
{icon}
{title}
</CardTitle>
{description && <CardDescription>{description}</CardDescription>}
</div>
{canEdit && !isEditing && (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => setIsEditing(true)}
>
<Pencil className="h-4 w-4" />
</Button>
)}
</div>
</CardHeader>
<CardContent>
{isEditing && editForm ? (
<div className="space-y-4">
{editForm}
<div className="flex justify-end gap-2 pt-2 border-t">
<Button
variant="outline"
size="sm"
onClick={handleCancel}
disabled={isSaving}
>
<X className="h-4 w-4 mr-1" />
Annuleren
</Button>
<Button
size="sm"
onClick={handleSave}
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Check className="h-4 w-4 mr-1" />
)}
Opslaan
</Button>
</div>
</div>
) : (
children
)}
</CardContent>
</Card>
);
}
// Simple inline edit buttons for list items
interface ItemActionsProps {
onEdit?: () => void;
onDelete?: () => void;
isDeleting?: boolean;
}
export function ItemActions({ onEdit, onDelete, isDeleting }: ItemActionsProps) {
return (
<div className="flex items-center gap-1">
{onEdit && (
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
>
<Pencil className="h-3.5 w-3.5" />
</Button>
)}
{onDelete && (
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
disabled={isDeleting}
>
{isDeleting ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<X className="h-3.5 w-3.5" />
)}
</Button>
)}
</div>
);
}

View File

@@ -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';

View File

@@ -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<Behandelstructuur>(
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 (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="duur">Duur</Label>
<Select value={data.duur} onValueChange={(v) => handleChange('duur', v)}>
<SelectTrigger>
<SelectValue placeholder="Selecteer duur" />
</SelectTrigger>
<SelectContent>
{DUUR_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="frequentie">Frequentie</Label>
<Select value={data.frequentie} onValueChange={(v) => handleChange('frequentie', v)}>
<SelectTrigger>
<SelectValue placeholder="Selecteer frequentie" />
</SelectTrigger>
<SelectContent>
{FREQUENTIE_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="sessies">Aantal sessies</Label>
<Input
id="sessies"
type="number"
min={1}
max={52}
value={data.aantalSessies}
onChange={(e) => handleChange('aantalSessies', parseInt(e.target.value) || 1)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="vorm">Vorm</Label>
<Select value={data.vorm} onValueChange={(v) => handleChange('vorm', v)}>
<SelectTrigger>
<SelectValue placeholder="Selecteer vorm" />
</SelectTrigger>
<SelectContent>
{VORM_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
}

View File

@@ -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<SmartGoal>(
initialData || {
id: crypto.randomUUID(),
title: '',
description: '',
clientVersion: '',
lifeDomain: 'dlv',
priority: 'middel',
measurability: '',
timelineWeeks: 8,
status: 'niet_gestart',
progress: 0,
}
);
const handleChange = <K extends keyof SmartGoal>(field: K, value: SmartGoal[K]) => {
const updated = { ...data, [field]: value };
setData(updated);
onChange(updated);
};
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="title">Titel</Label>
<Input
id="title"
value={data.title}
onChange={(e) => handleChange('title', e.target.value)}
placeholder="Korte beschrijving van het doel"
/>
</div>
<div className="space-y-2">
<Label htmlFor="lifeDomain">Leefgebied</Label>
<Select
value={data.lifeDomain}
onValueChange={(v) => handleChange('lifeDomain', v as LifeDomain)}
>
<SelectTrigger>
<SelectValue placeholder="Selecteer leefgebied" />
</SelectTrigger>
<SelectContent>
{LIFE_DOMAINS.map((domain) => (
<SelectItem key={domain} value={domain}>
{LIFE_DOMAIN_META[domain].emoji} {LIFE_DOMAIN_META[domain].label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">SMART Beschrijving</Label>
<Textarea
id="description"
value={data.description}
onChange={(e) => handleChange('description', e.target.value)}
placeholder="Specifiek, Meetbaar, Acceptabel, Realistisch, Tijdgebonden"
rows={2}
/>
</div>
<div className="space-y-2">
<Label htmlFor="clientVersion">Cliënt versie (B1-taal)</Label>
<Textarea
id="clientVersion"
value={data.clientVersion}
onChange={(e) => handleChange('clientVersion', e.target.value)}
placeholder="Eenvoudige uitleg voor de cliënt"
rows={2}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="priority">Prioriteit</Label>
<Select
value={data.priority}
onValueChange={(v) => handleChange('priority', v as 'hoog' | 'middel' | 'laag')}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PRIORITY_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
value={data.status}
onValueChange={(v) => handleChange('status', v as GoalStatus)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="timelineWeeks">Tijdlijn (weken)</Label>
<Input
id="timelineWeeks"
type="number"
min={1}
max={52}
value={data.timelineWeeks}
onChange={(e) => handleChange('timelineWeeks', parseInt(e.target.value) || 1)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="measurability">Meetbaarheid</Label>
<Input
id="measurability"
value={data.measurability}
onChange={(e) => handleChange('measurability', e.target.value)}
placeholder="Hoe meten we vooruitgang?"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label>Voortgang</Label>
<span className="text-sm text-slate-500">{data.progress}%</span>
</div>
<Slider
value={[data.progress]}
onValueChange={(v) => handleChange('progress', v[0])}
max={100}
step={5}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,115 @@
'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 type { Intervention, SmartGoal } from '@/lib/types/behandelplan';
import { Checkbox } from '@/components/ui/checkbox';
interface InterventionFormProps {
initialData?: Intervention | null;
goals?: SmartGoal[];
onChange: (data: Intervention) => void;
}
const COMMON_INTERVENTIONS = [
'CGT (Cognitieve Gedragstherapie)',
'EMDR',
'ACT (Acceptance and Commitment Therapy)',
'Schematherapie',
'Psycho-educatie',
'Mindfulness',
'Exposure therapie',
'Systeemtherapie',
];
export function InterventionForm({ initialData, goals = [], onChange }: InterventionFormProps) {
const [data, setData] = useState<Intervention>(
initialData || {
id: crypto.randomUUID(),
name: '',
description: '',
rationale: '',
linkedGoalIds: [],
}
);
const handleChange = <K extends keyof Intervention>(field: K, value: Intervention[K]) => {
const updated = { ...data, [field]: value };
setData(updated);
onChange(updated);
};
const toggleGoalLink = (goalId: string) => {
const linkedGoalIds = data.linkedGoalIds.includes(goalId)
? data.linkedGoalIds.filter((id) => id !== goalId)
: [...data.linkedGoalIds, goalId];
handleChange('linkedGoalIds', linkedGoalIds);
};
return (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Naam interventie</Label>
<Input
id="name"
value={data.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="bijv. CGT, EMDR, ACT"
list="interventions"
/>
<datalist id="interventions">
{COMMON_INTERVENTIONS.map((name) => (
<option key={name} value={name} />
))}
</datalist>
</div>
<div className="space-y-2">
<Label htmlFor="description">Beschrijving</Label>
<Textarea
id="description"
value={data.description}
onChange={(e) => handleChange('description', e.target.value)}
placeholder="Uitleg van de interventie"
rows={2}
/>
</div>
<div className="space-y-2">
<Label htmlFor="rationale">Rationale</Label>
<Textarea
id="rationale"
value={data.rationale}
onChange={(e) => handleChange('rationale', e.target.value)}
placeholder="Waarom past deze interventie bij deze cliënt?"
rows={2}
/>
</div>
{goals.length > 0 && (
<div className="space-y-2">
<Label>Gekoppelde doelen</Label>
<div className="space-y-2 p-3 bg-slate-50 rounded-lg">
{goals.map((goal) => (
<div key={goal.id} className="flex items-center space-x-2">
<Checkbox
id={`goal-${goal.id}`}
checked={data.linkedGoalIds.includes(goal.id)}
onCheckedChange={() => toggleGoalLink(goal.id)}
/>
<label
htmlFor={`goal-${goal.id}`}
className="text-sm cursor-pointer"
>
{goal.title || 'Doel zonder titel'}
</label>
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("grid place-content-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

22
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

159
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -100,12 +100,20 @@
"order": 7,
"status": "active"
},
{
"slug": "agenda",
"title": "Agenda Systeem",
"group": "features",
"description": "Behandelaar agenda met kalender, afspraken en verslaglegging integratie",
"order": 8,
"status": "completed"
},
{
"slug": "treatment-planning",
"title": "Behandelplanning",
"group": "features",
"description": "SMART doelen en voortgang monitoring",
"order": 8,
"order": 9,
"status": "planned"
},
{

View File

@@ -0,0 +1,234 @@
---
title: "Agenda Systeem"
category: "agenda"
group: "features"
version: "1.0.0"
releaseDate: "2025-12-05"
status: "completed"
description: "Behandelaar agenda met kalenderweergave, afspraken plannen, drag-and-drop en directe koppeling naar verslaglegging"
---
## Overzicht
De agenda module is het kloppend hart van het EPD. Hier beheer je alle afspraken met cliënten, plan je nieuwe sessies en start je direct met verslaglegging na een behandeling.
**Kernfunctionaliteit:**
- Kalenderweergave in dag-, week- of werkdagen-view
- Nieuwe afspraken plannen met cliënt zoeken
- Afspraken bewerken en verzetten via drag-and-drop
- Afspraken annuleren
- Conflictdetectie bij dubbele boekingen
- Directe koppeling naar verslaglegging
---
## Agenda Openen
Navigeer via de zijbalk naar **Agenda** om je persoonlijke agenda te openen. De agenda toont standaard de huidige week met al je geplande afspraken.
---
## Kalenderweergave
### Views
De agenda biedt drie weergaves:
| View | Beschrijving |
|------|--------------|
| **Dag** | Enkele dag met uren van 08:00-18:00 |
| **Week** | Maandag t/m zondag, 7 kolommen |
| **Werkdagen** | Maandag t/m vrijdag, 5 kolommen |
Wissel tussen views via de knoppen in de toolbar.
### Navigatie
- **Pijltjes** (◀ ▶): Ga naar vorige/volgende dag of week
- **Vandaag**: Spring direct naar de huidige datum
- **Mini-kalender**: Klik op een datum in de zijbalk voor snelle navigatie
### Visuele Elementen
- **Rode lijn**: Huidige tijd indicator
- **Gekleurde blokken**: Afspraken met kleurcodering per type
- **Vandaag highlight**: De huidige dag heeft een licht gekleurde achtergrond
- **Verslag-icoon**: Afspraken met een gekoppeld verslag tonen een document-icoon
---
## Afspraaktypen
Elk afspraaktype heeft een eigen kleur en standaardduur:
| Type | Kleur | Standaard duur |
|------|-------|----------------|
| **Intakegesprek** | Blauw | 60 minuten |
| **Behandelsessie** | Groen | 45 minuten |
| **Vervolggesprek** | Paars | 30 minuten |
| **Telefonisch contact** | Geel | 30 minuten |
| **Huisbezoek** | Oranje | 60 minuten |
| **Online consult** | Indigo | 45 minuten |
| **Crisiscontact** | Rood | 30 minuten |
| **Overig** | Grijs | 30 minuten |
---
## Nieuwe Afspraak Plannen
### Via Tijdslot Klikken
1. **Klik op een leeg tijdslot** in de kalender
2. Het afspraak formulier opent met datum en tijd vooringevuld
3. **Zoek de cliënt** via het zoekveld:
- Typ minimaal 2 karakters
- Zoek op naam, BSN of geboortedatum
- Selecteer de juiste cliënt uit de resultaten
4. **Selecteer het type** afspraak (intake, behandeling, etc.)
5. **Pas de duur aan** indien nodig (wordt automatisch ingevuld op basis van type)
6. Optioneel: voeg **notities** toe
7. Klik op **Opslaan**
### Via de Toolbar
1. Klik op **"+ Nieuwe afspraak"** in de toolbar
2. Selecteer datum en tijd handmatig
3. Volg verder dezelfde stappen als hierboven
### Slimme Defaults
Het systeem helpt je met intelligente voorinvullingen:
- **Type suggestie**: Nieuwe cliënt → Intake, Na intake → Behandeling, Na 3+ behandelingen → Follow-up
- **Duur**: Automatisch gebaseerd op afspraaktype
- **Recente cliënten**: Bij een leeg zoekveld verschijnen je laatst bezochte cliënten
---
## Afspraak Bewerken
### Details Bekijken
Klik op een afspraak in de kalender om de details te zien:
- Cliëntgegevens
- Datum en tijd
- Type afspraak
- Eventuele notities
- Beschikbare acties
### Wijzigen
1. Klik op de afspraak
2. Klik op **Bewerken**
3. Pas de gewenste velden aan
4. Klik op **Opslaan**
### Verzetten via Drag-and-Drop
1. Klik en houd de afspraak vast
2. Sleep naar het nieuwe tijdslot
3. Bevestig de wijziging in het dialoogvenster
4. De afspraak wordt automatisch bijgewerkt
---
## Afspraak Annuleren
1. Klik op de afspraak
2. Klik op **Annuleren** (rode tekst onderaan)
3. Bevestig de annulering
Geannuleerde afspraken blijven geregistreerd in het systeem maar worden niet meer getoond in de agenda.
---
## Conflictdetectie
Bij het plannen van een afspraak controleert het systeem automatisch op overlappende afspraken.
**Bij een conflict:**
1. Je ziet een waarschuwing met de details van de bestaande afspraak
2. Je kunt kiezen:
- **Wijzigen**: Terug naar het formulier om een andere tijd te kiezen
- **Toch inplannen**: De afspraak wordt alsnog aangemaakt
Het systeem blokkeert nooit een boeking - soms zijn dubbele boekingen bewust (bijv. korte telefoontjes tussendoor).
---
## Verslaglegging Starten
De agenda integreert naadloos met de verslaglegging module:
### Na een Afspraak
1. Klik op de afspraak in de agenda
2. Klik op **"Maak verslag"**
3. Je wordt doorgeleid naar de rapportage module met:
- Cliënt automatisch ingevuld
- Afspraak gekoppeld
- Type verslag voorgeselecteerd
4. Start met dicteren of typ je verslag
5. Laat eventueel AI het verslag genereren
### Verslag Indicator
Afspraken met een gekoppeld verslag tonen een document-icoon. Klik hierop om direct naar het verslag te gaan.
---
## Keyboard Shortcuts
Voor snelle navigatie zijn de volgende sneltoetsen beschikbaar:
| Toets | Actie |
|-------|-------|
| `N` | Nieuwe afspraak |
| `T` | Ga naar vandaag |
| `←` / `→` | Vorige/volgende periode |
| `1` / `2` / `3` | Dag / Week / Werkdagen view |
| `Esc` | Sluit modal of popup |
---
## Tips voor Gebruik
**Efficiënt plannen:**
- Gebruik de mini-kalender voor snelle navigatie naar specifieke weken
- Dubbelklik op een tijdslot om direct het formulier te openen
- Recente cliënten verschijnen automatisch bij een lege zoekopdracht
**Na een sessie:**
- Maak direct een verslag via de afspraak detail popup
- De koppeling tussen afspraak en verslag zorgt voor een compleet dossier
**Overzicht behouden:**
- Gebruik de kleurcodering om snel onderscheid te maken tussen afspraaktypen
- De werkdagen-view is ideaal voor een focus op de werkweek
---
## Locatie Types
Afspraken kunnen op verschillende locaties plaatsvinden:
| Code | Beschrijving |
|------|--------------|
| **AMB** | Praktijk (ambulant) |
| **VR** | Online / Virtueel |
| **HH** | Thuis (huisbezoek) |
---
## Gerelateerde Documentatie
**Workflow:**
- [Intake Systeem](/documentatie/intake-system) - Intake afspraken
- [Spraakgestuurde Verslaglegging](/documentatie/spraakgestuurde-verslaglegging) - Verslagen dicteren
**Cliëntbeheer:**
- [Client Management](/documentatie/client-management) - Cliëntdossiers
**Interface:**
- [Interface Design System](/documentatie/interface-design) - UI/UX specificaties

View File

@@ -1,6 +1,6 @@
{
"manifesto": {
"title": "Software on Demand - AI Speedrun Manifesto",
"title": "Het experiment: EPD bouwen in 4 weken",
"description": "Jensen Huang: 'AI is going to eat software'. Een experiment: bouw een EPD in 4 weken voor €200.",
"ogTitle": "Software on Demand - AI Speedrun",
"ogDescription": "Van €100k en 12 maanden naar €200 en 4 weken. Het nieuwe development.",

View File

@@ -110,7 +110,7 @@ Deze module is onderdeel van de AI Speedrun LinkedIn Serie. Het prototype demons
| E0 | Foundation | Types, database schema | ✅ Done | 3 | 2-3 uur |
| E1 | Leefgebieden | Intake formulier + score weergave | ✅ Done | 3 | 3-4 uur |
| E2 | AI Generatie | Claude API endpoint + prompts | ✅ Done | 3 | 3-4 uur |
| E3 | Behandelplan UI | Pagina + componenten | ⏳ To Do | 5 | 6-8 uur |
| E3 | Behandelplan UI | Pagina + componenten | Done | 5 | 6-8 uur |
| E4 | Stretch | Micro-regeneratie, radar chart | ⏳ Optioneel | 3 | 3-5 uur |
**Totaal MVP (E0-E3):** 14-19 uur
@@ -224,11 +224,11 @@ const settings = {
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|----------|--------------|---------------------|--------|------------------|----|
| E3.S1 | Behandelplan pagina | Placeholder vervangen, data loading, status weergave | | E2.S2 | 2 |
| E3.S2 | Generate button + flow | Button triggert AI, loading state, resultaat weergave | | E3.S1 | 2 |
| E3.S3 | SMART doelen sectie | Goal cards met progress, status, leefgebied badge | | E3.S1, E1.S3 | 3 |
| E3.S4 | Interventies sectie | Intervention cards met gekoppelde doelen | | E3.S3 | 2 |
| E3.S5 | Server actions | Create, update, delete, publish behandelplan | | E3.S1 | 2 |
| E3.S1 | Behandelplan pagina | Placeholder vervangen, data loading, status weergave | | E2.S2 | 2 |
| E3.S2 | Generate button + flow | Button triggert AI, loading state, resultaat weergave | | E3.S1 | 2 |
| E3.S3 | SMART doelen sectie | Goal cards met progress, status, leefgebied badge | | E3.S1, E1.S3 | 3 |
| E3.S4 | Interventies sectie | Intervention cards met gekoppelde doelen | | E3.S3 | 2 |
| E3.S5 | Server actions | Create, update, delete, publish behandelplan | | E3.S1 | 2 |
**Technical Notes:**
```
@@ -244,13 +244,10 @@ const settings = {
```
**Deliverables:**
- [ ] `app/epd/patients/[id]/behandelplan/page.tsx` (vervangen)
- [ ] `app/epd/patients/[id]/behandelplan/actions.ts`
- [ ] `components/behandelplan/behandelplan-view.tsx`
- [ ] `components/behandelplan/generate-button.tsx`
- [ ] `components/behandelplan/goals-section.tsx`
- [ ] `components/behandelplan/goal-card.tsx`
- [ ] `components/behandelplan/interventions-section.tsx`
- [x] `app/epd/patients/[id]/behandelplan/page.tsx` (vervangen)
- [x] `app/epd/patients/[id]/behandelplan/page-client.tsx` (nieuw - client wrapper)
- [x] `app/epd/patients/[id]/behandelplan/actions.ts`
- [x] `components/behandelplan/behandelplan-view.tsx` (bevat generate button, goals section, interventions inline)
---
@@ -438,3 +435,4 @@ Optioneel (indien tijd):
|--------|-------|--------|-----------|
| v1.0 | 03-12-2024 | Colin Lit | Initiële versie |
| v1.1 | 04-12-2024 | Colin Lit | Epic 0, 1, 2 afgerond - status bijgewerkt |
| v1.2 | 04-12-2024 | Colin Lit | Epic 3 afgerond - MVP compleet |

View File

@@ -349,7 +349,7 @@ export function canPublish(plan: GeneratedPlan): { valid: boolean; errors: strin
}
/**
* Status label voor UI
* Status label voor UI (oude Nederlandse keys - deprecated)
*/
export const PLAN_STATUS_LABELS: Record<PlanStatus, { label: string; color: string }> = {
concept: { label: 'Concept', color: '#60a5fa' }, // blauw
@@ -359,6 +359,22 @@ export const PLAN_STATUS_LABELS: Record<PlanStatus, { label: string; color: stri
gearchiveerd: { label: 'Gearchiveerd', color: '#9ca3af' }, // lichtgrijs
};
/**
* FHIR status naar Nederlandse UI labels
* Database gebruikt FHIR statussen, UI toont Nederlands
*/
export type FhirCarePlanStatus = 'draft' | 'active' | 'on-hold' | 'completed' | 'revoked' | 'entered-in-error' | 'unknown';
export const FHIR_STATUS_LABELS: Record<FhirCarePlanStatus, { label: string; color: string }> = {
draft: { label: 'Concept', color: '#60a5fa' }, // blauw
active: { label: 'Actueel', color: '#10b981' }, // groen
'on-hold': { label: 'Gepauzeerd', color: '#f59e0b' }, // oranje
completed: { label: 'Definitief', color: '#6b7280' }, // grijs
revoked: { label: 'Archief', color: '#9ca3af' }, // lichtgrijs
'entered-in-error': { label: 'Verwijderd', color: '#ef4444' }, // rood
unknown: { label: 'Onbekend', color: '#9ca3af' }, // lichtgrijs
};
/**
* Goal status label voor UI
*/

View File

@@ -20,11 +20,13 @@
"@fullcalendar/timegrid": "^6.1.19",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",

93
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
'@radix-ui/react-alert-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-checkbox':
specifier: ^1.3.3
version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -47,6 +50,9 @@ importers:
'@radix-ui/react-progress':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-select':
specifier: ^2.2.6
version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slider':
specifier: ^1.3.6
version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -597,6 +603,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-checkbox@1.3.3':
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -838,6 +857,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-select@2.2.6':
resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slider@1.3.6':
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
peerDependencies:
@@ -4229,6 +4261,22 @@ snapshots:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
@@ -4461,6 +4509,35 @@ snapshots:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
aria-hidden: 1.2.6
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.7.1(@types/react@18.3.27)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/number': 1.1.1
@@ -5755,8 +5832,8 @@ snapshots:
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@@ -5775,7 +5852,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -5786,22 +5863,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -5812,7 +5889,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3