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
|
* 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({
|
export default async function BehandelplanPage({
|
||||||
params,
|
params,
|
||||||
@@ -12,29 +13,21 @@ export default async function BehandelplanPage({
|
|||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
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 (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* Page Header */}
|
<BehandelplanPageClient
|
||||||
<div className="mb-6">
|
patientId={id}
|
||||||
<h2 className="text-lg font-semibold text-slate-900">Behandelplan</h2>
|
allPlans={allPlans}
|
||||||
<p className="text-sm text-slate-600 mt-1">
|
intakes={intakes}
|
||||||
Behandeldoelen, interventies en planning
|
conditions={conditions}
|
||||||
</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>
|
|
||||||
</div>
|
</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,
|
LeefgebiedenScoreBar,
|
||||||
} from './leefgebieden-scores';
|
} from './leefgebieden-scores';
|
||||||
export { LeefgebiedenForm, LeefgebiedenQuickForm } from './leefgebieden-form';
|
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,
|
"order": 7,
|
||||||
"status": "active"
|
"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",
|
"slug": "treatment-planning",
|
||||||
"title": "Behandelplanning",
|
"title": "Behandelplanning",
|
||||||
"group": "features",
|
"group": "features",
|
||||||
"description": "SMART doelen en voortgang monitoring",
|
"description": "SMART doelen en voortgang monitoring",
|
||||||
"order": 8,
|
"order": 9,
|
||||||
"status": "planned"
|
"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": {
|
"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.",
|
"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",
|
"ogTitle": "Software on Demand - AI Speedrun",
|
||||||
"ogDescription": "Van €100k en 12 maanden naar €200 en 4 weken. Het nieuwe development.",
|
"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 |
|
| E0 | Foundation | Types, database schema | ✅ Done | 3 | 2-3 uur |
|
||||||
| E1 | Leefgebieden | Intake formulier + score weergave | ✅ Done | 3 | 3-4 uur |
|
| E1 | Leefgebieden | Intake formulier + score weergave | ✅ Done | 3 | 3-4 uur |
|
||||||
| E2 | AI Generatie | Claude API endpoint + prompts | ✅ 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 |
|
| E4 | Stretch | Micro-regeneratie, radar chart | ⏳ Optioneel | 3 | 3-5 uur |
|
||||||
|
|
||||||
**Totaal MVP (E0-E3):** 14-19 uur
|
**Totaal MVP (E0-E3):** 14-19 uur
|
||||||
@@ -224,11 +224,11 @@ const settings = {
|
|||||||
|
|
||||||
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|
||||||
|----------|--------------|---------------------|--------|------------------|----|
|
|----------|--------------|---------------------|--------|------------------|----|
|
||||||
| E3.S1 | Behandelplan pagina | Placeholder vervangen, data loading, status weergave | ⏳ | E2.S2 | 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.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.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.S4 | Interventies sectie | Intervention cards met gekoppelde doelen | ✅ | E3.S3 | 2 |
|
||||||
| E3.S5 | Server actions | Create, update, delete, publish behandelplan | ⏳ | E3.S1 | 2 |
|
| E3.S5 | Server actions | Create, update, delete, publish behandelplan | ✅ | E3.S1 | 2 |
|
||||||
|
|
||||||
**Technical Notes:**
|
**Technical Notes:**
|
||||||
```
|
```
|
||||||
@@ -244,13 +244,10 @@ const settings = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] `app/epd/patients/[id]/behandelplan/page.tsx` (vervangen)
|
- [x] `app/epd/patients/[id]/behandelplan/page.tsx` (vervangen)
|
||||||
- [ ] `app/epd/patients/[id]/behandelplan/actions.ts`
|
- [x] `app/epd/patients/[id]/behandelplan/page-client.tsx` (nieuw - client wrapper)
|
||||||
- [ ] `components/behandelplan/behandelplan-view.tsx`
|
- [x] `app/epd/patients/[id]/behandelplan/actions.ts`
|
||||||
- [ ] `components/behandelplan/generate-button.tsx`
|
- [x] `components/behandelplan/behandelplan-view.tsx` (bevat generate button, goals section, interventions inline)
|
||||||
- [ ] `components/behandelplan/goals-section.tsx`
|
|
||||||
- [ ] `components/behandelplan/goal-card.tsx`
|
|
||||||
- [ ] `components/behandelplan/interventions-section.tsx`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -438,3 +435,4 @@ Optioneel (indien tijd):
|
|||||||
|--------|-------|--------|-----------|
|
|--------|-------|--------|-----------|
|
||||||
| v1.0 | 03-12-2024 | Colin Lit | Initiële versie |
|
| 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.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 }> = {
|
export const PLAN_STATUS_LABELS: Record<PlanStatus, { label: string; color: string }> = {
|
||||||
concept: { label: 'Concept', color: '#60a5fa' }, // blauw
|
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
|
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
|
* Goal status label voor UI
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,11 +20,13 @@
|
|||||||
"@fullcalendar/timegrid": "^6.1.19",
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@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-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.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-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@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':
|
'@radix-ui/react-alert-dialog':
|
||||||
specifier: ^1.1.15
|
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)
|
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':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.15
|
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)
|
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':
|
'@radix-ui/react-progress':
|
||||||
specifier: ^1.1.8
|
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)
|
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':
|
'@radix-ui/react-slider':
|
||||||
specifier: ^1.3.6
|
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)
|
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':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-collection@1.1.7':
|
||||||
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -838,6 +857,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-slider@1.3.6':
|
||||||
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
|
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4229,6 +4261,22 @@ snapshots:
|
|||||||
'@types/react': 18.3.27
|
'@types/react': 18.3.27
|
||||||
'@types/react-dom': 18.3.7(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
|
'@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': 18.3.27
|
||||||
'@types/react-dom': 18.3.7(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/number': 1.1.1
|
'@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)
|
'@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-node: 0.3.9
|
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)
|
||||||
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)
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(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: 7.37.5(eslint@8.57.1)
|
||||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
||||||
@@ -5775,7 +5852,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -5786,22 +5863,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
|
'@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-node: 0.3.9
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -5812,7 +5889,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
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
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user