diff --git a/app/epd/clients/[id]/components/intake-tab.tsx b/app/epd/clients/[id]/components/intake-tab.tsx index e470a9b..f01f5cd 100644 --- a/app/epd/clients/[id]/components/intake-tab.tsx +++ b/app/epd/clients/[id]/components/intake-tab.tsx @@ -1,99 +1,56 @@ 'use client'; -import { FileText, Plus, Calendar, Clock } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Plus } from 'lucide-react'; +import Link from 'next/link'; +import { getIntakesByClientId, Intake } from '../intakes/actions'; +import { IntakeList } from '../intakes/components/intake-list'; interface IntakeTabProps { clientId: string; } export function IntakeTab({ clientId }: IntakeTabProps) { + const [intakes, setIntakes] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + async function fetchIntakes() { + try { + const data = await getIntakesByClientId(clientId); + setIntakes(data); + } catch (error) { + console.error('Failed to fetch intakes:', error); + } finally { + setIsLoading(false); + } + } + + fetchIntakes(); + }, [clientId]); + return (
{/* Header */}

- Intake Notities + Intakes

- Gespreksverslagen en intake documenten + Overzicht van alle intakes

- + Nieuwe Intake +
- {/* Coming Soon State */} -
-
- -
-

- Coming Soon - Week 3 -

-

- De intake module met TipTap rich text editor en AI-samenvatting wordt - toegevoegd in Week 3 van de development sprint. -

- - {/* Feature Preview */} -
-
-

- 📋 Geplande Features: -

-
    -
  • - - - TipTap Rich Text Editor - Professionele - tekstverwerking - -
  • -
  • - - - AI Samenvatting - Claude 3.5 Sonnet - generatie (< 5 sec) - -
  • -
  • - - - B1 Readability - Automatische - tekstvereenvoudiging - -
  • -
  • - - - Tags & Categorieën - Intake, Evaluatie, Plan - -
  • -
  • - - - Versiehistorie - Track alle wijzigingen - -
  • -
-
-
- - {/* Timeline Preview */} -
- - - Week 3: 18-24 November 2024 - -
-
+
); } diff --git a/app/epd/clients/[id]/intakes/[intakeId]/components/intake-header.tsx b/app/epd/clients/[id]/intakes/[intakeId]/components/intake-header.tsx new file mode 100644 index 0000000..de2223d --- /dev/null +++ b/app/epd/clients/[id]/intakes/[intakeId]/components/intake-header.tsx @@ -0,0 +1,65 @@ +import { Intake } from '../../actions'; +import { format } from 'date-fns'; +import { nl } from 'date-fns/locale'; +import { Calendar, Clock, FileText } from 'lucide-react'; + +interface IntakeHeaderProps { + intake: Intake; +} + +export function IntakeHeader({ intake }: IntakeHeaderProps) { + const statusColors = { + Open: 'bg-blue-50 text-blue-700 border-blue-200', + Completed: 'bg-green-50 text-green-700 border-green-200', + Cancelled: 'bg-red-50 text-red-700 border-red-200', + Draft: 'bg-slate-50 text-slate-700 border-slate-200', + }; + + const status = intake.status || 'Draft'; + const statusClass = statusColors[status as keyof typeof statusColors] || statusColors.Draft; + + return ( +
+
+
+
+ +
+
+
+

{intake.title}

+ + {status} + +
+
+ {intake.department} + | +
+ + + Start: {format(new Date(intake.start_date), 'd MMM yyyy', { locale: nl })} + +
+ {intake.end_date && ( +
+ + + Eind: {format(new Date(intake.end_date), 'd MMM yyyy', { locale: nl })} + +
+ )} +
+
+
+ +
+ {/* Placeholder for actions like Edit, Close, etc. */} + +
+
+
+ ); +} diff --git a/app/epd/clients/[id]/intakes/[intakeId]/components/intake-tabs.tsx b/app/epd/clients/[id]/intakes/[intakeId]/components/intake-tabs.tsx new file mode 100644 index 0000000..73be98e --- /dev/null +++ b/app/epd/clients/[id]/intakes/[intakeId]/components/intake-tabs.tsx @@ -0,0 +1,52 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { cn } from '@/lib/utils'; + +interface IntakeTabsProps { + clientId: string; + intakeId: string; +} + +export function IntakeTabs({ clientId, intakeId }: IntakeTabsProps) { + const pathname = usePathname(); + const baseUrl = `/epd/clients/${clientId}/intakes/${intakeId}`; + + const tabs = [ + { name: 'Algemeen', href: baseUrl, exact: true }, + { name: 'Contactmomenten', href: `${baseUrl}/contacts` }, + { name: 'Kindcheck', href: `${baseUrl}/kindcheck` }, + { name: 'Risicotaxatie', href: `${baseUrl}/risk` }, + { name: 'Anamnese', href: `${baseUrl}/anamnese` }, + { name: 'Onderzoek', href: `${baseUrl}/examination` }, + { name: 'Diagnose & Advies', href: `${baseUrl}/diagnosis` }, + ]; + + return ( +
+ +
+ ); +} diff --git a/app/epd/clients/[id]/intakes/[intakeId]/layout.tsx b/app/epd/clients/[id]/intakes/[intakeId]/layout.tsx new file mode 100644 index 0000000..2b584a0 --- /dev/null +++ b/app/epd/clients/[id]/intakes/[intakeId]/layout.tsx @@ -0,0 +1,29 @@ +import { getIntakeById } from '../../actions'; +import { IntakeHeader } from './components/intake-header'; +import { IntakeTabs } from './components/intake-tabs'; +import { notFound } from 'next/navigation'; +import { ReactNode } from 'react'; + +interface IntakeLayoutProps { + children: ReactNode; + params: Promise<{ id: string; intakeId: string }>; +} + +export default async function IntakeLayout({ children, params }: IntakeLayoutProps) { + const { id, intakeId } = await params; + const intake = await getIntakeById(intakeId); + + if (!intake) { + notFound(); + } + + return ( +
+ + +
+ {children} +
+
+ ); +} diff --git a/app/epd/clients/[id]/intakes/[intakeId]/page.tsx b/app/epd/clients/[id]/intakes/[intakeId]/page.tsx new file mode 100644 index 0000000..0f4efef --- /dev/null +++ b/app/epd/clients/[id]/intakes/[intakeId]/page.tsx @@ -0,0 +1,52 @@ +import { getIntakeById } from '../actions'; +import { notFound } from 'next/navigation'; + +interface IntakePageProps { + params: Promise<{ intakeId: string }>; +} + +export default async function IntakePage({ params }: IntakePageProps) { + const { intakeId } = await params; + const intake = await getIntakeById(intakeId); + + if (!intake) { + notFound(); + } + + return ( +
+
+

Algemene Informatie

+ +
+
+ +

{intake.title}

+
+ +
+ +

{intake.department}

+
+ +
+ +

{intake.status}

+
+ +
+ +

{intake.start_date}

+
+
+ +
+ +
+ {intake.notes || 'Geen notities beschikbaar.'} +
+
+
+
+ ); +} diff --git a/app/epd/clients/[id]/intakes/actions.ts b/app/epd/clients/[id]/intakes/actions.ts new file mode 100644 index 0000000..6c48497 --- /dev/null +++ b/app/epd/clients/[id]/intakes/actions.ts @@ -0,0 +1,83 @@ +'use server'; + +import { createClient } from '@/lib/supabase/server'; +import { Database } from '@/lib/supabase/database.types'; +import { revalidatePath } from 'next/cache'; +import { redirect } from 'next/navigation'; +import { z } from 'zod'; + +export type Intake = Database['public']['Tables']['intakes']['Row']; + +const CreateIntakeSchema = z.object({ + title: z.string().min(1, 'Titel is verplicht'), + department: z.enum(['Volwassenen', 'Jeugd', 'Ouderen']), + start_date: z.string().min(1, 'Startdatum is verplicht'), + patient_id: z.string().uuid(), +}); + +export type CreateIntakeInput = z.infer; + +export async function getIntakesByClientId(clientId: string) { + const supabase = await createClient(); + + const { data, error } = await supabase + .from('intakes') + .select('*') + .eq('patient_id', clientId) + .order('start_date', { ascending: false }); + + if (error) { + console.error('Error fetching intakes:', error); + throw new Error('Failed to fetch intakes'); + } + + return data as Intake[]; +} + +export async function createIntake(input: CreateIntakeInput) { + const result = CreateIntakeSchema.safeParse(input); + + if (!result.success) { + throw new Error('Invalid input data'); + } + + const { title, department, start_date, patient_id } = result.data; + const supabase = await createClient(); + + const { data, error } = await supabase + .from('intakes') + .insert({ + title, + department, + start_date, + patient_id, + status: 'Open', // Default status + }) + .select() + .single(); + + if (error) { + console.error('Error creating intake:', error); + throw new Error('Failed to create intake'); + } + + revalidatePath(`/epd/clients/${patient_id}`); + redirect(`/epd/clients/${patient_id}?tab=intake`); +} + +export async function getIntakeById(intakeId: string) { + const supabase = await createClient(); + + const { data, error } = await supabase + .from('intakes') + .select('*') + .eq('id', intakeId) + .single(); + + if (error) { + console.error('Error fetching intake:', error); + return null; + } + + return data as Intake; +} diff --git a/app/epd/clients/[id]/intakes/components/intake-card.tsx b/app/epd/clients/[id]/intakes/components/intake-card.tsx new file mode 100644 index 0000000..5dcae83 --- /dev/null +++ b/app/epd/clients/[id]/intakes/components/intake-card.tsx @@ -0,0 +1,70 @@ +import { Calendar, ChevronRight, FileText } from 'lucide-react'; +import { Intake } from '../actions'; +import Link from 'next/link'; +import { format } from 'date-fns'; +import { nl } from 'date-fns/locale'; + +interface IntakeCardProps { + intake: Intake; + clientId: string; +} + +export function IntakeCard({ intake, clientId }: IntakeCardProps) { + const statusColors = { + Open: 'bg-blue-50 text-blue-700 border-blue-200', + Completed: 'bg-green-50 text-green-700 border-green-200', + Cancelled: 'bg-red-50 text-red-700 border-red-200', + Draft: 'bg-slate-50 text-slate-700 border-slate-200', + }; + + const status = intake.status || 'Draft'; + const statusClass = statusColors[status as keyof typeof statusColors] || statusColors.Draft; + + return ( + +
+
+
+
+ +
+
+

+ {intake.title} +

+

{intake.department}

+
+
+ + {status} + +
+ +
+
+ + + {format(new Date(intake.start_date), 'd MMM yyyy', { locale: nl })} + +
+ {intake.end_date && ( + <> + + + {format(new Date(intake.end_date), 'd MMM yyyy', { locale: nl })} + + + )} +
+ +
+
+
+ + ); +} diff --git a/app/epd/clients/[id]/intakes/components/intake-list.tsx b/app/epd/clients/[id]/intakes/components/intake-list.tsx new file mode 100644 index 0000000..df62948 --- /dev/null +++ b/app/epd/clients/[id]/intakes/components/intake-list.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { Intake } from '../actions'; +import { IntakeCard } from './intake-card'; +import { FileText } from 'lucide-react'; + +interface IntakeListProps { + intakes: Intake[]; + clientId: string; + isLoading?: boolean; +} + +export function IntakeList({ intakes, clientId, isLoading }: IntakeListProps) { + if (isLoading) { + return ( +
+ {[1, 2].map((i) => ( +
+ ))} +
+ ); + } + + if (intakes.length === 0) { + return ( +
+
+ +
+

+ Geen intakes gevonden +

+

+ Start een nieuwe intake om te beginnen. +

+
+ ); + } + + return ( +
+ {intakes.map((intake) => ( + + ))} +
+ ); +} diff --git a/app/epd/clients/[id]/intakes/components/new-intake-form.tsx b/app/epd/clients/[id]/intakes/components/new-intake-form.tsx new file mode 100644 index 0000000..0a25406 --- /dev/null +++ b/app/epd/clients/[id]/intakes/components/new-intake-form.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { createIntake } from '../actions'; +import { useState, useTransition } from 'react'; +import { CalendarIcon, Loader2 } from 'lucide-react'; +import { format } from 'date-fns'; +import { nl } from 'date-fns/locale'; +import { cn } from '@/lib/utils'; + +const formSchema = z.object({ + title: z.string().min(1, 'Titel is verplicht'), + department: z.enum(['Volwassenen', 'Jeugd', 'Ouderen']), + start_date: z.string().min(1, 'Startdatum is verplicht'), +}); + +type FormData = z.infer; + +interface NewIntakeFormProps { + clientId: string; +} + +export function NewIntakeForm({ clientId }: NewIntakeFormProps) { + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + department: 'Volwassenen', + start_date: format(new Date(), 'yyyy-MM-dd'), + }, + }); + + const onSubmit = (data: FormData) => { + setError(null); + startTransition(async () => { + try { + await createIntake({ + ...data, + patient_id: clientId, + }); + } catch (e) { + setError('Er is een fout opgetreden bij het aanmaken van de intake.'); + console.error(e); + } + }); + }; + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ + + {errors.title && ( +

{errors.title.message}

+ )} +
+ +
+ + + {errors.department && ( +

{errors.department.message}

+ )} +
+ +
+ +
+ +
+ {errors.start_date && ( +

{errors.start_date.message}

+ )} +
+ +
+ +
+
+ ); +} diff --git a/app/epd/clients/[id]/intakes/new/page.tsx b/app/epd/clients/[id]/intakes/new/page.tsx new file mode 100644 index 0000000..abfd285 --- /dev/null +++ b/app/epd/clients/[id]/intakes/new/page.tsx @@ -0,0 +1,33 @@ +import { NewIntakeForm } from '../../components/new-intake-form'; +import Link from 'next/link'; +import { ChevronLeft } from 'lucide-react'; + +interface NewIntakePageProps { + params: Promise<{ id: string }>; +} + +export default async function NewIntakePage({ params }: NewIntakePageProps) { + const { id } = await params; + + return ( +
+
+ + + Terug naar overzicht + +

Nieuwe Intake Starten

+

+ Vul de basisgegevens in om een nieuwe intake te starten. +

+
+ +
+ +
+
+ ); +} diff --git a/app/epd/patients/[id]/components/client-header.tsx b/app/epd/patients/[id]/components/client-header.tsx new file mode 100644 index 0000000..84ac1f7 --- /dev/null +++ b/app/epd/patients/[id]/components/client-header.tsx @@ -0,0 +1,107 @@ +'use client'; + +/** + * Client Header Component + * E2.S3: Context-aware header showing client name, status, and last modified + */ + +import type { FHIRPatient } from '@/lib/fhir'; + +interface ClientHeaderProps { + patient: FHIRPatient; +} + +// Status badge component +function StatusBadge({ status }: { status?: string }) { + const badges = { + planned: { label: 'Screening', className: 'bg-amber-100 text-amber-800 border-amber-200' }, + active: { label: 'Actief', className: 'bg-emerald-100 text-emerald-800 border-emerald-200' }, + finished: { label: 'Afgerond', className: 'bg-slate-100 text-slate-800 border-slate-200' }, + cancelled: { label: 'Afgemeld', className: 'bg-red-100 text-red-800 border-red-200' }, + }; + + const badge = status && status in badges ? badges[status as keyof typeof badges] : null; + + if (!badge) { + return -; + } + + return ( + + {badge.label} + + ); +} + +export function ClientHeader({ patient }: ClientHeaderProps) { + // Extract name + const name = patient.name?.[0]; + const fullName = [ + ...(name?.prefix || []), + ...(name?.given || []), + name?.family, + ] + .filter(Boolean) + .join(' '); + + // Extract status from extension + const statusExtension = (patient as any).extension?.find( + (ext: any) => ext.url === 'http://mini-epd.local/fhir/StructureDefinition/episode-status' + ); + const status = statusExtension?.valueCode; + + // Extract last modified + const lastModified = patient.meta?.lastUpdated + ? new Date(patient.meta.lastUpdated).toLocaleString('nl-NL', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + : null; + + // Check if John Doe + const isJohnDoe = (patient as any).extension?.find( + (ext: any) => ext.url === 'http://mini-epd.local/fhir/StructureDefinition/john-doe' + )?.valueBoolean; + + return ( +
+
+
+ {/* Name and John Doe indicator */} +
+

