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:
520
app/epd/patients/[id]/behandelplan/actions.ts
Normal file
520
app/epd/patients/[id]/behandelplan/actions.ts
Normal 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`);
|
||||
}
|
||||
420
app/epd/patients/[id]/behandelplan/page-client.tsx
Normal file
420
app/epd/patients/[id]/behandelplan/page-client.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
125
components/behandelplan/behandelplan-list.tsx
Normal file
125
components/behandelplan/behandelplan-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1023
components/behandelplan/behandelplan-view.tsx
Normal file
1023
components/behandelplan/behandelplan-view.tsx
Normal file
File diff suppressed because it is too large
Load Diff
162
components/behandelplan/editable-section.tsx
Normal file
162
components/behandelplan/editable-section.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
97
components/behandelplan/sections/behandelstructuur-form.tsx
Normal file
97
components/behandelplan/sections/behandelstructuur-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
183
components/behandelplan/sections/goal-form.tsx
Normal file
183
components/behandelplan/sections/goal-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
115
components/behandelplan/sections/intervention-form.tsx
Normal file
115
components/behandelplan/sections/intervention-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
components/ui/checkbox.tsx
Normal file
30
components/ui/checkbox.tsx
Normal 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
22
components/ui/input.tsx
Normal 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
159
components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
234
content/nl/documentatie/agenda-systeem.mdx
Normal file
234
content/nl/documentatie/agenda-systeem.mdx
Normal 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
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
93
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user