{fullName}

+ {isJohnDoe && ( + + John Doe + + )} +
+ + {/* Status and Last Modified */} +
+
+ Status: + +
+ {lastModified && ( +
+ Laatst gewijzigd: + {lastModified} +
+ )} +
+
+ + {/* Patient ID (subtle) */} +
+ ID: {patient.id} +
+
+
+ ); +} diff --git a/app/epd/patients/[id]/components/client-sidebar.tsx b/app/epd/patients/[id]/components/client-sidebar.tsx new file mode 100644 index 0000000..fffbfb1 --- /dev/null +++ b/app/epd/patients/[id]/components/client-sidebar.tsx @@ -0,0 +1,113 @@ +'use client'; + +/** + * Client Sidebar Navigation Component + * E2.S3: Context-aware sidebar with tabs for client dossier + */ + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { + ChevronLeft, + LayoutDashboard, + User, + ClipboardList, + FileText, + Stethoscope, + Calendar, + FileBarChart, +} from 'lucide-react'; + +interface ClientSidebarProps { + patientId: string; +} + +interface NavItem { + label: string; + href: string; + icon: React.ElementType; +} + +export function ClientSidebar({ patientId }: ClientSidebarProps) { + const pathname = usePathname(); + + // Navigation items + const navItems: NavItem[] = [ + { + label: 'Dashboard', + href: `/epd/patients/${patientId}`, + icon: LayoutDashboard, + }, + { + label: 'Basisgegevens', + href: `/epd/patients/${patientId}/basisgegevens`, + icon: User, + }, + { + label: 'Screening', + href: `/epd/patients/${patientId}/screening`, + icon: ClipboardList, + }, + { + label: 'Intake', + href: `/epd/patients/${patientId}/intake`, + icon: FileText, + }, + { + label: 'Diagnose', + href: `/epd/patients/${patientId}/diagnose`, + icon: Stethoscope, + }, + { + label: 'Behandelplan', + href: `/epd/patients/${patientId}/behandelplan`, + icon: Calendar, + }, + { + label: 'Rapportage', + href: `/epd/patients/${patientId}/rapportage`, + icon: FileBarChart, + }, + ]; + + return ( + + ); +} diff --git a/app/epd/patients/[id]/layout.tsx b/app/epd/patients/[id]/layout.tsx new file mode 100644 index 0000000..091cdf4 --- /dev/null +++ b/app/epd/patients/[id]/layout.tsx @@ -0,0 +1,32 @@ +import { getPatient } from '../actions'; +import { ClientHeader } from './components/client-header'; +import { ClientSidebar } from './components/client-sidebar'; + +export default async function PatientDetailLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const patient = await getPatient(id); + + return ( +
+ {/* Client Header */} + + + {/* Main Content Area with Sidebar */} +
+ {/* Sidebar Navigation */} + + + {/* Page Content */} +
+ {children} +
+
+
+ ); +} diff --git a/app/epd/patients/[id]/page.tsx b/app/epd/patients/[id]/page.tsx index d01a703..8f25965 100644 --- a/app/epd/patients/[id]/page.tsx +++ b/app/epd/patients/[id]/page.tsx @@ -1,45 +1,108 @@ -import { getPatient } from '../actions'; -import { PatientForm } from '../components/patient-form'; -import Link from 'next/link'; -import { ChevronLeft } from 'lucide-react'; +/** + * Patient Dashboard Page + * E2.S3: Default view showing patient overview and status + */ -export default async function PatientDetailPage({ +import Link from 'next/link'; +import { + User, + ClipboardList, + FileText, + ArrowRight, + AlertCircle, +} from 'lucide-react'; + +export default async function PatientDashboardPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; - const patient = await getPatient(id); - - const name = patient.name?.[0]; - const fullName = [ - ...(name?.prefix || []), - ...(name?.given || []), - name?.family, - ] - .filter(Boolean) - .join(' '); return ( -
- {/* Header with back button */} -
- - - Terug naar patiënten - -

Patiënt bewerken

+
+ {/* Page Header */} +
+

Dashboard

- {fullName} - ID: {patient.id} + Overzicht van cliëntgegevens en voortgang

- {/* Patient Form */} -
- + {/* Quick Actions Grid */} +
+ {/* Basisgegevens Card */} + +
+
+
+ +
+
+

Basisgegevens

+

NAW & contactgegevens

+
+
+ +
+ + + {/* Screening Card */} + +
+
+
+ +
+
+

Screening

+

Activiteiten & besluit

+
+
+ +
+ + + {/* Intake Card */} + +
+
+
+ +
+
+

Intake

+

Gesprekken & registraties

+
+
+ +
+ +
+ + {/* Next Steps Section */} +
+
+ +
+

Volgende stappen

+
    +
  • • Controleer en vul basisgegevens aan indien nodig
  • +
  • • Start screening door activiteiten te loggen
  • +
  • • Upload relevante documenten (verwijsbrief, etc.)
  • +
  • • Neem screeningsbesluit om door te gaan naar intake
  • +
+
+
); diff --git a/docs/specs/screening-intake/bouwplan-screening-intake-v1.0.md b/docs/specs/screening-intake/bouwplan-screening-intake-v1.0.md index 9f0f8b8..e48abb9 100644 --- a/docs/specs/screening-intake/bouwplan-screening-intake-v1.0.md +++ b/docs/specs/screening-intake/bouwplan-screening-intake-v1.0.md @@ -44,7 +44,7 @@ | E1 | Database & Types | Datamodel implementeren in Supabase | ✅ Done | 3 | | E2 | Cliëntenbeheer | Lijstweergave en aanmaken cliënten | 🔨 In Progress | 3 | | E3 | Screening Module | Screening tab en functionaliteit | ⏳ To Do | 4 | -| E4 | Intake Core | Intake overzicht en navigatie | ⏳ To Do | 3 | +| E4 | Intake Core | Intake overzicht en navigatie | ✅ Done | 3 | | E5 | Intake Details | Specifieke tabbladen (Contact, Risico, etc.) | ⏳ To Do | 5 | | E6 | Diagnose & Advies | Diagnose stelling en behandeladvies | ⏳ To Do | 3 | @@ -82,14 +82,15 @@ | E3.S3 | Screeningsbesluit | Formulier voor besluit (geschikt/niet geschikt) + status update logica. | | E3.S4 | Basisgegevens Tab | Read-only weergave met edit-modus voor NAW gegevens. | -### Epic 4 — Intake Core (Level 2) +### Epic 4 — Intake Core (Level 2) ✅ **Doel:** Beheer van intakes (meerdere per cliënt mogelijk). +**Status:** Done - Alle stories voltooid op 22-11-2025 -| Story ID | Beschrijving | Acceptatiecriteria | -|----------|--------------|---------------------| -| E4.S1 | Intake Overzicht | Kaartweergave van alle intakes per cliënt. | -| E4.S2 | Nieuwe Intake | Modal/page voor starten nieuwe intake. | -| E4.S3 | Intake Layout | Sub-navigatie (tabs) binnen een specifieke intake. | +| Story ID | Status | Beschrijving | Acceptatiecriteria | +|----------|--------|--------------|---------------------| +| E4.S1 | ✅ Done | Intake Overzicht | Kaartweergave van alle intakes per cliënt:
- `IntakeCard` component met status badges
- `IntakeList` component voor grid weergave
- Server Action `getIntakesByClientId` voor data fetching
- Geïntegreerd in `IntakeTab` op cliënt detail pagina | +| E4.S2 | ✅ Done | Nieuwe Intake | Modal/page voor starten nieuwe intake:
- `NewIntakeForm` met Zod validatie
- Velden: Titel, Afdeling, Startdatum
- Server Action `createIntake` voor aanmaken record
- Redirect naar intake lijst na succes | +| E4.S3 | ✅ Done | Intake Layout | Sub-navigatie (tabs) binnen een specifieke intake:
- `IntakeLayout` met `IntakeHeader` en `IntakeTabs`
- Header toont titel, status, en datums
- Tabs voor navigatie naar sub-onderdelen (Algemeen, Contact, etc.)
- Server Action `getIntakeById` voor ophalen details | ### Epic 5 — Intake Details (Tabs) **Doel:** Inhoudelijke registratie van de intake. diff --git a/lib/supabase/migrations/20251122-current-db-scheme.sql b/lib/supabase/migrations/20251122-current-db-scheme.sql new file mode 100644 index 0000000..7803216 --- /dev/null +++ b/lib/supabase/migrations/20251122-current-db-scheme.sql @@ -0,0 +1,266 @@ +-- WARNING: This schema is for context only and is not meant to be run. +-- Table order and constraints may not be valid for execution. + +CREATE TABLE public.ai_events ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + kind text NOT NULL CHECK (kind = ANY (ARRAY['summarize'::text, 'readability'::text, 'extract'::text, 'plan'::text])), + client_id uuid, + note_id uuid, + request jsonb NOT NULL DEFAULT '{}'::jsonb, + response jsonb NOT NULL DEFAULT '{}'::jsonb, + duration_ms integer NOT NULL DEFAULT 0 CHECK (duration_ms >= 0), + created_at timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT ai_events_pkey PRIMARY KEY (id), + CONSTRAINT ai_events_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id), + CONSTRAINT ai_events_note_id_fkey FOREIGN KEY (note_id) REFERENCES public.intake_notes(id) +); +CREATE TABLE public.care_plans ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + identifier text DEFAULT (gen_random_uuid())::text UNIQUE, + status USER-DEFINED NOT NULL DEFAULT 'draft'::careplan_status, + intent text NOT NULL DEFAULT 'plan'::text, + category_code text DEFAULT 'ggz-behandelplan'::text, + category_display text DEFAULT 'GGZ Behandelplan'::text, + title text NOT NULL, + description text, + patient_id uuid NOT NULL, + encounter_id uuid, + period_start date, + period_end date, + created_date timestamp with time zone DEFAULT now(), + author_id uuid, + contributor_ids ARRAY, + care_team_ids ARRAY, + addresses_condition_ids ARRAY, + goals jsonb DEFAULT '[]'::jsonb, + activities jsonb DEFAULT '[]'::jsonb, + note text, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT care_plans_pkey PRIMARY KEY (id), + CONSTRAINT care_plans_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id), + CONSTRAINT care_plans_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id), + CONSTRAINT care_plans_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.practitioners(id) +); +CREATE TABLE public.clients ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + first_name text NOT NULL, + last_name text NOT NULL, + birth_date date NOT NULL, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT clients_pkey PRIMARY KEY (id) +); +CREATE TABLE public.conditions ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + identifier text DEFAULT (gen_random_uuid())::text UNIQUE, + clinical_status USER-DEFINED NOT NULL DEFAULT 'active'::condition_clinical_status, + verification_status USER-DEFINED NOT NULL DEFAULT 'provisional'::condition_verification_status, + category text NOT NULL DEFAULT 'encounter-diagnosis'::text, + severity_code text, + severity_display text, + code_system text NOT NULL DEFAULT 'http://hl7.org/fhir/sid/icd-10'::text, + code_code text NOT NULL, + code_display text NOT NULL, + body_site_code text, + body_site_display text, + patient_id uuid NOT NULL, + encounter_id uuid, + onset_datetime timestamp with time zone, + onset_age integer, + abatement_datetime timestamp with time zone, + abatement_age integer, + recorded_date timestamp with time zone NOT NULL DEFAULT now(), + recorder_id uuid, + asserter_id uuid, + note text, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT conditions_pkey PRIMARY KEY (id), + CONSTRAINT conditions_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id), + CONSTRAINT conditions_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id), + CONSTRAINT conditions_recorder_id_fkey FOREIGN KEY (recorder_id) REFERENCES public.practitioners(id), + CONSTRAINT conditions_asserter_id_fkey FOREIGN KEY (asserter_id) REFERENCES public.practitioners(id) +); +CREATE TABLE public.demo_users ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + user_id uuid UNIQUE, + access_level text NOT NULL DEFAULT 'read_only'::text CHECK (access_level = ANY (ARRAY['read_only'::text, 'interactive'::text, 'presenter'::text])), + expires_at timestamp with time zone DEFAULT (now() + '90 days'::interval), + usage_count integer DEFAULT 0, + last_login_at timestamp with time zone, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now(), + notes text, + CONSTRAINT demo_users_pkey PRIMARY KEY (id), + CONSTRAINT demo_users_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) +); +CREATE TABLE public.encounters ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + identifier text DEFAULT (gen_random_uuid())::text UNIQUE, + status USER-DEFINED NOT NULL DEFAULT 'planned'::encounter_status, + class_code text NOT NULL, + class_display text NOT NULL, + type_code text NOT NULL, + type_display text NOT NULL, + priority_code text, + priority_display text, + patient_id uuid NOT NULL, + practitioner_id uuid, + organization_id uuid, + period_start timestamp with time zone NOT NULL, + period_end timestamp with time zone, + reason_code ARRAY, + reason_display ARRAY, + admission_source text, + discharge_disposition text, + notes text, + intake_note_id uuid, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT encounters_pkey PRIMARY KEY (id), + CONSTRAINT encounters_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id), + CONSTRAINT encounters_practitioner_id_fkey FOREIGN KEY (practitioner_id) REFERENCES public.practitioners(id), + CONSTRAINT encounters_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organizations(id), + CONSTRAINT encounters_intake_note_id_fkey FOREIGN KEY (intake_note_id) REFERENCES public.intake_notes(id) +); +CREATE TABLE public.intake_notes ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + client_id uuid NOT NULL, + title text, + tag text CHECK (tag = ANY (ARRAY['Intake'::text, 'Evaluatie'::text, 'Plan'::text])), + content_json jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (content_json IS NOT NULL), + content_text text, + author uuid, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT intake_notes_pkey PRIMARY KEY (id), + CONSTRAINT intake_notes_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id) +); +CREATE TABLE public.observations ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + identifier text DEFAULT (gen_random_uuid())::text UNIQUE, + status USER-DEFINED NOT NULL DEFAULT 'final'::observation_status, + category text NOT NULL, + code_system text NOT NULL, + code_code text NOT NULL, + code_display text NOT NULL, + patient_id uuid NOT NULL, + encounter_id uuid, + effective_datetime timestamp with time zone NOT NULL, + issued timestamp with time zone DEFAULT now(), + performer_id uuid, + value_type text NOT NULL, + value_quantity_value numeric, + value_quantity_unit text, + value_quantity_comparator text, + value_string text, + value_boolean boolean, + value_codeable_concept jsonb, + interpretation_code text, + interpretation_display text, + note text, + body_site text, + method_code text, + method_display text, + reference_range_low numeric, + reference_range_high numeric, + reference_range_text text, + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT observations_pkey PRIMARY KEY (id), + CONSTRAINT observations_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id), + CONSTRAINT observations_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id), + CONSTRAINT observations_performer_id_fkey FOREIGN KEY (performer_id) REFERENCES public.practitioners(id) +); +CREATE TABLE public.organizations ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + identifier_agb text UNIQUE, + identifier_kvk text, + name text NOT NULL, + alias ARRAY, + type_code text DEFAULT 'prov'::text, + type_display text DEFAULT 'Healthcare Provider'::text, + telecom_phone text, + telecom_email text, + telecom_website text, + address_line ARRAY, + address_city text, + address_postal_code text, + address_country text DEFAULT 'NL'::text, + active boolean DEFAULT true, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT organizations_pkey PRIMARY KEY (id) +); +CREATE TABLE public.patients ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + identifier_bsn text DEFAULT '999999990'::text, + identifier_client_number text, + name_family text NOT NULL, + name_given ARRAY NOT NULL, + name_prefix text, + name_use text DEFAULT 'official'::text, + birth_date date NOT NULL, + gender USER-DEFINED NOT NULL, + telecom_phone text, + telecom_email text, + address_line ARRAY, + address_city text, + address_postal_code text, + address_country text DEFAULT 'NL'::text, + insurance_company text, + insurance_number text, + emergency_contact_name text, + emergency_contact_relationship text, + emergency_contact_phone text, + active boolean DEFAULT true, + general_practitioner_name text, + general_practitioner_agb text, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT patients_pkey PRIMARY KEY (id) +); +CREATE TABLE public.practitioners ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + identifier_big text UNIQUE, + identifier_agb text, + name_prefix text, + name_given ARRAY NOT NULL, + name_family text NOT NULL, + name_suffix text, + qualification ARRAY, + telecom_phone text, + telecom_email text, + active boolean DEFAULT true, + user_id uuid, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT practitioners_pkey PRIMARY KEY (id), + CONSTRAINT practitioners_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) +); +CREATE TABLE public.problem_profiles ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + client_id uuid NOT NULL, + category text NOT NULL CHECK (category = ANY (ARRAY['stemming_depressie'::text, 'angst'::text, 'gedrag_impuls'::text, 'middelen_gebruik'::text, 'cognitief'::text, 'context_psychosociaal'::text])), + severity text NOT NULL CHECK (severity = ANY (ARRAY['laag'::text, 'middel'::text, 'hoog'::text])), + remarks text, + source_note_id uuid, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT problem_profiles_pkey PRIMARY KEY (id), + CONSTRAINT problem_profiles_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id), + CONSTRAINT problem_profiles_source_note_id_fkey FOREIGN KEY (source_note_id) REFERENCES public.intake_notes(id) +); +CREATE TABLE public.treatment_plans ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + client_id uuid NOT NULL, + version integer NOT NULL DEFAULT 1 CHECK (version > 0), + status text NOT NULL DEFAULT 'concept'::text CHECK (status = ANY (ARRAY['concept'::text, 'gepubliceerd'::text])), + plan jsonb NOT NULL DEFAULT '{"doelen": [], "frequentie": "", "interventies": [], "meetmomenten": []}'::jsonb CHECK (plan ? 'doelen'::text AND plan ? 'interventies'::text AND plan ? 'frequentie'::text AND plan ? 'meetmomenten'::text), + created_by uuid, + created_at timestamp with time zone NOT NULL DEFAULT now(), + published_at timestamp with time zone, + updated_at timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT treatment_plans_pkey PRIMARY KEY (id), + CONSTRAINT treatment_plans_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id) +); \ No newline at end of file