chore(strip): fase 0 — verwijder marketing, leads, archive en sitemap

Prototype-ballast verwijderd als eerste stap van de ECD-rebuild:
- app/(marketing) incl. blog, contact, documentatie + lib/mdx, lib/content, content/
- /api/leads en publieke marketing-routes uit middleware
- app/epd/_archive (backup van clients-module)
- sitemap.ts (verwees alleen naar blog), globals.css.backup
- root / redirect naar /login; robots.txt op disallow-all (afgeschermd systeem)

FHIR-routes blijven bewust staan: /api/fhir/Patient is de facto de
patienten-API voor dossier, agenda en Cortex — vervangen volgt in fase 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
colinislit
2026-07-14 22:21:20 +02:00
parent c0a4fa24f2
commit 10c994b252
109 changed files with 8 additions and 14533 deletions

View File

@@ -1,169 +0,0 @@
/**
* Blog Overview Page
*
* Landing page with series cards and recent posts
*/
import Link from 'next/link'
import { getAllPosts, getSeriesWithCounts, type BlogSeries } from '@/lib/mdx/blog'
export const metadata = {
title: 'Blog - AI Speedrun',
description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.',
openGraph: {
title: 'Blog - AI Speedrun',
description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.',
type: 'website',
siteName: 'AI Speedrun',
locale: 'nl_NL',
},
twitter: {
card: 'summary',
title: 'Blog - AI Speedrun',
description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.',
},
alternates: {
canonical: `${process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'}/blog`,
},
}
export default async function BlogPage() {
const series = await getSeriesWithCounts()
const posts = await getAllPosts()
return (
<div className="min-h-screen bg-slate-50 pb-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
<div className="mb-8 md:mb-12 pt-20 md:pt-24">
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-3 md:mb-4">
Blog
</h1>
<p className="text-lg md:text-xl text-slate-600 max-w-3xl leading-relaxed">
Artikelen over AI-gestuurde ontwikkeling en de reis van het bouwen van een EPD.
</p>
</div>
{/* Series Section */}
{series.length > 0 && (
<section className="mb-12">
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
Series
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{series.map((serie) => (
<SeriesCard key={serie.id} series={serie} />
))}
</div>
</section>
)}
{/* Recent Posts Section */}
<section>
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
Recente Posts
</h2>
{posts.length === 0 ? (
<p className="text-slate-500 text-lg text-center py-12">
Nog geen blogposts gepubliceerd.
</p>
) : (
<div className="space-y-4">
{posts.map((post) => (
<Link
key={`${post.seriesId}-${post.slug}`}
href={`/blog/serie/${post.seriesId}/${post.slug}`}
className="block bg-white rounded-lg border border-slate-200 p-5 md:p-6 hover:border-teal-300 hover:shadow-md transition-all active:scale-[0.99]"
>
<div className="flex flex-wrap items-center gap-2 mb-2 text-sm text-slate-500">
<span className="text-xs font-medium uppercase tracking-wide">
{post.seriesId.replace('-', ' ')}
</span>
<span></span>
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString('nl-NL', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<span></span>
<span>{post.readingTime} min leestijd</span>
</div>
<h3 className="text-xl md:text-2xl font-bold text-slate-900 mb-2 leading-tight">
{post.frontmatter.title}
</h3>
<p className="text-slate-600 leading-relaxed line-clamp-2">
{post.frontmatter.description}
</p>
{post.frontmatter.tags && post.frontmatter.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4">
{post.frontmatter.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 bg-slate-100 text-slate-600 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
)}
</Link>
))}
</div>
)}
</section>
</div>
</div>
)
}
function SeriesCard({ series }: { series: BlogSeries & { postCount: number } }) {
const colorStyles = {
teal: {
bg: 'bg-teal-50',
border: 'border-teal-200 hover:border-teal-400',
badge: 'bg-teal-100 text-teal-700',
dot: 'bg-teal-500',
},
amber: {
bg: 'bg-amber-50',
border: 'border-amber-200 hover:border-amber-400',
badge: 'bg-amber-100 text-amber-700',
dot: 'bg-amber-500',
},
slate: {
bg: 'bg-slate-50',
border: 'border-slate-200 hover:border-slate-400',
badge: 'bg-slate-100 text-slate-700',
dot: 'bg-slate-500',
},
}
const styles = colorStyles[series.color] || colorStyles.slate
const statusLabels = {
completed: 'Voltooid',
active: 'Actief',
planned: 'Gepland',
}
return (
<Link
href={`/blog/serie/${series.id}`}
className={`block rounded-lg border-2 p-5 transition-all hover:shadow-md active:scale-[0.98] ${styles.bg} ${styles.border}`}
>
<div className="flex items-center gap-2 mb-2">
<div className={`w-2 h-2 rounded-full ${styles.dot}`} />
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${styles.badge}`}>
{statusLabels[series.status]}
</span>
</div>
<h3 className="text-lg font-bold text-slate-900 mb-1">{series.title}</h3>
<p className="text-sm text-slate-600 mb-3 line-clamp-2">{series.description}</p>
<div className="text-xs text-slate-500">
{series.postCount} {series.postCount === 1 ? 'deel' : 'delen'}
</div>
</Link>
)
}

View File

@@ -1,324 +0,0 @@
/**
* Individual Blog Post Page
*
* Renders MDX content with series navigation
*/
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { getPost, getSeries, getPostsBySeries, getSeriesNavigation, getAllPosts } from '@/lib/mdx/blog'
import { mdxComponents } from '../../../../documentatie/components/mdx-components'
interface PostPageProps {
params: Promise<{ serieId: string; slug: string }>
}
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({
serieId: post.seriesId,
slug: post.slug,
}))
}
export async function generateMetadata({ params }: PostPageProps) {
const { serieId, slug } = await params
const post = await getPost(serieId, slug)
const series = await getSeries(serieId)
if (!post || !series) {
return { title: 'Post niet gevonden' }
}
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const postUrl = `${siteUrl}/blog/serie/${serieId}/${slug}`
// Use post-specific image if provided, otherwise fall back to default
const ogImageUrl = post.frontmatter.image
? `${siteUrl}${post.frontmatter.image.startsWith('/') ? '' : '/'}${post.frontmatter.image}`
: `${siteUrl}/og-blog-default.png`
return {
title: `${post.frontmatter.title} - ${series.title}`,
description: post.frontmatter.description,
authors: [{ name: 'Colin van der Heijden', url: 'https://ikbenlit.nl' }],
keywords: post.frontmatter.tags || [],
openGraph: {
title: post.frontmatter.title,
description: post.frontmatter.description,
type: 'article',
publishedTime: post.frontmatter.date,
authors: ['Colin van der Heijden'],
siteName: 'AI Speedrun',
locale: 'nl_NL',
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: post.frontmatter.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: post.frontmatter.title,
description: post.frontmatter.description,
images: [ogImageUrl],
},
alternates: {
canonical: postUrl,
},
}
}
export default async function PostPage({ params }: PostPageProps) {
const { serieId, slug } = await params
const post = await getPost(serieId, slug)
const series = await getSeries(serieId)
if (!post || !series) {
notFound()
}
const navigation = await getSeriesNavigation(serieId, slug)
const { frontmatter, content, readingTime } = post
// Generate Article structured data for SEO
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const postUrl = `${siteUrl}/blog/serie/${serieId}/${slug}`
// Use post-specific image if provided, otherwise fall back to default
const articleImage = frontmatter.image
? `${siteUrl}${frontmatter.image.startsWith('/') ? '' : '/'}${frontmatter.image}`
: `${siteUrl}/og-blog-default.png`
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: frontmatter.title,
description: frontmatter.description,
image: articleImage,
datePublished: frontmatter.date,
dateModified: frontmatter.date,
author: {
'@type': 'Person',
name: 'Colin van der Heijden',
url: 'https://ikbenlit.nl',
},
publisher: {
'@type': 'Organization',
name: 'AI Speedrun',
url: siteUrl,
logo: {
'@type': 'ImageObject',
url: `${siteUrl}/images/aispeedrun-logo.webp`,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': postUrl,
},
articleSection: series.title,
keywords: frontmatter.tags?.join(', ') || '',
wordCount: content.split(/\s+/).length,
timeRequired: `PT${readingTime}M`,
}
// Generate BreadcrumbList structured data
const breadcrumbSchema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: siteUrl,
},
{
'@type': 'ListItem',
position: 2,
name: 'Blog',
item: `${siteUrl}/blog`,
},
{
'@type': 'ListItem',
position: 3,
name: series.title,
item: `${siteUrl}/blog/serie/${serieId}`,
},
{
'@type': 'ListItem',
position: 4,
name: frontmatter.title,
item: postUrl,
},
],
}
const colorStyles = {
teal: {
badge: 'bg-teal-100 text-teal-700 border-teal-200',
link: 'text-teal-600 hover:text-teal-700',
},
amber: {
badge: 'bg-amber-100 text-amber-700 border-amber-200',
link: 'text-amber-600 hover:text-amber-700',
},
slate: {
badge: 'bg-slate-100 text-slate-700 border-slate-200',
link: 'text-slate-600 hover:text-slate-700',
},
}
const styles = colorStyles[series.color] || colorStyles.slate
return (
<div className="min-h-screen bg-white pb-16">
{/* Structured Data for SEO */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
<article className="max-w-3xl mx-auto px-4 md:px-8">
{/* Back link */}
<div className="pt-20 md:pt-24 mb-6">
<Link
href={`/blog/serie/${serieId}`}
className="inline-flex items-center gap-2 text-slate-600 hover:text-teal-600 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span>Terug naar {series.title}</span>
</Link>
</div>
{/* Header */}
<header className="mb-8 pb-8 border-b border-slate-200">
{/* Series badge */}
<div className="flex flex-wrap items-center gap-3 mb-4">
<Link
href={`/blog/serie/${serieId}`}
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border transition-colors hover:opacity-80 ${styles.badge}`}
>
{series.title}
</Link>
<span className="text-sm text-slate-500">
Deel {navigation.current} van {navigation.total}
</span>
</div>
{/* Title */}
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-4 leading-tight">
{frontmatter.title}
</h1>
{/* Description */}
<p className="text-xl text-slate-600 leading-relaxed mb-4">
{frontmatter.description}
</p>
{/* Meta */}
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500">
<time dateTime={frontmatter.date}>
{new Date(frontmatter.date).toLocaleDateString('nl-NL', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<span></span>
<span>{readingTime} min leestijd</span>
</div>
{/* Tags */}
{frontmatter.tags && frontmatter.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4">
{frontmatter.tags.map((tag) => (
<span
key={tag}
className="px-2.5 py-1 bg-slate-100 text-slate-600 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
)}
</header>
{/* MDX Content */}
<div className="prose prose-slate max-w-none">
<MDXRemote source={content} components={mdxComponents} />
</div>
{/* Series Navigation */}
<nav className="mt-12 pt-8 border-t border-slate-200">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
{series.title} Serie
</div>
<div className="grid grid-cols-2 gap-4">
{/* Previous */}
<div>
{navigation.previous ? (
<Link
href={`/blog/serie/${serieId}/${navigation.previous.slug}`}
className="block p-4 rounded-lg border border-slate-200 hover:border-teal-300 hover:shadow-sm transition-all group"
>
<div className="flex items-center gap-2 text-sm text-slate-500 mb-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span>Vorige</span>
</div>
<div className="font-medium text-slate-900 group-hover:text-teal-600 transition-colors line-clamp-1">
{navigation.previous.title}
</div>
</Link>
) : (
<div />
)}
</div>
{/* Next */}
<div>
{navigation.next ? (
<Link
href={`/blog/serie/${serieId}/${navigation.next.slug}`}
className="block p-4 rounded-lg border border-slate-200 hover:border-teal-300 hover:shadow-sm transition-all group text-right"
>
<div className="flex items-center justify-end gap-2 text-sm text-slate-500 mb-1">
<span>Volgende</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
<div className="font-medium text-slate-900 group-hover:text-teal-600 transition-colors line-clamp-1">
{navigation.next.title}
</div>
</Link>
) : (
<div />
)}
</div>
</div>
</nav>
{/* Back to overview */}
<footer className="mt-8 text-center">
<Link
href="/blog"
className="text-sm text-slate-600 hover:text-teal-600 transition-colors"
>
Terug naar blog overzicht
</Link>
</footer>
</article>
</div>
)
}

View File

@@ -1,178 +0,0 @@
/**
* Series Overview Page
*
* Shows all posts in a specific series
*/
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { getSeries, getPostsBySeries, getAllSeries } from '@/lib/mdx/blog'
interface SeriesPageProps {
params: Promise<{ serieId: string }>
}
export async function generateStaticParams() {
const series = await getAllSeries()
return series.map((serie) => ({ serieId: serie.id }))
}
export async function generateMetadata({ params }: SeriesPageProps) {
const { serieId } = await params
const series = await getSeries(serieId)
if (!series) {
return { title: 'Serie niet gevonden' }
}
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const seriesUrl = `${siteUrl}/blog/serie/${serieId}`
return {
title: `${series.title} - Blog | AI Speedrun`,
description: series.description,
openGraph: {
title: series.title,
description: series.description,
type: 'website',
siteName: 'AI Speedrun',
locale: 'nl_NL',
},
twitter: {
card: 'summary',
title: series.title,
description: series.description,
},
alternates: {
canonical: seriesUrl,
},
}
}
export default async function SeriesPage({ params }: SeriesPageProps) {
const { serieId } = await params
const series = await getSeries(serieId)
if (!series) {
notFound()
}
const posts = await getPostsBySeries(serieId)
const colorStyles = {
teal: {
bg: 'bg-teal-50',
badge: 'bg-teal-100 text-teal-700 border-teal-200',
dot: 'bg-teal-500',
number: 'bg-teal-600 text-white',
},
amber: {
bg: 'bg-amber-50',
badge: 'bg-amber-100 text-amber-700 border-amber-200',
dot: 'bg-amber-500',
number: 'bg-amber-600 text-white',
},
slate: {
bg: 'bg-slate-50',
badge: 'bg-slate-100 text-slate-700 border-slate-200',
dot: 'bg-slate-500',
number: 'bg-slate-600 text-white',
},
}
const styles = colorStyles[series.color] || colorStyles.slate
const statusLabels = {
completed: 'Voltooid',
active: 'Actief',
planned: 'Gepland',
}
return (
<div className="min-h-screen bg-slate-50 pb-16">
<div className="max-w-3xl mx-auto px-4 sm:px-6 md:px-8">
{/* Back link */}
<div className="pt-20 md:pt-24 mb-6">
<Link
href="/blog"
className="inline-flex items-center gap-2 text-slate-600 hover:text-teal-600 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span>Terug naar blog</span>
</Link>
</div>
{/* Series Header */}
<header className={`rounded-xl p-6 md:p-8 mb-8 ${styles.bg}`}>
<div className="flex items-center gap-2 mb-3">
<div className={`w-2.5 h-2.5 rounded-full ${styles.dot}`} />
<span className={`text-xs font-medium px-2.5 py-1 rounded-full border ${styles.badge}`}>
{statusLabels[series.status]}
</span>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-slate-900 mb-3">
{series.title}
</h1>
<p className="text-lg text-slate-600 leading-relaxed">
{series.description}
</p>
<div className="mt-4 text-sm text-slate-500">
{posts.length} {posts.length === 1 ? 'deel' : 'delen'}
</div>
</header>
{/* Posts List */}
{posts.length === 0 ? (
<p className="text-slate-500 text-center py-12">
Nog geen posts in deze serie.
</p>
) : (
<div className="space-y-3">
{posts.map((post, index) => (
<Link
key={post.slug}
href={`/blog/serie/${serieId}/${post.slug}`}
className="flex items-start gap-4 bg-white rounded-lg border border-slate-200 p-4 md:p-5 hover:border-teal-300 hover:shadow-md transition-all active:scale-[0.99]"
>
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${styles.number}`}
>
{index + 1}
</div>
<div className="flex-1 min-w-0">
<h2 className="text-lg font-bold text-slate-900 mb-1 leading-tight">
{post.frontmatter.title}
</h2>
<p className="text-sm text-slate-600 mb-2 line-clamp-2">
{post.frontmatter.description}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-slate-500">
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString('nl-NL', {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
</time>
<span></span>
<span>{post.readingTime} min</span>
</div>
</div>
<svg
className="w-5 h-5 text-slate-400 flex-shrink-0 mt-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
))}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,38 +0,0 @@
'use client'
import { useEffect } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
/**
* Auth Code Handler
*
* Handles auth codes that Supabase redirects to the home page
* Redirects to /auth/callback to process the code exchange
*/
export function AuthCodeHandler() {
const searchParams = useSearchParams()
const router = useRouter()
const code = searchParams.get('code')
const type = searchParams.get('type')
useEffect(() => {
if (code) {
// Build callback URL with all parameters
const callbackUrl = new URL('/auth/callback', window.location.origin)
callbackUrl.searchParams.set('code', code)
if (type) {
callbackUrl.searchParams.set('type', type)
}
// If it's a recovery flow, ensure we redirect to update-password
if (type === 'recovery') {
callbackUrl.searchParams.set('next', '/update-password')
}
// Redirect to callback route
router.replace(callbackUrl.pathname + callbackUrl.search)
}
}, [code, type, router])
return null
}

View File

@@ -1,273 +0,0 @@
"use client";
import { useMotionValueEvent, useScroll, useTransform, motion } from "framer-motion";
import React, { useEffect, useRef, useState } from "react";
import {
Rocket, Database, Sparkles, Palette, Calendar, Layout,
Users, Smartphone, FileText, Brain, Tags, Target,
HelpCircle, Zap, Eye
} from "lucide-react";
// Icon mapping
const iconMap = {
Rocket,
Database,
Sparkles,
Palette,
Calendar,
Layout,
Users,
Smartphone,
FileText,
Brain,
Tags,
Target,
HelpCircle,
Zap,
Eye,
};
interface Feature {
title: string;
description: string;
time?: string;
traditional?: string;
icon: keyof typeof iconMap;
}
interface WeekData {
weekNumber: number;
title: string;
status: "completed" | "in_progress" | "planned";
description: string;
features: Feature[];
metrics: {
hours: string;
cost: string;
linesOfCode: string;
};
achievements: string[];
}
interface TimelineData {
heading: string;
description: string;
weeks: WeekData[];
}
interface BuildTimelineProps {
data: TimelineData;
}
const StatusBadge = ({ status }: { status: WeekData["status"] }) => {
const styles = {
completed: "bg-teal-100 text-teal-700 border-teal-200",
in_progress: "bg-amber-100 text-amber-700 border-amber-200",
planned: "bg-slate-100 text-slate-600 border-slate-200",
};
const labels = {
completed: "Voltooid",
in_progress: "Bezig",
planned: "Gepland",
};
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${styles[status]}`}>
{labels[status]}
</span>
);
};
const FeatureCard = ({ feature }: { feature: Feature }) => {
const Icon = iconMap[feature.icon];
return (
<div className="bg-white border border-slate-200 rounded-lg p-4 hover:border-teal-300 transition-colors">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-10 h-10 bg-teal-100 rounded-lg flex items-center justify-center">
<Icon className="w-5 h-5 text-teal-600" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-slate-900 text-sm mb-1">
{feature.title}
</h4>
<p className="text-slate-600 text-sm leading-relaxed">
{feature.description}
</p>
{feature.time && feature.traditional && (
<div className="mt-2 flex flex-col gap-1 text-xs">
<div className="flex items-center gap-2">
<span className="font-medium text-teal-600">Met AI:</span>
<span className="text-slate-900 font-semibold">{feature.time}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium text-slate-500">Traditioneel:</span>
<span className="text-slate-600 line-through">{feature.traditional}</span>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export const BuildTimeline = ({ data }: BuildTimelineProps) => {
const ref = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(0);
useEffect(() => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setHeight(rect.height);
}
}, [ref]);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start 10%", "end 50%"],
});
const heightTransform = useTransform(scrollYProgress, [0, 1], [0, height]);
const opacityTransform = useTransform(scrollYProgress, [0, 0.1], [0, 1]);
return (
<div className="w-full bg-slate-50 font-sans" ref={containerRef}>
{/* Header */}
<div className="max-w-7xl mx-auto py-16 px-4 md:px-8 lg:px-10 text-center">
<h2 className="font-serif text-3xl md:text-4xl font-bold text-slate-900 mb-4">
{data.heading}
</h2>
<p className="text-slate-600 text-lg max-w-3xl mx-auto">
{data.description}
</p>
</div>
{/* Timeline */}
<div ref={ref} className="relative max-w-7xl mx-auto pb-20">
{data.weeks.map((week, index) => (
<div
key={index}
className="flex justify-start pt-10 md:pt-20 md:gap-10"
>
{/* Left side - Week title (sticky) */}
<div className="sticky flex flex-col md:flex-row z-40 items-center top-40 self-start max-w-xs lg:max-w-sm md:w-full">
{/* Timeline dot */}
<div className="h-10 absolute left-3 md:left-3 w-10 rounded-full bg-slate-50 flex items-center justify-center">
<div className="h-4 w-4 rounded-full bg-teal-500 border-2 border-white shadow-md" />
</div>
{/* Week title - hidden on mobile */}
<div className="hidden md:block md:pl-20">
<h3 className="text-2xl md:text-3xl font-bold text-slate-800 mb-2">
{week.title}
</h3>
<StatusBadge status={week.status} />
</div>
</div>
{/* Right side - Content */}
<div className="relative pl-20 pr-4 md:pl-4 w-full">
{/* Week title - mobile only */}
<div className="md:hidden mb-4">
<h3 className="text-2xl font-bold text-slate-800 mb-2">
{week.title}
</h3>
<StatusBadge status={week.status} />
</div>
{/* Description */}
<p className="text-slate-700 text-base leading-relaxed mb-6">
{week.description}
</p>
{/* Features */}
{week.features.length > 0 && (
<div className="mb-6">
<h4 className="text-sm font-semibold text-slate-500 uppercase tracking-wide mb-3">
Features
</h4>
<div className="grid gap-3 md:grid-cols-2">
{week.features.map((feature, fIndex) => (
<FeatureCard key={fIndex} feature={feature} />
))}
</div>
</div>
)}
{/* Metrics */}
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-6">
<h4 className="text-sm font-semibold text-slate-500 uppercase tracking-wide mb-3">
Metrics
</h4>
<div className="grid grid-cols-3 gap-4">
<div>
<div className="text-2xl font-bold text-teal-600">
{week.metrics.hours}
</div>
<div className="text-xs text-slate-500">Development</div>
</div>
<div>
<div className="text-2xl font-bold text-teal-600">
{week.metrics.cost}
</div>
<div className="text-xs text-slate-500">Infrastructure</div>
</div>
<div>
<div className="text-2xl font-bold text-teal-600">
{week.metrics.linesOfCode}
</div>
<div className="text-xs text-slate-500">Lines of Code</div>
</div>
</div>
</div>
{/* Achievements */}
{week.achievements.length > 0 && (
<div className="bg-teal-50 border border-teal-100 rounded-lg p-4 mb-4">
<h4 className="text-sm font-semibold text-teal-700 uppercase tracking-wide mb-2">
Achievements
</h4>
<ul className="space-y-1">
{week.achievements.map((achievement, aIndex) => (
<li key={aIndex} className="text-sm text-slate-700">
{achievement}
</li>
))}
</ul>
</div>
)}
{/* Link to detailed documentation */}
<a
href="/documentatie"
className="inline-flex items-center gap-2 text-teal-600 hover:text-teal-700 font-medium text-sm transition-colors"
>
<span>Bekijk uitgebreide documentatie</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
</div>
))}
{/* Animated timeline line */}
<div
style={{ height: height + "px" }}
className="absolute md:left-8 left-8 top-0 overflow-hidden w-[2px] bg-gradient-to-b from-transparent via-slate-300 to-transparent"
>
<motion.div
style={{
height: heightTransform,
opacity: opacityTransform,
}}
className="absolute inset-x-0 top-0 w-[2px] bg-gradient-to-b from-teal-500 via-teal-400 to-transparent rounded-full"
/>
</div>
</div>
</div>
);
};

View File

@@ -1,58 +0,0 @@
/**
* Hero Quote Component
*
* Full-viewport hero section with Jensen Huang quote.
* Displays quote, attribution, and subtitle from content.
*/
import type { HeroContent } from '@/content/schemas/manifesto'
import { MarketingShader } from './marketing-shader'
interface HeroQuoteProps {
content: HeroContent
}
export function HeroQuote({ content }: HeroQuoteProps) {
return (
<section
className="relative min-h-screen flex items-center justify-center bg-slate-900"
style={{ backgroundColor: '#0F172A' }} // Expliciet donkere achtergrond voor maximum contrast
aria-label="Hero quote sectie"
>
{/* Shader Background - zeer subtiel (opacity 0.02) */}
{/* Op mobile: donkere fallback, op desktop: subtiele shader */}
<MarketingShader variant="hero" className="z-0" />
{/* Content Overlay */}
<div className="relative z-10 w-full max-w-4xl px-4 md:px-8 text-center pt-20 md:pt-0">
<blockquote
className="text-white font-serif italic mb-4 md:mb-6"
style={{
fontSize: 'clamp(1.5rem, 5vw, 4rem)', // Mobile: smaller, Desktop: larger
lineHeight: 'var(--line-height-tight)',
textShadow: '0 2px 8px rgba(0, 0, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.3)', // Sterkere shadow voor beter contrast
color: '#FFFFFF', // Expliciet wit voor maximum contrast
}}
cite={content.attribution}
>
&ldquo;{content.quote}&rdquo;
</blockquote>
<div className="text-white font-sans text-sm md:text-base" style={{ textShadow: '0 1px 3px rgba(0, 0, 0, 0.4)' }}>
<cite className="not-italic font-semibold text-base md:text-lg text-white">
{content.attribution}
</cite>
{content.attributionContext && (
<span className="text-slate-200 text-sm md:text-base ml-2" aria-hidden="true">
{content.attributionContext}
</span>
)}
</div>
<p className="text-white mt-4 md:mt-6 text-base md:text-xl font-sans" style={{ textShadow: '0 1px 3px rgba(0, 0, 0, 0.4)' }}>
{content.subtitle}
</p>
</div>
</section>
)
}

View File

@@ -1,33 +0,0 @@
'use client'
import { HeroSection } from '@/components/ui/hero-section-2'
interface HeroSectionClientProps {
logo?: {
url: string
alt: string
text?: string
}
slogan?: string
title: React.ReactNode
subtitle: string | React.ReactNode
punchline?: string | React.ReactNode
callToAction: {
text: string
href: string
}
secondaryCallToAction?: {
text: string
href: string
}
backgroundImage: string
contactInfo?: {
website: string
phone: string
address: string
}
}
export function HeroSectionClient(props: HeroSectionClientProps) {
return <HeroSection {...props} />
}

View File

@@ -1,32 +0,0 @@
/**
* Insight Box Component
*
* Visual anchor for key takeaways with yellow left border.
* Used to highlight important insights in the manifesto content.
*/
interface InsightBoxProps {
children: React.ReactNode
variant?: 'default' | 'highlight'
}
export function InsightBox({ children, variant = 'default' }: InsightBoxProps) {
const borderColor = variant === 'highlight' ? '#D97706' : '#F59E0B'
return (
<div
className="bg-white shadow-sm my-6 md:my-8 p-6 border-t-4 md:border-t-0 md:border-l-4"
style={{
borderLeftColor: borderColor,
borderTopColor: borderColor,
borderLeftWidth: '3px',
borderTopWidth: '3px',
}}
>
<p className="text-base md:text-lg font-serif text-slate-900 leading-relaxed">
{children}
</p>
</div>
)
}

View File

@@ -1,83 +0,0 @@
'use client'
/**
* Marketing Shader Component
*
* Wrapper around DotScreenShader for marketing pages.
* Very subtle opacity (0.02) for hero sections.
* Includes mobile fallback and reduced motion support.
*/
import dynamic from 'next/dynamic'
import { useEffect, useState } from 'react'
// Lazy load the shader component (heavy, only load when needed)
const DotScreenShader = dynamic(
() => import('@/components/ui/dot-shader-background').then((mod) => ({ default: mod.DotScreenShader })),
{
ssr: false,
loading: () => null
}
)
interface MarketingShaderProps {
variant?: 'hero' | 'section'
className?: string
}
export function MarketingShader({ variant = 'hero', className = '' }: MarketingShaderProps) {
const [mounted, setMounted] = useState(false)
const [reducedMotion, setReducedMotion] = useState(false)
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
setMounted(true)
// Check for reduced motion preference
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
setReducedMotion(mediaQuery.matches)
const handleChange = (e: MediaQueryListEvent) => setReducedMotion(e.matches)
mediaQuery.addEventListener('change', handleChange)
// Check for mobile (simple check)
const checkMobile = () => {
setIsMobile(window.innerWidth < 768)
}
checkMobile()
window.addEventListener('resize', checkMobile)
return () => {
mediaQuery.removeEventListener('change', handleChange)
window.removeEventListener('resize', checkMobile)
}
}, [])
// Fallback voor mobile of reduced motion
// Voor hero variant: donkere achtergrond, voor section variant: lichte achtergrond
if (!mounted || reducedMotion || isMobile) {
const fallbackBg = variant === 'hero'
? 'bg-slate-900' // Donkere achtergrond voor hero
: 'bg-gradient-to-br from-slate-50 to-slate-100' // Lichte achtergrond voor sections
return (
<div
className={`absolute inset-0 ${fallbackBg} ${className}`}
aria-hidden="true"
/>
)
}
// Desktop: shader component met zeer subtiele opacity
// Opacity 0.02 is zeer subtiel - bijna onzichtbaar maar aanwezig
return (
<div
className={`absolute inset-0 overflow-hidden ${className}`}
aria-hidden="true"
style={{ opacity: variant === 'hero' ? 0.02 : 0.03 }}
>
<DotScreenShader />
</div>
)
}

View File

@@ -1,154 +0,0 @@
'use client'
/**
* Minimal Navigation Component
*
* Fixed top navigation with logo and links.
* Mobile-friendly with hamburger menu.
* Adaptive styling based on scroll position (hero vs content).
*/
import { useState, useEffect } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { Menu, X } from 'lucide-react'
import type { NavigationContent } from '@/content/schemas/manifesto'
interface MinimalNavProps {
content: NavigationContent
}
export function MinimalNav({ content }: MinimalNavProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isScrolled, setIsScrolled] = useState(false)
useEffect(() => {
const handleScroll = () => {
// Check if scrolled past hero section (approximately 100vh)
setIsScrolled(window.scrollY > window.innerHeight * 0.8)
}
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll() // Initial check
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
// Close menu when clicking outside or on link
useEffect(() => {
if (isMenuOpen) {
const handleClickOutside = () => setIsMenuOpen(false)
document.addEventListener('click', handleClickOutside)
return () => document.removeEventListener('click', handleClickOutside)
}
}, [isMenuOpen])
// Determine nav styling based on scroll position
// On hero: dark nav with light text, on content: light nav with dark text
const navClasses = isScrolled
? 'bg-white/95 backdrop-blur-sm shadow-sm'
: 'bg-slate-900/80 backdrop-blur-sm md:bg-white/80 md:backdrop-blur-sm'
const logoClasses = isScrolled
? 'text-teal-600'
: 'text-white md:text-teal-600'
const linkClasses = isScrolled
? 'text-slate-900 hover:text-slate-700'
: 'text-white hover:text-slate-200 md:mix-blend-difference md:text-white md:hover:opacity-80'
const menuButtonClasses = isScrolled
? 'text-slate-900 focus-visible:outline-slate-900'
: 'text-white focus-visible:outline-white'
return (
<nav
className={`fixed top-0 left-0 right-0 z-[60] transition-colors duration-200 ${navClasses}`}
role="navigation"
aria-label="Hoofdnavigatie"
>
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex justify-between items-center h-16">
{/* Logo */}
<Link
href="/"
className={`flex items-center gap-2 font-black text-xl uppercase font-sans tracking-tight leading-none transition-colors hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:rounded ${logoClasses}`}
aria-label="AI Speedrun Home"
onClick={() => setIsMenuOpen(false)}
>
{content.logo}
<Image
src="/images/aispeedrun-logo.webp"
alt=""
width={40}
height={40}
className="rounded-md"
/>
</Link>
{/* Desktop Navigation Links */}
<div className="hidden md:flex gap-6 items-center" role="list">
{content.links.map((link) => (
<Link
key={link.href}
href={link.href}
className={`font-semibold text-base font-sans transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:rounded ${linkClasses}`}
role="listitem"
>
{link.label}
</Link>
))}
</div>
{/* Mobile Menu Button */}
<button
className={`md:hidden p-2 -mr-2 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:rounded ${menuButtonClasses}`}
onClick={(e) => {
e.stopPropagation()
setIsMenuOpen(!isMenuOpen)
}}
aria-label="Menu"
aria-expanded={isMenuOpen}
aria-controls="mobile-menu"
>
{isMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</button>
</div>
{/* Mobile Menu */}
{isMenuOpen && (
<div
id="mobile-menu"
className={`md:hidden border-t ${
isScrolled
? 'bg-white border-slate-200'
: 'bg-slate-900 border-slate-800'
}`}
onClick={(e) => e.stopPropagation()}
>
<div className="px-4 py-4 space-y-3">
{content.links.map((link) => (
<Link
key={link.href}
href={link.href}
className={`block font-semibold text-base py-2 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:rounded ${
isScrolled
? 'text-slate-900 hover:text-slate-700 focus-visible:outline-slate-900'
: 'text-white hover:text-slate-200 focus-visible:outline-white'
}`}
onClick={() => setIsMenuOpen(false)}
>
{link.label}
</Link>
))}
</div>
</div>
)}
</nav>
)
}

View File

@@ -1,69 +0,0 @@
'use client'
/**
* Reading Progress Bar Component
*
* Fixed top progress indicator that shows scroll progress through the page.
* Smooth animation, 2px height, accent blue color.
* Respects prefers-reduced-motion for accessibility.
*/
import { useEffect, useState } from 'react'
export function ReadingProgress() {
const [progress, setProgress] = useState(0)
const [reducedMotion, setReducedMotion] = useState(false)
useEffect(() => {
// Check for reduced motion preference
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
setReducedMotion(mediaQuery.matches)
const handleChange = (e: MediaQueryListEvent) => setReducedMotion(e.matches)
mediaQuery.addEventListener('change', handleChange)
const updateProgress = () => {
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
const scrollTop = window.scrollY || document.documentElement.scrollTop
// Calculate scroll progress percentage
const scrollableHeight = documentHeight - windowHeight
const currentProgress = scrollableHeight > 0
? (scrollTop / scrollableHeight) * 100
: 0
setProgress(Math.min(100, Math.max(0, currentProgress)))
}
// Initial calculation
updateProgress()
// Update on scroll
window.addEventListener('scroll', updateProgress, { passive: true })
window.addEventListener('resize', updateProgress, { passive: true })
return () => {
mediaQuery.removeEventListener('change', handleChange)
window.removeEventListener('scroll', updateProgress)
window.removeEventListener('resize', updateProgress)
}
}, [])
return (
<div
className="fixed top-[64px] left-0 right-0 z-50 h-0.5 bg-slate-200"
role="progressbar"
aria-valuenow={Math.round(progress)}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Leesvoortgang"
>
<div
className={`h-full bg-teal-600 ${reducedMotion ? '' : 'transition-all duration-150 ease-out'}`}
style={{ width: `${progress}%` }}
/>
</div>
)
}

View File

@@ -1,51 +0,0 @@
/**
* Statement Section Component
*
* Full-width section with dark background for impactful statements.
* Used to highlight key messages in the manifesto.
*/
import type { StatementSection as StatementSectionType } from '@/content/schemas/manifesto'
interface StatementSectionProps {
section: StatementSectionType
}
export function StatementSection({ section }: StatementSectionProps) {
const isDark = section.variant === 'dark' || !section.variant
return (
<section
className={`w-full ${
isDark ? 'bg-slate-900' : 'bg-slate-50'
}`}
style={{
padding: '4rem 2rem',
}}
aria-labelledby={`statement-${section.id}`}
>
<div className="max-w-4xl mx-auto text-center">
<h2
id={`statement-${section.id}`}
className={`font-bold mb-4 md:mb-6 font-serif ${
isDark ? 'text-white' : 'text-slate-900'
}`}
style={{
fontSize: 'clamp(2rem, 5vw, 4rem)',
lineHeight: 'var(--line-height-tight)',
}}
>
{section.heading}
</h2>
<p
className={`text-lg md:text-xl font-serif leading-relaxed ${
isDark ? 'text-slate-300' : 'text-slate-700'
}`}
>
{section.content}
</p>
</div>
</section>
)
}

View File

@@ -1,326 +0,0 @@
'use client'
import { useState } from 'react'
import { Send, CheckCircle, XCircle } from 'lucide-react'
import type { FormSection } from '@/content/schemas/manifesto'
import type { LeadFormData } from '@/app/api/leads/route'
interface ContactFormProps {
content: FormSection
}
type FormStatus = 'idle' | 'submitting' | 'success' | 'error'
export function ContactForm({ content }: ContactFormProps) {
const [status, setStatus] = useState<FormStatus>('idle')
const [errors, setErrors] = useState<Record<string, string>>({})
const [formData, setFormData] = useState<LeadFormData>({
name: '',
email: '',
company: '',
projectType: '',
budget: '',
message: '',
})
const validateField = (name: keyof LeadFormData, value: string): string | null => {
const field = content.fields[name]
if (field.required && !value.trim()) {
return field.error || `${field.label} is verplicht`
}
if (name === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return 'Ongeldig email adres'
}
}
if (name === 'message' && value && field.minLength) {
if (value.length < field.minLength) {
return `Minimaal ${field.minLength} karakters vereist`
}
}
return null
}
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
// Clear error on change
if (errors[name]) {
setErrors(prev => {
const newErrors = { ...prev }
delete newErrors[name]
return newErrors
})
}
}
const handleBlur = (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target
const error = validateField(name as keyof LeadFormData, value)
if (error) {
setErrors(prev => ({ ...prev, [name]: error }))
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Validate all fields
const newErrors: Record<string, string> = {}
Object.keys(formData).forEach(key => {
const error = validateField(key as keyof LeadFormData, formData[key as keyof LeadFormData] || '')
if (error) {
newErrors[key] = error
}
})
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// Submit form
setStatus('submitting')
try {
const response = await fetch('/api/leads', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Er ging iets mis')
}
setStatus('success')
// Reset form
setFormData({
name: '',
email: '',
company: '',
projectType: '',
budget: '',
message: '',
})
} catch (error) {
console.error('Form submission error:', error)
setStatus('error')
}
}
// Success state
if (status === 'success') {
return (
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-8 text-center">
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-slate-900 mb-2">
{content.success.title}
</h3>
<p className="text-slate-600 mb-6">
{content.success.message}
</p>
<a
href="/"
className="inline-block px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors"
>
{content.success.cta}
</a>
</div>
)
}
// Error state
if (status === 'error') {
return (
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-8 text-center">
<XCircle className="w-16 h-16 text-red-600 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-slate-900 mb-2">
{content.error.title}
</h3>
<p className="text-slate-600 mb-6">
{content.error.message}
</p>
<button
onClick={() => setStatus('idle')}
className="inline-block px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors"
>
{content.error.retry}
</button>
</div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Name */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.name.label} {content.fields.name.required && <span className="text-red-500">*</span>}
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
placeholder={content.fields.name.placeholder}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.name ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
{/* Email */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.email.label} {content.fields.email.required && <span className="text-red-500">*</span>}
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder={content.fields.email.placeholder}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.email ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Company */}
<div>
<label htmlFor="company" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.company.label}
</label>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleChange}
placeholder={content.fields.company.placeholder}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors"
disabled={status === 'submitting'}
/>
</div>
{/* Project Type */}
<div>
<label htmlFor="projectType" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.projectType.label} {content.fields.projectType.required && <span className="text-red-500">*</span>}
</label>
<select
id="projectType"
name="projectType"
value={formData.projectType}
onChange={handleChange}
onBlur={handleBlur}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.projectType ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
>
<option value="">{content.fields.projectType.placeholder}</option>
{content.fields.projectType.options?.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
{errors.projectType && (
<p className="mt-1 text-sm text-red-600">{errors.projectType}</p>
)}
</div>
{/* Budget */}
<div>
<label htmlFor="budget" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.budget.label}
</label>
<select
id="budget"
name="budget"
value={formData.budget}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors"
disabled={status === 'submitting'}
>
<option value="">{content.fields.budget.placeholder}</option>
{content.fields.budget.options?.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
{/* Message */}
<div>
<label htmlFor="message" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.message.label} {content.fields.message.required && <span className="text-red-500">*</span>}
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
onBlur={handleBlur}
placeholder={content.fields.message.placeholder}
rows={6}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.message ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
/>
{errors.message && (
<p className="mt-1 text-sm text-red-600">{errors.message}</p>
)}
<p className="mt-1 text-sm text-slate-500">
{formData.message.length} / {content.fields.message.minLength} minimum
</p>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={status === 'submitting'}
className="w-full px-8 py-4 bg-green-600 hover:bg-green-700 disabled:bg-slate-400 text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
>
{status === 'submitting' ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
{content.buttons.submitting}
</>
) : (
<>
<Send className="w-5 h-5" />
{content.buttons.submit}
</>
)}
</button>
</form>
)
}

View File

@@ -1,55 +0,0 @@
/**
* Contact Page - Under Construction
*/
import type { Metadata } from 'next'
import { Construction } from 'lucide-react'
import Link from 'next/link'
// Generate metadata for SEO
export async function generateMetadata(): Promise<Metadata> {
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
return {
title: 'Contact - AI Speedrun',
description: 'Deze pagina is momenteel in ontwikkeling.',
openGraph: {
type: 'website',
title: 'Contact - AI Speedrun',
description: 'Deze pagina is momenteel in ontwikkeling',
images: [`${siteUrl}/og-image.png`],
siteName: 'AI Speedrun',
locale: 'nl_NL',
},
alternates: {
canonical: `${siteUrl}/contact`,
},
}
}
export default function ContactPage() {
return (
<section className="relative min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-white px-4">
<div className="max-w-2xl mx-auto text-center">
<div className="inline-flex items-center justify-center w-24 h-24 bg-orange-100 rounded-full mb-8">
<Construction className="w-12 h-12 text-orange-600" />
</div>
<h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-4">
Pagina in ontwikkeling
</h1>
<p className="text-xl text-slate-600 mb-8">
We zijn bezig met het bouwen van deze pagina. Kom binnenkort terug!
</p>
<Link
href="/"
className="inline-flex items-center justify-center px-6 py-3 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors"
>
Terug naar home
</Link>
</div>
</section>
)
}

View File

@@ -1,202 +0,0 @@
/**
* Release Detail Page
*
* Renders individual release note from MDX file
*/
import { notFound } from 'next/navigation'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { getRelease, getAllReleases } from '@/lib/mdx/documentatie'
import { mdxComponents } from '../components/mdx-components'
interface ReleasePageProps {
params: Promise<{
category: string
}>
}
// Generate static params for all releases
export async function generateStaticParams() {
const releases = await getAllReleases()
return releases.map((release) => ({
category: release.slug,
}))
}
// Generate metadata for SEO
export async function generateMetadata({ params }: ReleasePageProps) {
const { category } = await params
const release = await getRelease(category)
if (!release) {
return {
title: 'Release Not Found',
}
}
return {
title: `${release.frontmatter.title} - Documentatie`,
description: release.frontmatter.description,
}
}
function ArticleJsonLd({
title,
description,
releaseDate,
slug,
}: {
title: string
description: string
releaseDate: string
slug: string
}) {
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Article',
'@id': `${siteUrl}/documentatie/${slug}#article`,
headline: title,
description: description,
datePublished: releaseDate,
dateModified: releaseDate,
author: {
'@type': 'Person',
name: 'Colin van der Heijden',
url: 'https://ikbenlit.nl',
},
publisher: { '@id': `${siteUrl}/#organization` },
mainEntityOfPage: `${siteUrl}/documentatie/${slug}`,
inLanguage: 'nl-NL',
},
{
'@type': 'BreadcrumbList',
'@id': `${siteUrl}/documentatie/${slug}#breadcrumb`,
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: siteUrl,
},
{
'@type': 'ListItem',
position: 2,
name: 'Documentatie',
item: `${siteUrl}/documentatie`,
},
{
'@type': 'ListItem',
position: 3,
name: title,
},
],
},
],
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
export default async function ReleasePage({ params }: ReleasePageProps) {
const { category } = await params
const release = await getRelease(category)
if (!release) {
notFound()
}
const { frontmatter, content } = release
return (
<div className="min-h-screen bg-white pb-16">
<ArticleJsonLd
title={frontmatter.title}
description={frontmatter.description}
releaseDate={frontmatter.releaseDate}
slug={category}
/>
<article className="max-w-4xl mx-auto px-4 md:px-8 pt-20 md:pt-20">
{/* Header */}
<header className="mb-8 pb-8 border-b border-slate-200">
<div className="flex items-center gap-3 mb-4">
<StatusBadge status={frontmatter.status} />
<span className="text-sm text-slate-500">v{frontmatter.version}</span>
<span className="text-sm text-slate-500"></span>
<time className="text-sm text-slate-500">
{new Date(frontmatter.releaseDate).toLocaleDateString('nl-NL', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-4">
{frontmatter.title}
</h1>
<p className="text-xl text-slate-600">
{frontmatter.description}
</p>
</header>
{/* MDX Content */}
<div className="prose prose-slate max-w-none">
<MDXRemote source={content} components={mdxComponents} />
</div>
{/* Footer Navigation */}
<footer className="mt-12 pt-8 border-slate-200">
<div className="flex justify-between items-center">
<a
href="/documentatie"
className="text-teal-600 hover:text-teal-700 font-medium flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Terug naar overzicht
</a>
<a
href="/#timeline"
className="text-slate-600 hover:text-slate-700 font-medium"
>
Bekijk timeline
</a>
</div>
</footer>
</article>
</div>
)
}
function StatusBadge({ status }: { status: 'completed' | 'in_progress' | 'planned' }) {
const styles = {
completed: 'bg-teal-100 text-teal-700 border-teal-200',
in_progress: 'bg-amber-100 text-amber-700 border-amber-200',
planned: 'bg-slate-100 text-slate-600 border-slate-200',
}
const labels = {
completed: 'Voltooid',
in_progress: 'Bezig',
planned: 'Gepland',
}
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${styles[status]}`}>
{labels[status]}
</span>
)
}

View File

@@ -1,191 +0,0 @@
/**
* Custom MDX Components
*
* Styled components for rendering MDX content
*/
import Image from 'next/image'
import Link from 'next/link'
import type { MDXComponents } from 'mdx/types'
/**
* Generate slug from heading text for anchor links
*/
function slugify(text: string): string {
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
}
/**
* Extract text content from React children
*/
function getTextContent(children: React.ReactNode): string {
if (typeof children === 'string') return children
if (Array.isArray(children)) return children.map(getTextContent).join('')
if (children && typeof children === 'object' && 'props' in children) {
return getTextContent(children.props.children)
}
return ''
}
export const mdxComponents: MDXComponents = {
// Headings with anchor links
h1: ({ children, ...props }) => {
const text = getTextContent(children)
const id = slugify(text)
return (
<h1 id={id} className="text-4xl font-bold text-slate-900 mt-8 mb-4 scroll-mt-20" {...props}>
{children}
</h1>
)
},
h2: ({ children, ...props }) => {
const text = getTextContent(children)
const id = slugify(text)
return (
<h2 id={id} className="text-3xl font-bold text-slate-900 mt-8 mb-4 border-b border-slate-200 pb-2 scroll-mt-20" {...props}>
{children}
</h2>
)
},
h3: ({ children, ...props }) => {
const text = getTextContent(children)
const id = slugify(text)
return (
<h3 id={id} className="text-2xl font-semibold text-slate-900 mt-6 mb-3 scroll-mt-20" {...props}>
{children}
</h3>
)
},
h4: ({ children, ...props }) => {
const text = getTextContent(children)
const id = slugify(text)
return (
<h4 id={id} className="text-xl font-semibold text-slate-900 mt-4 mb-2 scroll-mt-20" {...props}>
{children}
</h4>
)
},
// Paragraphs
p: ({ children, ...props }) => (
<div className="text-slate-700 leading-relaxed mb-4" {...props}>
{children}
</div>
),
// Lists
ul: ({ children, ...props }) => (
<ul className="list-disc list-inside space-y-2 mb-4 text-slate-700" {...props}>
{children}
</ul>
),
ol: ({ children, ...props }) => (
<ol className="list-decimal list-inside space-y-2 mb-4 text-slate-700" {...props}>
{children}
</ol>
),
li: ({ children, ...props }) => (
<li className="ml-4" {...props}>
{children}
</li>
),
// Links
a: ({ href, children, ...props }) => (
<Link
href={href || '#'}
className="text-teal-600 hover:text-teal-700 underline underline-offset-2"
{...props}
>
{children}
</Link>
),
// Images
img: ({ src, alt, width, height, ...props }) => {
if (!src) return null
return (
<figure className="my-8">
<div className="relative rounded-lg overflow-hidden border border-slate-200 bg-slate-50">
<Image
src={src}
alt={alt || ''}
width={Number(width) || 1200}
height={Number(height) || 675}
className="w-full h-auto"
{...props}
/>
</div>
{alt && (
<figcaption className="text-sm text-slate-500 italic mt-2 text-center">
{alt}
</figcaption>
)}
</figure>
)
},
// Code blocks
pre: ({ children, ...props }) => (
<pre className="bg-slate-900 text-slate-50 rounded-lg p-4 overflow-x-auto mb-4 text-sm" {...props}>
{children}
</pre>
),
code: ({ children, ...props }) => (
<code className="bg-slate-100 text-slate-800 px-1.5 py-0.5 rounded text-sm font-mono" {...props}>
{children}
</code>
),
// Blockquote
blockquote: ({ children, ...props }) => (
<blockquote className="border-l-4 border-teal-500 pl-4 py-2 my-4 bg-teal-50 text-slate-700 italic" {...props}>
{children}
</blockquote>
),
// Horizontal rule
hr: (props) => (
<hr className="my-8 border-slate-200" {...props} />
),
// Table
table: ({ children, ...props }) => (
<div className="overflow-x-auto my-6">
<table className="min-w-full divide-y divide-slate-200" {...props}>
{children}
</table>
</div>
),
th: ({ children, ...props }) => (
<th className="px-4 py-3 bg-slate-50 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider" {...props}>
{children}
</th>
),
td: ({ children, ...props }) => (
<td className="px-4 py-3 text-sm text-slate-700 border-t border-slate-200" {...props}>
{children}
</td>
),
// Strong/Bold
strong: ({ children, ...props }) => (
<strong className="font-semibold text-slate-900" {...props}>
{children}
</strong>
),
// Emphasis/Italic
em: ({ children, ...props }) => (
<em className="italic" {...props}>
{children}
</em>
),
}

View File

@@ -1,23 +0,0 @@
'use client'
/**
* Release Sidebar Wrapper
*
* Simple wrapper to avoid webpack module resolution issues
*/
import { ReleaseSidebar } from './release-sidebar'
import type { ReleaseNote, GroupMetadata, CategoryMetadata, TocItem } from '@/lib/mdx/documentatie'
interface ReleaseSidebarWrapperProps {
releases: ReleaseNote[]
metadata: {
groups: GroupMetadata[]
categories: CategoryMetadata[]
}
tocMap: Record<string, TocItem[]>
}
export default function ReleaseSidebarWrapper(props: ReleaseSidebarWrapperProps) {
return <ReleaseSidebar {...props} />
}

View File

@@ -1,304 +0,0 @@
'use client'
/**
* Release Sidebar Navigation
*
* Fixed sidebar with grouped list of all releases
* Auto-generated from MDX files
*/
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ChevronRight, ChevronDown, ChevronLeft, X } from 'lucide-react'
import { useState, useEffect } from 'react'
import type { ReleaseNote, GroupMetadata, CategoryMetadata, TocItem } from '@/lib/mdx/documentatie'
interface ReleaseSidebarProps {
releases: ReleaseNote[]
metadata: {
groups: GroupMetadata[]
categories: CategoryMetadata[]
}
tocMap: Record<string, TocItem[]>
}
export function ReleaseSidebar({ releases, metadata, tocMap }: ReleaseSidebarProps) {
const pathname = usePathname()
const [isExpanded, setIsExpanded] = useState(false)
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({
foundation: true,
architecture: true,
features: true,
infrastructure: true,
bugs: true,
})
// Auto-close sidebar on navigation
useEffect(() => {
setIsExpanded(false)
}, [pathname])
const toggleGroup = (groupId: string) => {
setExpandedGroups(prev => ({
...prev,
[groupId]: !prev[groupId],
}))
}
// Group releases by their group
const releasesByGroup = releases.reduce((acc, release) => {
const group = release.frontmatter.group
if (!acc[group]) acc[group] = []
acc[group].push(release)
return acc
}, {} as Record<string, ReleaseNote[]>)
// Check if we're on a specific release page
const isReleaseDetail = pathname.startsWith('/documentatie/') && pathname !== '/documentatie'
return (
<>
{/* MOBILE: Overlay when expanded */}
{isExpanded && (
<div
className="lg:hidden fixed inset-0 bg-black/40 backdrop-blur-sm z-30 top-16"
onClick={() => setIsExpanded(false)}
aria-hidden="true"
/>
)}
{/* MOBILE: Collapsible sidebar from left */}
<aside
className={`
lg:hidden
fixed top-16 bottom-0 left-0 z-40
bg-white border-r border-slate-200
transition-all duration-300 ease-in-out
${isExpanded ? 'w-80 shadow-2xl' : 'w-12'}
`}
>
{/* Collapsed state: Icon bar */}
{!isExpanded && (
<div className="flex flex-col items-center pt-6">
<button
onClick={() => setIsExpanded(true)}
className="p-2 rounded-lg hover:bg-teal-50 transition-colors group"
aria-label="Open documentatie menu"
>
<ChevronRight className="w-6 h-6 text-slate-600 group-hover:text-teal-600 transition-colors" />
</button>
</div>
)}
{/* Expanded state: Full navigation */}
{isExpanded && (
<div className="p-6 overflow-y-auto h-full">
{/* Header met close button */}
<div className="flex items-center justify-between mb-6 pb-4 border-b border-slate-200">
<h2 className="text-lg font-bold text-slate-900">Documentatie</h2>
<button
onClick={() => setIsExpanded(false)}
className="p-1.5 rounded-lg hover:bg-slate-100 transition-colors group"
aria-label="Sluit menu"
>
<ChevronLeft className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors" />
</button>
</div>
<nav className="space-y-1">
{/* Overzicht link */}
<Link
href="/documentatie"
className={`flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
pathname === '/documentatie' && !isReleaseDetail
? 'bg-teal-50 text-teal-700'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
<span>Overzicht</span>
</Link>
{/* Grouped releases */}
{metadata?.groups && metadata.groups
.sort((a, b) => a.order - b.order)
.map((group) => {
const groupReleases = releasesByGroup[group.id] || []
const isGroupExpanded = expandedGroups[group.id]
return (
<div key={group.id} className="space-y-1">
{/* Group header */}
<button
onClick={() => toggleGroup(group.id)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wide hover:text-slate-700 transition-colors"
>
<span>{group.title}</span>
{isGroupExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
{/* Group items */}
{isGroupExpanded && (
<div className="space-y-1 ml-2">
{groupReleases.map((release) => {
const isActive = pathname === `/documentatie/${release.slug}`
const toc = tocMap[release.slug] || []
return (
<div key={release.slug}>
<Link
href={`/documentatie/${release.slug}`}
className={`flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors group ${
isActive
? 'bg-teal-50 text-teal-700'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
<div className="flex items-center gap-2 min-w-0">
<StatusDot status={release.frontmatter.status} />
<span className="whitespace-normal">{release.frontmatter.title}</span>
</div>
<ChevronRight className={`w-4 h-4 flex-shrink-0 transition-opacity ${
isActive ? 'opacity-100' : 'opacity-0 group-hover:opacity-50'
}`} />
</Link>
{/* Table of Contents for active page */}
{isActive && toc.length > 0 && (
<div className="ml-4 mt-1 space-y-1 border-l-2 border-teal-200">
{toc.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className={`block py-1.5 text-xs text-slate-600 hover:text-teal-600 transition-colors ${
item.level === 2 ? 'pl-3 font-medium' : 'pl-6'
}`}
>
{item.text}
</a>
))}
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
})}
</nav>
</div>
)}
</aside>
{/* DESKTOP: Fixed sidebar */}
<aside className="hidden lg:block w-80 fixed top-16 bottom-0 bg-white border-r border-slate-200 overflow-y-auto">
<div className="p-6">
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
Documentatie
</h2>
<nav className="space-y-1">
{/* Overview link */}
<Link
href="/documentatie"
className={`flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors ${pathname === '/documentatie' && !isReleaseDetail
? 'bg-teal-50 text-teal-700'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
<span>Overzicht</span>
</Link>
{/* Grouped releases */}
{metadata?.groups && metadata.groups
.sort((a, b) => a.order - b.order)
.map((group) => {
const groupReleases = releasesByGroup[group.id] || []
const isExpanded = expandedGroups[group.id]
return (
<div key={group.id} className="space-y-1">
{/* Group header */}
<button
onClick={() => toggleGroup(group.id)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wide hover:text-slate-700 transition-colors"
>
<span>{group.title}</span>
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
{/* Group items */}
{isExpanded && (
<div className="space-y-1 ml-2">
{groupReleases.map((release) => {
const isActive = pathname === `/documentatie/${release.slug}`
const toc = tocMap[release.slug] || []
return (
<div key={release.slug}>
<Link
href={`/documentatie/${release.slug}`}
className={`flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors group ${isActive
? 'bg-teal-50 text-teal-700'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
<div className="flex items-center gap-2 min-w-0">
<StatusDot status={release.frontmatter.status} />
<span className="whitespace-normal">{release.frontmatter.title}</span>
</div>
<ChevronRight className={`w-4 h-4 flex-shrink-0 transition-opacity ${isActive ? 'opacity-100' : 'opacity-0 group-hover:opacity-50'
}`} />
</Link>
{/* Table of Contents for active page */}
{isActive && toc.length > 0 && (
<div className="ml-4 mt-1 space-y-1 border-l-2 border-teal-200">
{toc.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className={`block py-1.5 text-xs text-slate-600 hover:text-teal-600 transition-colors ${
item.level === 2 ? 'pl-3 font-medium' : 'pl-6'
}`}
>
{item.text}
</a>
))}
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
})}
</nav>
</div>
</aside>
</>
)
}
function StatusDot({ status }: { status: 'completed' | 'in_progress' | 'planned' }) {
const colors = {
completed: 'bg-teal-500',
in_progress: 'bg-amber-500',
planned: 'bg-slate-300',
}
return (
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${colors[status]}`} />
)
}

View File

@@ -1,51 +0,0 @@
/**
* Releases Layout
*
* Layout for release notes pages with sidebar navigation
*/
import type { ReactNode } from 'react'
import { getAllReleases, getCategoryMetadata, extractHeadings, type ReleaseNote, type TocItem } from '@/lib/mdx/documentatie'
import ReleaseSidebarWrapper from './components/release-sidebar-wrapper'
interface ReleasesLayoutProps {
children: ReactNode
}
export default async function ReleasesLayout({ children }: ReleasesLayoutProps) {
let releases: ReleaseNote[] = []
let metadata = { groups: [], categories: [] } as Awaited<ReturnType<typeof getCategoryMetadata>>
let tocMap: Record<string, TocItem[]> = {}
try {
releases = await getAllReleases()
metadata = await getCategoryMetadata()
// Extract headings for each release
tocMap = releases.reduce((acc, release) => {
acc[release.slug] = extractHeadings(release.content)
return acc
}, {} as Record<string, TocItem[]>)
} catch (error) {
console.error('Error loading releases or metadata:', error)
}
return (
<div className="min-h-screen bg-slate-50">
{/* Mobile sidebar is sticky, so we need padding for header only */}
<div className="pt-16 lg:pt-0">
<div className="max-w-[1600px] mx-auto">
<div className="flex">
{/* Sidebar - collapsible on mobile (48px collapsed), fixed on desktop (320px) */}
<ReleaseSidebarWrapper releases={releases} metadata={metadata} tocMap={tocMap} />
{/* Main Content - margin for collapsed sidebar on mobile, fixed sidebar on desktop */}
<div className="flex-1 ml-12 lg:ml-80">
{children}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,155 +0,0 @@
/**
* Releases Overview Page
*
* Landing page for all release notes grouped by category
*/
import Link from 'next/link'
import { getAllReleases } from '@/lib/mdx/documentatie'
export const metadata = {
title: 'Documentatie - AI Speedrun',
description: 'Feature documentatie van het Mini-ECD prototype. Transparant build-in-public overzicht van gebouwde functionaliteit.',
}
export default async function ReleasesPage() {
const releases = await getAllReleases()
// Group by status for better overview
const completed = releases.filter(r => r.frontmatter.status === 'completed')
const inProgress = releases.filter(r => r.frontmatter.status === 'in_progress')
const planned = releases.filter(r => r.frontmatter.status === 'planned')
return (
<div className="min-h-screen bg-slate-50 pb-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
<div className="mb-8 md:mb-12 pt-20 md:pt-20">
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-3 md:mb-4">
Feature Documentatie
</h1>
<p className="text-lg md:text-xl text-slate-600 max-w-3xl leading-relaxed">
Uitgebreide documentatie van alle gebouwde functionaliteit. Build in public met transparantie over features, implementatie en kosten.
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-12">
<div className="bg-white rounded-lg border border-slate-200 p-6 text-center shadow-sm">
<div className="text-4xl md:text-5xl font-bold text-teal-600 mb-2">{completed.length}</div>
<div className="text-sm font-medium text-slate-600">Voltooid</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6 text-center shadow-sm">
<div className="text-4xl md:text-5xl font-bold text-amber-600 mb-2">{inProgress.length}</div>
<div className="text-sm font-medium text-slate-600">In Progress</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6 text-center shadow-sm">
<div className="text-4xl md:text-5xl font-bold text-slate-400 mb-2">{planned.length}</div>
<div className="text-sm font-medium text-slate-600">Gepland</div>
</div>
</div>
{/* Completed Releases */}
{completed.length > 0 && (
<section className="mb-10 md:mb-12">
<h2 className="text-xl md:text-2xl font-bold text-slate-900 mb-4 md:mb-6">
Voltooid
</h2>
<div className="space-y-4">
{completed.map((release) => (
<ReleaseCard key={release.slug} release={release} />
))}
</div>
</section>
)}
{/* In Progress Releases */}
{inProgress.length > 0 && (
<section className="mb-10 md:mb-12">
<h2 className="text-xl md:text-2xl font-bold text-slate-900 mb-4 md:mb-6">
🔄 In Progress
</h2>
<div className="space-y-4">
{inProgress.map((release) => (
<ReleaseCard key={release.slug} release={release} />
))}
</div>
</section>
)}
{/* Planned Releases */}
{planned.length > 0 && (
<section className="mb-10 md:mb-12">
<h2 className="text-xl md:text-2xl font-bold text-slate-900 mb-4 md:mb-6">
Gepland
</h2>
<div className="space-y-4">
{planned.map((release) => (
<ReleaseCard key={release.slug} release={release} />
))}
</div>
</section>
)}
</div>
</div>
)
}
function ReleaseCard({ release }: { release: any }) {
return (
<Link
href={`/documentatie/${release.slug}`}
className="block bg-white rounded-lg border border-slate-200 p-5 md:p-6 hover:border-teal-300 hover:shadow-md transition-all active:scale-[0.98]"
>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 sm:gap-4">
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
<h3 className="text-lg md:text-xl font-bold text-slate-900 leading-tight">
{release.frontmatter.title}
</h3>
<StatusBadge status={release.frontmatter.status} />
</div>
<p className="text-slate-600 mb-3 text-sm md:text-base leading-relaxed">
{release.frontmatter.description}
</p>
<div className="flex flex-wrap items-center gap-2 md:gap-4 text-xs md:text-sm text-slate-500">
<span className="capitalize">{release.frontmatter.group.replace('-', ' ')}</span>
<span className="hidden sm:inline"></span>
<span>v{release.frontmatter.version}</span>
<span className="hidden sm:inline"></span>
<span className="whitespace-nowrap">{new Date(release.frontmatter.releaseDate).toLocaleDateString('nl-NL', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}</span>
</div>
</div>
<div className="text-slate-400 flex-shrink-0 self-start sm:self-center">
<svg className="w-5 h-5 md:w-6 md:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</Link>
)
}
function StatusBadge({ status }: { status: 'completed' | 'in_progress' | 'planned' }) {
const styles = {
completed: 'bg-teal-100 text-teal-700 border-teal-200',
in_progress: 'bg-amber-100 text-amber-700 border-amber-200',
planned: 'bg-slate-100 text-slate-600 border-slate-200',
}
const labels = {
completed: 'Voltooid',
in_progress: 'Bezig',
planned: 'Gepland',
}
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${styles[status]}`}>
{labels[status]}
</span>
)
}

View File

@@ -1,45 +0,0 @@
/**
* Marketing Layout
*
* Full-width layout without sidebar for marketing pages.
* Includes minimal navigation and reading progress bar.
*
* Performance optimizations:
* - ReadingProgress is a client component, automatically code-split by Next.js
*/
import type { ReactNode } from 'react'
import { getContent } from '@/lib/content/loader'
import type { NavigationContent } from '@/content/schemas/manifesto'
import { MinimalNav } from './components/minimal-nav'
import { ReadingProgress } from './components/reading-progress'
interface MarketingLayoutProps {
children: ReactNode
}
export default async function MarketingLayout({ children }: MarketingLayoutProps) {
// Load navigation content
const navigationContent = await getContent<NavigationContent>('nl', 'navigation')
return (
<div className="min-h-screen bg-white">
{/* Skip to main content link for accessibility */}
<a href="#main-content" className="skip-to-main">
Spring naar hoofdinhoud
</a>
{/* Minimal Navigation - fixed top */}
<MinimalNav content={navigationContent} />
{/* Reading progress bar - fixed top, below nav */}
{/* Client component, automatically code-split by Next.js */}
<ReadingProgress />
<main id="main-content" tabIndex={-1}>
{children}
</main>
</div>
)
}

View File

@@ -1,239 +0,0 @@
/**
* Homepage - AI Speedrun v2.1
*
* Vereenvoudigde landing page met:
* - Hero quote (Jensen Huang)
* - Statement section (Software on Demand concept)
* - Timeline (coming in E1.S3)
* - CTA naar /login
*
* Performance optimizations:
* - Critical content (Hero, Statement) loads immediately
* - Timeline will be lazy loaded when added
*/
import type { Metadata } from 'next'
import { Suspense } from 'react'
import { getContent } from '@/lib/content/loader'
import type { MetadataContent } from '@/content/schemas/manifesto'
import { HeroSectionClient } from './components/hero-section-client'
import { BuildTimeline } from './components/build-timeline'
import { WhyMe } from '@/components/ui/why-me'
import { AuthCodeHandler } from './components/auth-code-handler'
// Generate metadata for SEO
export async function generateMetadata(): Promise<Metadata> {
const metadataContent = await getContent<MetadataContent>('nl', 'metadata')
const meta = metadataContent.manifesto
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const ogImageUrl = `${siteUrl}${meta.ogImage}`
return {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
authors: [{ name: 'Colin van der Heijden' }],
openGraph: {
type: 'article',
title: meta.ogTitle,
description: meta.ogDescription,
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: meta.ogTitle,
},
],
siteName: 'AI Speedrun',
locale: 'nl_NL',
},
twitter: {
card: 'summary_large_image',
title: meta.ogTitle,
description: meta.ogDescription,
images: [ogImageUrl],
},
alternates: {
canonical: siteUrl,
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}
}
// Content interfaces
interface HeroContent {
title: {
main: string
accent: string
}
subtitle: string
punchline: {
question: string
answer: string
}
callToAction: {
primary: {
text: string
href: string
}
secondary: {
text: string
href: string
}
}
backgroundImage: string
}
interface StatementContent {
hero: HeroContent
}
interface Feature {
title: string
description: string
time?: string
traditional?: string
icon: "Rocket" | "Database" | "Sparkles" | "Palette" | "Calendar" | "Layout" | "Users" | "Smartphone" | "FileText" | "Brain" | "Tags" | "Target" | "HelpCircle" | "Zap" | "Eye"
}
interface WeekData {
weekNumber: number
title: string
status: "completed" | "in_progress" | "planned"
description: string
features: Feature[]
metrics: {
hours: string
cost: string
linesOfCode: string
}
achievements: string[]
}
interface TimelineContent {
heading: string
description: string
weeks: WeekData[]
}
export default async function HomePage() {
// Load content
const manifestoContent = await getContent<StatementContent>('nl', 'manifesto')
const timelineContent = await getContent<TimelineContent>('nl', 'timeline')
const heroContent = manifestoContent.hero
return (
<>
{/* Handle auth codes that Supabase redirects to home page */}
<Suspense fallback={null}>
<AuthCodeHandler />
</Suspense>
{/* Hero Section */}
<HeroSectionClient
title={
<>
{heroContent.title.main}{' '}
<span className="text-teal-600 block mt-2">
{heroContent.title.accent}
</span>
</>
}
subtitle={heroContent.subtitle}
punchline={
<>
{heroContent.punchline.question}
<br />
<br />
<strong>{heroContent.punchline.answer}</strong>
</>
}
callToAction={heroContent.callToAction.primary}
secondaryCallToAction={heroContent.callToAction.secondary}
backgroundImage={heroContent.backgroundImage}
/>
{/* Statement Section - Software on Demand */}
<section className="w-full md:max-w-[750px] mx-auto px-4 md:px-16 py-16 md:py-24">
<div className="prose prose-slate max-w-none">
<h2 className="font-serif text-3xl md:text-4xl font-bold text-slate-900 mb-8">
Software on Demand: Van 100k naar 200
</h2>
<div className="space-y-6 text-lg leading-relaxed text-slate-700">
<p>
<strong className="text-slate-900">Het probleem:</strong> Enterprise software kost 100.000+ per jaar
en implementaties duren 12-24 maanden. Je betaalt voor potentieel, niet voor werkelijke waarde.
Vendors optimaliseren voor meer seats, meer modules, meer lock-in.
</p>
<p>
<strong className="text-slate-900">De oplossing:</strong> AI-powered development verkort dit naar
4 weken en 200 totale kosten. Niet omdat AI magisch is, maar omdat je 80% van de standaard-code
niet meer hoeft te schrijven. De kostenbasis verschuift compleet - geen armies van consultants,
geen jarenlange developmenttrajecten.
</p>
<p>
<strong className="text-slate-900">Het bewijs:</strong> Dit EPD prototype is het levende bewijs.
Gebouwd in 4 weken, volledig transparant, build in public. Infrastructuur die schaalt met gebruik,
AI die de heavy lifting doet. En het belangrijkste: je bezit de code, je controleert de roadmap.
</p>
<p className="text-teal-700 font-medium text-xl pt-4">
Volg de voortgang hieronder en zie hoe elk onderdeel tot leven komt
</p>
</div>
</div>
</section>
{/* Timeline Section */}
<section id="timeline">
<BuildTimeline data={timelineContent} />
</section>
{/* About Me Section */}
<WhyMe />
{/* CTA Section */}
<section className="py-24 px-4 text-center bg-gradient-to-br from-slate-50 to-white">
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
Klaar om het prototype te proberen?
</h2>
<p className="text-lg text-slate-600 mb-8 max-w-2xl mx-auto">
Login om toegang te krijgen tot de EPD applicatie en zie AI-gestuurde workflows in actie
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="/login"
className="inline-block px-8 py-3 bg-teal-600 hover:bg-teal-700 text-white font-medium rounded-lg transition-colors"
>
Probeer het prototype
</a>
<a
href="#timeline"
className="inline-block px-8 py-3 bg-white hover:bg-slate-50 text-slate-700 font-medium rounded-lg border-2 border-slate-300 transition-colors"
>
Bekijk voortgang
</a>
</div>
</div>
</section>
</>
)
}

View File

@@ -1,116 +0,0 @@
/**
* Leads API Endpoint
*
* POST /api/leads - Submit contact form lead
*
* Security:
* - Rate limiting via headers
* - Input validation
* - Server-side Supabase client
*/
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/server'
import { z } from 'zod'
// Validation schema
const leadSchema = z.object({
name: z.string().min(2, 'Naam moet minimaal 2 karakters zijn'),
email: z.string().email('Ongeldig email adres'),
company: z.string().optional(),
projectType: z.string().min(1, 'Selecteer een project type'),
budget: z.string().optional(),
message: z.string().min(20, 'Beschrijving moet minimaal 20 karakters zijn'),
})
export type LeadFormData = z.infer<typeof leadSchema>
export async function POST(request: NextRequest) {
try {
// Parse request body
const body = await request.json()
// Validate input
const validatedData = leadSchema.parse(body)
// Get client IP and user agent for tracking
const ip = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown'
const userAgent = request.headers.get('user-agent') || 'unknown'
const referrer = request.headers.get('referer') || 'direct'
// Insert lead into database (using admin client for server-side insert)
const { data, error } = await supabaseAdmin
.from('leads')
.insert({
name: validatedData.name,
email: validatedData.email,
company: validatedData.company || null,
project_type: validatedData.projectType,
budget: validatedData.budget || null,
message: validatedData.message,
status: 'new',
ip_address: ip,
user_agent: userAgent,
referrer: referrer,
source: 'website',
})
.select()
.single()
if (error) {
console.error('Supabase error:', error)
return NextResponse.json(
{ error: 'Database error', details: error.message },
{ status: 500 }
)
}
// Return success
return NextResponse.json(
{
success: true,
message: 'Lead ontvangen',
leadId: data.id
},
{ status: 201 }
)
} catch (error) {
// Validation error
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'Validatie fout',
issues: error.issues.map(e => ({
field: e.path.join('.'),
message: e.message
}))
},
{ status: 400 }
)
}
// Generic error
console.error('Unexpected error:', error)
return NextResponse.json(
{ error: 'Interne server fout' },
{ status: 500 }
)
}
}
// OPTIONS for CORS (if needed)
export async function OPTIONS() {
return NextResponse.json(
{},
{
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
}
)
}

View File

@@ -1,31 +0,0 @@
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
/**
* Catch-all Redirect Route for /epd/clients/
*
* Redirects all /epd/clients/* routes to /epd/patients/* for backward compatibility
* This ensures that old bookmarks and links continue to work after migration.
*
* Preserves query parameters (e.g., ?tab=intake&search=test)
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params;
const searchParams = request.nextUrl.searchParams;
// Build new path with query parameters
const newPath = `/epd/patients/${path.join('/')}`;
const newUrl = new URL(newPath, request.url);
// Preserve all query parameters
searchParams.forEach((value, key) => {
newUrl.searchParams.set(key, value);
});
redirect(newUrl.toString());
}

View File

@@ -1,91 +0,0 @@
'use client';
import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { FileText, User, Target } from 'lucide-react';
import { IntakeTab } from './intake-tab';
import { ProfileTab } from './profile-tab';
import { PlanTab } from './plan-tab';
interface ClientTabsProps {
clientId: string;
activeTab?: string;
}
type TabId = 'intake' | 'profile' | 'plan';
const tabs = [
{
id: 'intake' as TabId,
label: 'Intake',
icon: FileText,
description: 'Intake gesprekken en notities',
},
{
id: 'profile' as TabId,
label: 'Profiel',
icon: User,
description: 'Probleemprofielen en DSM categorieën',
},
{
id: 'plan' as TabId,
label: 'Behandelplan',
icon: Target,
description: 'Behandeldoelen en interventies',
},
];
export function ClientTabs({ clientId, activeTab }: ClientTabsProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [currentTab, setCurrentTab] = useState<TabId>(
(activeTab as TabId) || 'intake'
);
const handleTabChange = (tabId: TabId) => {
setCurrentTab(tabId);
const params = new URLSearchParams(searchParams.toString());
params.set('tab', tabId);
router.push(`/epd/clients/${clientId}?${params.toString()}`);
};
return (
<div className="space-y-6">
{/* Tab Navigation */}
<div className="bg-white rounded-lg border border-slate-200 p-1">
<div className="flex gap-1">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = currentTab === tab.id;
return (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={`
flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-md text-sm font-medium transition-all
${
isActive
? 'bg-teal-50 text-teal-700 shadow-sm'
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
}
`}
>
<Icon className="h-4 w-4" />
<span className="hidden sm:inline">{tab.label}</span>
<span className="sm:hidden">{tab.label}</span>
</button>
);
})}
</div>
</div>
{/* Tab Content */}
<div className="bg-white rounded-lg border border-slate-200">
{currentTab === 'intake' && <IntakeTab clientId={clientId} />}
{currentTab === 'profile' && <ProfileTab clientId={clientId} />}
{currentTab === 'plan' && <PlanTab clientId={clientId} />}
</div>
</div>
);
}

View File

@@ -1,56 +0,0 @@
'use client';
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<Intake[]>([]);
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 (
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-slate-900">
Intakes
</h2>
<p className="text-sm text-slate-600 mt-1">
Overzicht van alle intakes
</p>
</div>
<Link
href={`/epd/clients/${clientId}/intakes/new`}
className="inline-flex items-center gap-2 px-4 py-2 bg-teal-600 text-white font-medium rounded-lg hover:bg-teal-700 transition-colors"
>
<Plus className="h-4 w-4" />
<span>Nieuwe Intake</span>
</Link>
</div>
<IntakeList intakes={intakes} clientId={clientId} isLoading={isLoading} />
</div>
);
}

View File

@@ -1,143 +0,0 @@
'use client';
import { Target, Sparkles, Clock } from 'lucide-react';
interface PlanTabProps {
clientId: string;
}
export function PlanTab({ clientId }: PlanTabProps) {
return (
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-slate-900">
Behandelplan
</h2>
<p className="text-sm text-slate-600 mt-1">
SMART doelen en interventies
</p>
</div>
<button
disabled
className="inline-flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-400 font-medium rounded-lg cursor-not-allowed"
title="Beschikbaar in Week 3"
>
<Sparkles className="h-4 w-4" />
<span>Genereer Plan</span>
</button>
</div>
{/* Coming Soon State */}
<div className="py-12 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-amber-50 mb-4">
<Target className="h-8 w-8 text-amber-500" />
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">
Coming Soon - Week 3
</h3>
<p className="text-sm text-slate-600 max-w-md mx-auto mb-6">
AI-gegenereerde behandelplannen met SMART doelen en evidence-based
interventies worden toegevoegd in Week 3.
</p>
{/* Feature Preview */}
<div className="max-w-2xl mx-auto">
<div className="bg-slate-50 rounded-lg border border-slate-200 p-6 text-left">
<h4 className="font-medium text-slate-900 mb-3">
🎯 Plan Structuur:
</h4>
<div className="space-y-4">
{/* SMART Doelen */}
<div className="p-4 bg-white rounded-lg border border-slate-200">
<h5 className="font-medium text-slate-800 mb-2">
1. SMART Doelen
</h5>
<p className="text-sm text-slate-600">
Specifieke, Meetbare, Acceptabele, Realistische en
Tijdgebonden behandeldoelen
</p>
</div>
{/* Interventies */}
<div className="p-4 bg-white rounded-lg border border-slate-200">
<h5 className="font-medium text-slate-800 mb-2">
2. Interventies
</h5>
<p className="text-sm text-slate-600">
Evidence-based behandelmethoden (CGT, ACT, EMDR, etc.)
</p>
</div>
{/* Frequentie */}
<div className="p-4 bg-white rounded-lg border border-slate-200">
<h5 className="font-medium text-slate-800 mb-2">
3. Frequentie
</h5>
<p className="text-sm text-slate-600">
Sessie planning en behandelintensiteit
</p>
</div>
{/* Meetmomenten */}
<div className="p-4 bg-white rounded-lg border border-slate-200">
<h5 className="font-medium text-slate-800 mb-2">
4. Meetmomenten
</h5>
<p className="text-sm text-slate-600">
Evaluatie en voortgangsmetingen
</p>
</div>
</div>
<h4 className="font-medium text-slate-900 mb-3 mt-6">
Geplande Features:
</h4>
<ul className="space-y-2 text-sm text-slate-700">
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>AI Plan Generator</strong> - Gebaseerd op intake +
profiel
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>Versioning</strong> - Meerdere versies per cliënt
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>Status Tracking</strong> - Concept vs. Gepubliceerd
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>JSONB Opslag</strong> - Flexibele datastructuur
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>Export Functie</strong> - PDF generatie voor dossier
</span>
</li>
</ul>
</div>
</div>
{/* Timeline Preview */}
<div className="mt-8 inline-flex items-center gap-2 px-4 py-2 bg-teal-50 border border-teal-200 rounded-full text-sm">
<Clock className="h-4 w-4 text-teal-600" />
<span className="text-teal-800">
<strong>Week 3:</strong> 18-24 November 2024
</span>
</div>
</div>
</div>
);
}

View File

@@ -1,122 +0,0 @@
'use client';
import { User, Brain, Clock } from 'lucide-react';
interface ProfileTabProps {
clientId: string;
}
export function ProfileTab({ clientId }: ProfileTabProps) {
return (
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-slate-900">
Probleemprofiel
</h2>
<p className="text-sm text-slate-600 mt-1">
DSM-light categorisatie en ernst indicatie
</p>
</div>
<button
disabled
className="inline-flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-400 font-medium rounded-lg cursor-not-allowed"
title="Beschikbaar in Week 3"
>
<Brain className="h-4 w-4" />
<span>AI Analyse</span>
</button>
</div>
{/* Coming Soon State */}
<div className="py-12 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-amber-50 mb-4">
<User className="h-8 w-8 text-amber-500" />
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">
Coming Soon - Week 3
</h3>
<p className="text-sm text-slate-600 max-w-md mx-auto mb-6">
AI-gestuurde probleemclassificatie met DSM-light categorieën wordt
toegevoegd in Week 3.
</p>
{/* Feature Preview */}
<div className="max-w-2xl mx-auto">
<div className="bg-slate-50 rounded-lg border border-slate-200 p-6 text-left">
<h4 className="font-medium text-slate-900 mb-3">
🎯 DSM-light Categorieën:
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div className="flex items-center gap-2 p-3 bg-white rounded-lg border border-slate-200">
<div className="h-2 w-2 rounded-full bg-blue-500" />
<span className="text-slate-700">Stemming & Depressie</span>
</div>
<div className="flex items-center gap-2 p-3 bg-white rounded-lg border border-slate-200">
<div className="h-2 w-2 rounded-full bg-purple-500" />
<span className="text-slate-700">Angst</span>
</div>
<div className="flex items-center gap-2 p-3 bg-white rounded-lg border border-slate-200">
<div className="h-2 w-2 rounded-full bg-red-500" />
<span className="text-slate-700">Gedrag & Impuls</span>
</div>
<div className="flex items-center gap-2 p-3 bg-white rounded-lg border border-slate-200">
<div className="h-2 w-2 rounded-full bg-orange-500" />
<span className="text-slate-700">Middelengebruik</span>
</div>
<div className="flex items-center gap-2 p-3 bg-white rounded-lg border border-slate-200">
<div className="h-2 w-2 rounded-full bg-green-500" />
<span className="text-slate-700">Cognitief</span>
</div>
<div className="flex items-center gap-2 p-3 bg-white rounded-lg border border-slate-200">
<div className="h-2 w-2 rounded-full bg-teal-500" />
<span className="text-slate-700">Context & Psychosociaal</span>
</div>
</div>
<h4 className="font-medium text-slate-900 mb-3 mt-6">
📊 Geplande Features:
</h4>
<ul className="space-y-2 text-sm text-slate-700">
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>AI Categorisatie</strong> - Automatische DSM-light
classificatie
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>Ernst Indicatie</strong> - Laag, Middel, Hoog scoring
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>Visuele Dashboard</strong> - Overzichtelijke
weergave per categorie
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-teal-600 font-bold"></span>
<span>
<strong>Bronverwijzing</strong> - Link naar intake notities
</span>
</li>
</ul>
</div>
</div>
{/* Timeline Preview */}
<div className="mt-8 inline-flex items-center gap-2 px-4 py-2 bg-teal-50 border border-teal-200 rounded-full text-sm">
<Clock className="h-4 w-4 text-teal-600" />
<span className="text-teal-800">
<strong>Week 3:</strong> 18-24 November 2024
</span>
</div>
</div>
</div>
);
}

View File

@@ -1,101 +0,0 @@
/**
* Client Dashboard - Level 2 (Client Dossier)
*
* Overzicht van client voortgang, recente activiteit en belangrijke metrics.
* Dit is de hoofd-dashboard pagina die opent wanneer je een client selecteert.
*/
import { notFound } from 'next/navigation';
import { ChevronLeft } from 'lucide-react';
import Link from 'next/link';
import { getClient } from '../../actions';
import { transformClient } from '@/lib/types/client';
import { ClientTabs } from '../components/client-tabs';
interface ClientDashboardPageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ tab?: string }>;
}
export default async function ClientDashboardPage({
params,
searchParams,
}: ClientDashboardPageProps) {
const { id } = await params;
const { tab } = await searchParams;
// Fetch client data
let client;
try {
client = await getClient(id);
} catch (error) {
notFound();
}
if (!client) {
notFound();
}
const clientWithAge = transformClient(client);
return (
<div className="min-h-full bg-slate-50">
{/* Header */}
<div className="bg-white border-b border-slate-200">
<div className="px-4 sm:px-6 lg:px-8 py-6">
{/* Breadcrumb */}
<Link
href="/epd/clients"
className="inline-flex items-center gap-2 text-sm text-slate-600 hover:text-slate-900 mb-4 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
<span>Terug naar cliënten</span>
</Link>
{/* Client Info */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
{/* Avatar */}
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center shadow-md">
<span className="text-white font-semibold text-xl">
{client.first_name[0]}
{client.last_name[0]}
</span>
</div>
{/* Name & Info */}
<div>
<h1 className="text-2xl font-bold text-slate-900">
{clientWithAge.full_name}
</h1>
<div className="flex items-center gap-4 mt-1 text-sm text-slate-600">
<span>{clientWithAge.age} jaar</span>
<span></span>
<span>
Geboren:{' '}
{new Date(client.birth_date).toLocaleDateString('nl-NL')}
</span>
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<Link
href={`/epd/clients/${client.id}/edit`}
className="inline-flex items-center gap-2 px-4 py-2 border border-slate-300 text-slate-700 font-medium rounded-lg hover:bg-slate-50 transition-colors"
>
Bewerken
</Link>
</div>
</div>
</div>
</div>
{/* Tab Content */}
<div className="px-4 sm:px-6 lg:px-8 py-6">
<ClientTabs clientId={client.id} activeTab={tab} />
</div>
</div>
);
}

View File

@@ -1,27 +0,0 @@
/**
* Diagnose & Probleemprofiel - Level 2 (Client Dossier)
*
* DSM-light categorieën met severity tracking.
*/
export default function DiagnosePage({ params }: { params: Promise<{ id: string }> }) {
return (
<div>
<h1 className="text-2xl font-bold text-slate-900 mb-6">
Diagnose & Probleemprofiel
</h1>
<div className="bg-slate-100 border-2 border-dashed border-slate-300 rounded-lg p-12 text-center">
<p className="text-slate-600 text-lg mb-2">
DSM-light Categorieën
</p>
<p className="text-slate-500 text-sm">
6 categorieën met severity indicators (Laag/Middel/Hoog)
</p>
<p className="text-xs text-slate-400 mt-4">
Epic 4 - Placeholder for future content
</p>
</div>
</div>
);
}

View File

@@ -1,49 +0,0 @@
import { notFound } from 'next/navigation';
import { ChevronLeft } from 'lucide-react';
import Link from 'next/link';
import { getClient } from '../../actions';
import { ClientForm } from '../../components/client-form';
interface EditClientPageProps {
params: Promise<{ id: string }>;
}
export default async function EditClientPage({ params }: EditClientPageProps) {
const { id } = await params;
// Fetch client data
let client;
try {
client = await getClient(id);
} catch (error) {
notFound();
}
if (!client) {
notFound();
}
return (
<div className="px-4 sm:px-6 lg:px-8 py-8 max-w-2xl mx-auto">
{/* Back Button */}
<Link
href={`/epd/clients/${id}`}
className="inline-flex items-center gap-2 text-sm text-slate-600 hover:text-slate-900 mb-6 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
<span>Terug naar cliënt</span>
</Link>
{/* Page Header */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-900">Cliënt bewerken</h1>
<p className="text-sm text-slate-600 mt-1">
Wijzig de gegevens van {client.first_name} {client.last_name}
</p>
</div>
{/* Form */}
<ClientForm client={client} />
</div>
);
}

View File

@@ -1,27 +0,0 @@
/**
* Intake Sectie - Level 2 (Client Dossier)
*
* TipTap editor voor intake notities met CRUD functionaliteit.
*/
export default function IntakePage({ params }: { params: Promise<{ id: string }> }) {
return (
<div>
<h1 className="text-2xl font-bold text-slate-900 mb-6">
Intakes
</h1>
<div className="bg-slate-100 border-2 border-dashed border-slate-300 rounded-lg p-12 text-center">
<p className="text-slate-600 text-lg mb-2">
Intake Notities
</p>
<p className="text-slate-500 text-sm">
TipTap editor, CRUD voor intakes, slide-in detail panel
</p>
<p className="text-xs text-slate-400 mt-4">
Epic 3 - Placeholder for future content
</p>
</div>
</div>
);
}

View File

@@ -1,65 +0,0 @@
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 (
<div className="bg-white border-b border-slate-200 px-6 py-4">
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="p-3 bg-teal-50 rounded-lg text-teal-600">
<FileText className="h-6 w-6" />
</div>
<div>
<div className="flex items-center gap-3 mb-1">
<h1 className="text-xl font-bold text-slate-900">{intake.title}</h1>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium border ${statusClass}`}>
{status}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-slate-500">
<span className="font-medium text-slate-700">{intake.department}</span>
<span className="text-slate-300">|</span>
<div className="flex items-center gap-1.5">
<Calendar className="h-4 w-4" />
<span>
Start: {format(new Date(intake.start_date), 'd MMM yyyy', { locale: nl })}
</span>
</div>
{intake.end_date && (
<div className="flex items-center gap-1.5">
<Clock className="h-4 w-4" />
<span>
Eind: {format(new Date(intake.end_date), 'd MMM yyyy', { locale: nl })}
</span>
</div>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{/* Placeholder for actions like Edit, Close, etc. */}
<button className="px-3 py-1.5 text-sm font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-md transition-colors">
Bewerken
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,52 +0,0 @@
'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 (
<div className="border-b border-slate-200 bg-white px-6">
<nav className="-mb-px flex space-x-6 overflow-x-auto">
{tabs.map((tab) => {
const isActive = tab.exact
? pathname === tab.href
: pathname.startsWith(tab.href);
return (
<Link
key={tab.name}
href={tab.href}
className={cn(
'whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium transition-colors',
isActive
? 'border-teal-500 text-teal-600'
: 'border-transparent text-slate-500 hover:border-slate-300 hover:text-slate-700'
)}
>
{tab.name}
</Link>
);
})}
</nav>
</div>
);
}

View File

@@ -1,29 +0,0 @@
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 (
<div className="flex flex-col h-full bg-slate-50">
<IntakeHeader intake={intake} />
<IntakeTabs clientId={id} intakeId={intakeId} />
<div className="flex-1 p-6 overflow-auto">
{children}
</div>
</div>
);
}

View File

@@ -1,52 +0,0 @@
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 (
<div className="max-w-3xl">
<div className="bg-white rounded-lg border border-slate-200 p-6 shadow-sm">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Algemene Informatie</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Titel</label>
<p className="text-slate-900">{intake.title}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Afdeling</label>
<p className="text-slate-900">{intake.department}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Status</label>
<p className="text-slate-900">{intake.status}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Startdatum</label>
<p className="text-slate-900">{intake.start_date}</p>
</div>
</div>
<div className="mt-6 pt-6 border-t border-slate-100">
<label className="block text-sm font-medium text-slate-500 mb-2">Notities</label>
<div className="bg-slate-50 rounded-md p-4 text-slate-600 text-sm min-h-[100px]">
{intake.notes || 'Geen notities beschikbaar.'}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,83 +0,0 @@
'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<typeof CreateIntakeSchema>;
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;
}

View File

@@ -1,70 +0,0 @@
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 (
<Link
href={`/epd/clients/${clientId}/intakes/${intake.id}`}
className="block group"
>
<div className="bg-white border border-slate-200 rounded-lg p-4 hover:border-teal-500 hover:shadow-sm transition-all">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="p-2 bg-teal-50 rounded-md text-teal-600 group-hover:bg-teal-100 transition-colors">
<FileText className="h-5 w-5" />
</div>
<div>
<h3 className="font-medium text-slate-900 group-hover:text-teal-700 transition-colors">
{intake.title}
</h3>
<p className="text-sm text-slate-500">{intake.department}</p>
</div>
</div>
<span
className={`px-2.5 py-0.5 rounded-full text-xs font-medium border ${statusClass}`}
>
{status}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-slate-500 mt-4 pt-4 border-t border-slate-100">
<div className="flex items-center gap-1.5">
<Calendar className="h-4 w-4" />
<span>
{format(new Date(intake.start_date), 'd MMM yyyy', { locale: nl })}
</span>
</div>
{intake.end_date && (
<>
<span>&rarr;</span>
<span>
{format(new Date(intake.end_date), 'd MMM yyyy', { locale: nl })}
</span>
</>
)}
<div className="ml-auto">
<ChevronRight className="h-4 w-4 text-slate-300 group-hover:text-teal-500 transition-colors" />
</div>
</div>
</div>
</Link>
);
}

View File

@@ -1,50 +0,0 @@
'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 (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[1, 2].map((i) => (
<div
key={i}
className="h-32 bg-slate-50 rounded-lg border border-slate-200 animate-pulse"
/>
))}
</div>
);
}
if (intakes.length === 0) {
return (
<div className="text-center py-12 bg-slate-50 rounded-lg border border-dashed border-slate-300">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 mb-4">
<FileText className="h-6 w-6 text-slate-400" />
</div>
<h3 className="text-sm font-medium text-slate-900 mb-1">
Geen intakes gevonden
</h3>
<p className="text-sm text-slate-500">
Start een nieuwe intake om te beginnen.
</p>
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{intakes.map((intake) => (
<IntakeCard key={intake.id} intake={intake} clientId={clientId} />
))}
</div>
);
}

View File

@@ -1,137 +0,0 @@
'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<typeof formSchema>;
interface NewIntakeFormProps {
clientId: string;
}
export function NewIntakeForm({ clientId }: NewIntakeFormProps) {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
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 (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 max-w-md">
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
{error}
</div>
)}
<div className="space-y-2">
<label htmlFor="title" className="text-sm font-medium text-slate-900">
Titel Intake
</label>
<input
id="title"
type="text"
{...register('title')}
placeholder="Bijv. Intake Depressie"
className={cn(
"flex h-10 w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50",
errors.title && "border-red-500 focus:ring-red-500"
)}
disabled={isPending}
/>
{errors.title && (
<p className="text-xs text-red-500">{errors.title.message}</p>
)}
</div>
<div className="space-y-2">
<label htmlFor="department" className="text-sm font-medium text-slate-900">
Afdeling
</label>
<select
id="department"
{...register('department')}
className="flex h-10 w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50"
disabled={isPending}
>
<option value="Volwassenen">Volwassenen</option>
<option value="Jeugd">Jeugd</option>
<option value="Ouderen">Ouderen</option>
</select>
{errors.department && (
<p className="text-xs text-red-500">{errors.department.message}</p>
)}
</div>
<div className="space-y-2">
<label htmlFor="start_date" className="text-sm font-medium text-slate-900">
Startdatum
</label>
<div className="relative">
<input
id="start_date"
type="date"
{...register('start_date')}
className={cn(
"flex h-10 w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50",
errors.start_date && "border-red-500 focus:ring-red-500"
)}
disabled={isPending}
/>
</div>
{errors.start_date && (
<p className="text-xs text-red-500">{errors.start_date.message}</p>
)}
</div>
<div className="pt-4">
<button
type="submit"
disabled={isPending}
className="w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-teal-600 text-white font-medium rounded-lg hover:bg-teal-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isPending && <Loader2 className="h-4 w-4 animate-spin" />}
{isPending ? 'Aanmaken...' : 'Start Intake'}
</button>
</div>
</form>
);
}

View File

@@ -1,33 +0,0 @@
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 (
<div className="max-w-2xl mx-auto py-8">
<div className="mb-8">
<Link
href={`/epd/clients/${id}?tab=intake`}
className="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-900 mb-4 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
Terug naar overzicht
</Link>
<h1 className="text-2xl font-bold text-slate-900">Nieuwe Intake Starten</h1>
<p className="text-slate-600 mt-2">
Vul de basisgegevens in om een nieuwe intake te starten.
</p>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6 shadow-sm">
<NewIntakeForm clientId={id} />
</div>
</div>
);
}

View File

@@ -1,18 +0,0 @@
import { redirect } from 'next/navigation';
interface ClientDetailPageProps {
params: Promise<{ id: string }>;
}
/**
* Redirect route: /epd/clients/[id] -> /epd/clients/[id]/dashboard
*
* Wanneer een gebruiker direct naar /epd/clients/[id] navigeert,
* wordt deze automatisch doorgestuurd naar het dashboard.
*/
export default async function ClientRedirect({
params,
}: ClientDetailPageProps) {
const { id } = await params;
redirect(`/epd/clients/${id}/dashboard`);
}

View File

@@ -1,27 +0,0 @@
/**
* Behandelplan - Level 2 (Client Dossier)
*
* SMART doelen tracking met interventies en versioning.
*/
export default function PlanPage({ params }: { params: Promise<{ id: string }> }) {
return (
<div>
<h1 className="text-2xl font-bold text-slate-900 mb-6">
Behandelplan
</h1>
<div className="bg-slate-100 border-2 border-dashed border-slate-300 rounded-lg p-12 text-center">
<p className="text-slate-600 text-lg mb-2">
SMART Doelen & Interventies
</p>
<p className="text-slate-500 text-sm">
Treatment plan met versioning (v1, v2, concept/gepubliceerd)
</p>
<p className="text-xs text-slate-400 mt-4">
Epic 5 - Placeholder for future content
</p>
</div>
</div>
);
}

View File

@@ -1,27 +0,0 @@
/**
* Client Rapportage - Level 2 (Client Dossier)
*
* Client-specifieke voortgang en metrics.
*/
export default function ClientReportsPage({ params }: { params: Promise<{ id: string }> }) {
return (
<div>
<h1 className="text-2xl font-bold text-slate-900 mb-6">
Rapportage & Voortgang
</h1>
<div className="bg-slate-100 border-2 border-dashed border-slate-300 rounded-lg p-12 text-center">
<p className="text-slate-600 text-lg mb-2">
Client Voortgang
</p>
<p className="text-slate-500 text-sm">
Behandelduur, sessies, doelvoortgang tracking
</p>
<p className="text-xs text-slate-400 mt-4">
Placeholder - Not designed yet
</p>
</div>
</div>
);
}

View File

@@ -1,146 +0,0 @@
'use server';
/**
* Client CRUD Server Actions
*
* Server-side actions for client management
*/
import { revalidatePath } from 'next/cache';
import { createClient as createSupabaseClient } from '@/lib/auth/server';
import type { ClientFormData, ClientFilters } from '@/lib/types/client';
/**
* Get all clients with optional filtering
*/
export async function getClients(filters?: ClientFilters) {
const supabase = await createSupabaseClient();
let query = supabase
.from('clients')
.select('*');
// Apply search filter
if (filters?.search) {
const searchTerm = filters.search.trim();
// Use PostgREST or() syntax: column.operator.value,column.operator.value
// Format: column.operator.value,column.operator.value (no quotes needed for ilike)
const searchPattern = `%${searchTerm}%`;
query = query.or(`first_name.ilike.${searchPattern},last_name.ilike.${searchPattern}`);
}
// Apply sorting
const sortBy = filters?.sortBy || 'created_at';
const sortOrder = filters?.sortOrder || 'desc';
if (sortBy === 'name') {
query = query.order('last_name', { ascending: sortOrder === 'asc' });
query = query.order('first_name', { ascending: sortOrder === 'asc' });
} else if (sortBy === 'created_at') {
query = query.order('created_at', { ascending: sortOrder === 'asc' });
} else if (sortBy === 'age') {
// Sort by birth_date (newest birth = youngest age)
query = query.order('birth_date', { ascending: sortOrder === 'desc' });
}
const { data, error } = await query;
if (error) {
console.error('Error fetching clients:', error);
throw new Error('Failed to fetch clients');
}
return data || [];
}
/**
* Get single client by ID
*/
export async function getClient(id: string) {
const supabase = await createSupabaseClient();
const { data, error } = await supabase
.from('clients')
.select('*')
.eq('id', id)
.single();
if (error) {
console.error('Error fetching client:', error);
throw new Error('Failed to fetch client');
}
return data;
}
/**
* Create new client
*/
export async function createClient(formData: ClientFormData) {
const supabase = await createSupabaseClient();
const { data, error } = await supabase
.from('clients')
.insert({
first_name: formData.first_name.trim(),
last_name: formData.last_name.trim(),
birth_date: formData.birth_date,
})
.select()
.single();
if (error) {
console.error('Error creating client:', error);
throw new Error('Failed to create client');
}
revalidatePath('/epd/clients');
return data;
}
/**
* Update existing client
*/
export async function updateClient(id: string, formData: ClientFormData) {
const supabase = await createSupabaseClient();
const { data, error } = await supabase
.from('clients')
.update({
first_name: formData.first_name.trim(),
last_name: formData.last_name.trim(),
birth_date: formData.birth_date,
})
.eq('id', id)
.select()
.single();
if (error) {
console.error('Error updating client:', error);
throw new Error('Failed to update client');
}
revalidatePath('/epd/clients');
revalidatePath(`/epd/clients/${id}`);
return data;
}
/**
* Delete client
*/
export async function deleteClient(id: string) {
const supabase = await createSupabaseClient();
const { error } = await supabase
.from('clients')
.delete()
.eq('id', id);
if (error) {
console.error('Error deleting client:', error);
throw new Error('Failed to delete client');
}
revalidatePath('/epd/clients');
return { success: true };
}

View File

@@ -1,191 +0,0 @@
import { CheckCircle2, Circle, Clock, Rocket } from "lucide-react"
export default function ComingSoonPage() {
// Roadmap items from bouwplan v2.1
const roadmapItems = [
{
week: "Week 1",
status: "in-progress",
title: "Foundation & Marketing",
items: [
{ done: true, text: "Project Setup - Next.js + Supabase" },
{ done: true, text: "Design System - Teal-first kleuren" },
{ done: true, text: "App Layout - Header + Sidebar" },
{ done: false, text: "Marketing Website - Timeline + Features" },
],
},
{
week: "Week 2",
status: "upcoming",
title: "EPD Core",
items: [
{ done: false, text: "Database Schema + RLS Policies" },
{ done: false, text: "Client Module - CRUD Operations" },
{ done: false, text: "Client Detail Page" },
],
},
{
week: "Week 3",
status: "upcoming",
title: "AI Magic",
items: [
{ done: false, text: "TipTap Rich Text Editor" },
{ done: false, text: "Claude API - Intake Samenvatting" },
{ done: false, text: "AI Profiel + Behandelplan Generator" },
],
},
{
week: "Week 4",
status: "upcoming",
title: "Polish & Launch",
items: [
{ done: false, text: "Onboarding System" },
{ done: false, text: "Performance Optimization" },
{ done: false, text: "Demo Preparation + LinkedIn Launch" },
],
},
]
return (
<div className="px-4 sm:px-6 lg:px-8 py-8 max-w-5xl mx-auto">
{/* Hero Section */}
<div className="text-center mb-16">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-br from-amber-500 to-amber-600 mb-6 shadow-lg">
<Rocket className="h-8 w-8 text-white" />
</div>
<h1 className="text-4xl sm:text-5xl font-bold text-slate-900 mb-4">
Coming Soon
</h1>
<p className="text-xl text-slate-600 max-w-2xl mx-auto mb-2">
Het EPD wordt gebouwd in <span className="font-semibold text-teal-700">4 weken</span>
</p>
<p className="text-sm text-slate-500 mb-6">
Van 100.000+ en 12-24 maanden <span className="font-mono font-semibold text-amber-700">200 + 4 weken</span>
</p>
{/* Build in Public Badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 bg-teal-50 border border-teal-200 rounded-full">
<div className="h-2 w-2 rounded-full bg-teal-500 animate-pulse" />
<span className="text-sm font-medium text-teal-800">
Building in Public
</span>
</div>
</div>
{/* Roadmap */}
<div className="space-y-8">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-slate-900 mb-2">
4-Weken Roadmap
</h2>
<p className="text-slate-600">
Volg de voortgang van dit experiment
</p>
</div>
{roadmapItems.map((week, idx) => (
<div
key={week.week}
className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm hover:shadow-md transition-shadow"
>
{/* Week Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className={`h-10 w-10 rounded-lg flex items-center justify-center font-mono text-sm font-semibold ${
week.status === "in-progress"
? "bg-gradient-to-br from-amber-500 to-amber-600 text-white"
: week.status === "upcoming"
? "bg-slate-100 text-slate-400"
: "bg-gradient-to-br from-teal-600 to-teal-700 text-white"
}`}
>
W{idx + 1}
</div>
<div>
<h3 className="text-lg font-semibold text-slate-900">
{week.week}
</h3>
<p className="text-sm text-slate-600">{week.title}</p>
</div>
</div>
<StatusBadge status={week.status} />
</div>
{/* Items Checklist */}
<ul className="space-y-2">
{week.items.map((item, itemIdx) => (
<li key={itemIdx} className="flex items-start gap-3">
{item.done ? (
<CheckCircle2 className="h-5 w-5 text-teal-600 flex-shrink-0 mt-0.5" />
) : (
<Circle className="h-5 w-5 text-slate-300 flex-shrink-0 mt-0.5" />
)}
<span
className={`text-sm ${
item.done
? "text-slate-700 line-through"
: "text-slate-600"
}`}
>
{item.text}
</span>
</li>
))}
</ul>
</div>
))}
</div>
{/* Footer CTA */}
<div className="mt-12 text-center">
<p className="text-slate-600 mb-4">
Volg de build op LinkedIn voor real-time updates
</p>
<a
href="https://linkedin.com/in/colinlit"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-teal-600 to-teal-700 hover:from-teal-700 hover:to-teal-800 text-white font-medium rounded-lg shadow-md hover:shadow-lg transition-all"
>
<span>Volg op LinkedIn</span>
<span></span>
</a>
</div>
</div>
)
}
function StatusBadge({ status }: { status: string }) {
const config = {
"in-progress": {
bg: "bg-amber-50 border-amber-200",
text: "text-amber-700",
label: "In Progress",
icon: Clock,
},
upcoming: {
bg: "bg-slate-50 border-slate-200",
text: "text-slate-600",
label: "Upcoming",
icon: Circle,
},
completed: {
bg: "bg-teal-50 border-teal-200",
text: "text-teal-700",
label: "Completed",
icon: CheckCircle2,
},
}
const { bg, text, label, icon: Icon } = config[status as keyof typeof config]
return (
<div
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full border text-xs font-medium ${bg} ${text}`}
>
<Icon className="h-3.5 w-3.5" />
{label}
</div>
)
}

View File

@@ -1,149 +0,0 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Save, Loader2 } from 'lucide-react';
import type { ClientFormData } from '@/lib/types/client';
import type { Client } from '@/lib/types/client';
import { createClient, updateClient } from '../actions';
interface ClientFormProps {
client?: Client;
}
export function ClientForm({ client }: ClientFormProps) {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState<ClientFormData>({
first_name: client?.first_name || '',
last_name: client?.last_name || '',
birth_date: client?.birth_date || '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setIsSubmitting(true);
try {
if (client) {
await updateClient(client.id, formData);
router.push(`/epd/clients/${client.id}`);
} else {
const newClient = await createClient(formData);
router.push(`/epd/clients/${newClient.id}`);
}
router.refresh();
} catch (err) {
console.error('Form submission error:', err);
setError('Er is een fout opgetreden. Probeer het opnieuw.');
} finally {
setIsSubmitting(false);
}
};
const handleChange = (field: keyof ClientFormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
{/* Form Card */}
<div className="bg-white rounded-lg border border-slate-200 p-6 space-y-6">
{/* First Name */}
<div>
<label
htmlFor="first_name"
className="block text-sm font-medium text-slate-700 mb-2"
>
Voornaam <span className="text-red-500">*</span>
</label>
<input
type="text"
id="first_name"
required
value={formData.first_name}
onChange={(e) => handleChange('first_name', e.target.value)}
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
placeholder="bijv. Jan"
/>
</div>
{/* Last Name */}
<div>
<label
htmlFor="last_name"
className="block text-sm font-medium text-slate-700 mb-2"
>
Achternaam <span className="text-red-500">*</span>
</label>
<input
type="text"
id="last_name"
required
value={formData.last_name}
onChange={(e) => handleChange('last_name', e.target.value)}
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
placeholder="bijv. de Vries"
/>
</div>
{/* Birth Date */}
<div>
<label
htmlFor="birth_date"
className="block text-sm font-medium text-slate-700 mb-2"
>
Geboortedatum <span className="text-red-500">*</span>
</label>
<input
type="date"
id="birth_date"
required
value={formData.birth_date}
onChange={(e) => handleChange('birth_date', e.target.value)}
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
</div>
{/* Form Actions */}
<div className="flex items-center justify-end gap-3">
<button
type="button"
onClick={() => router.back()}
disabled={isSubmitting}
className="px-6 py-2 border border-slate-300 text-slate-700 font-medium rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
>
Annuleren
</button>
<button
type="submit"
disabled={isSubmitting}
className="inline-flex items-center gap-2 px-6 py-2 bg-gradient-to-r from-teal-600 to-teal-700 hover:from-teal-700 hover:to-teal-800 text-white font-medium rounded-lg shadow-sm hover:shadow-md transition-all disabled:opacity-50"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<span>Bezig...</span>
</>
) : (
<>
<Save className="h-4 w-4" />
<span>{client ? 'Bijwerken' : 'Opslaan'}</span>
</>
)}
</button>
</div>
</form>
);
}

View File

@@ -1,56 +0,0 @@
'use client'
export function ClientListSkeleton() {
return (
<div className="space-y-4">
{/* Search Bar Skeleton */}
<div className="bg-white rounded-lg border border-slate-200 p-4">
<div className="h-10 bg-slate-100 rounded-lg animate-pulse" />
</div>
{/* Table Skeleton - Desktop */}
<div className="hidden md:block bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="p-4 space-y-3">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-slate-100 animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-slate-100 rounded w-1/4 animate-pulse" />
<div className="h-3 bg-slate-100 rounded w-1/6 animate-pulse" />
</div>
<div className="flex gap-2">
<div className="h-8 w-8 bg-slate-100 rounded animate-pulse" />
<div className="h-8 w-8 bg-slate-100 rounded animate-pulse" />
<div className="h-8 w-8 bg-slate-100 rounded animate-pulse" />
</div>
</div>
))}
</div>
</div>
{/* Card Skeleton - Mobile */}
<div className="md:hidden space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-white rounded-lg border border-slate-200 p-4">
<div className="flex items-center gap-3 mb-3">
<div className="h-12 w-12 rounded-full bg-slate-100 animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-slate-100 rounded w-3/4 animate-pulse" />
<div className="h-3 bg-slate-100 rounded w-1/2 animate-pulse" />
</div>
</div>
<div className="space-y-2 mb-3">
<div className="h-3 bg-slate-100 rounded animate-pulse" />
<div className="h-3 bg-slate-100 rounded animate-pulse" />
</div>
<div className="flex gap-2">
<div className="flex-1 h-10 bg-slate-100 rounded-lg animate-pulse" />
<div className="flex-1 h-10 bg-slate-100 rounded-lg animate-pulse" />
<div className="h-10 w-10 bg-slate-100 rounded-lg animate-pulse" />
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -1,260 +0,0 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Search, ArrowUpDown, Eye, Edit2, Trash2, Users } from 'lucide-react';
import type { Client } from '@/lib/types/client';
import { transformClient } from '@/lib/types/client';
import { deleteClient } from '../actions';
interface ClientListProps {
initialClients: Client[];
}
export function ClientList({ initialClients }: ClientListProps) {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState('');
const [isDeleting, setIsDeleting] = useState<string | null>(null);
// Transform clients with computed fields
const clients = initialClients.map(transformClient);
// Client-side filtering (for instant feedback)
const filteredClients = clients.filter((client) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
client.first_name.toLowerCase().includes(query) ||
client.last_name.toLowerCase().includes(query) ||
client.full_name.toLowerCase().includes(query)
);
});
const handleSearch = (value: string) => {
setSearchQuery(value);
// Update URL with search param
const params = new URLSearchParams();
if (value) params.set('search', value);
router.push(`/epd/clients?${params.toString()}`);
};
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Weet u zeker dat u ${name} wilt verwijderen?`)) {
return;
}
setIsDeleting(id);
try {
await deleteClient(id);
router.refresh();
} catch (error) {
console.error('Error deleting client:', error);
alert('Fout bij verwijderen van cliënt');
} finally {
setIsDeleting(null);
}
};
return (
<div className="space-y-4">
{/* Search Bar */}
<div className="bg-white rounded-lg border border-slate-200 p-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
<input
type="text"
placeholder="Zoek op naam..."
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
</div>
</div>
{/* Empty State */}
{filteredClients.length === 0 && (
<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-slate-100 mb-4">
<Users className="h-8 w-8 text-slate-400" />
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-1">
{searchQuery ? 'Geen resultaten' : 'Geen cliënten'}
</h3>
<p className="text-sm text-slate-600 mb-4">
{searchQuery
? 'Probeer een andere zoekopdracht'
: 'Voeg uw eerste cliënt toe om te beginnen'}
</p>
{!searchQuery && (
<Link
href="/epd/clients/new"
className="inline-flex items-center gap-2 px-4 py-2 bg-teal-600 hover:bg-teal-700 text-white font-medium rounded-lg transition-colors"
>
<span>Nieuwe cliënt toevoegen</span>
</Link>
)}
</div>
)}
{/* Desktop Table View */}
{filteredClients.length > 0 && (
<div className="hidden md:block bg-white rounded-lg border border-slate-200 overflow-hidden">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Naam
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Leeftijd
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Geboortedatum
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Toegevoegd
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">
Acties
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-slate-200">
{filteredClients.map((client) => (
<tr
key={client.id}
onClick={() => router.push(`/epd/clients/${client.id}`)}
className="hover:bg-slate-50 transition-colors cursor-pointer"
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="h-10 w-10 flex-shrink-0">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center">
<span className="text-white font-medium text-sm">
{client.first_name[0]}
{client.last_name[0]}
</span>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-slate-900">
{client.full_name}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-600">
{client.age} jaar
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-600">
{new Date(client.birth_date).toLocaleDateString('nl-NL')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-600">
{new Date(client.created_at).toLocaleDateString('nl-NL')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div
className="flex items-center justify-end gap-2"
onClick={(e) => e.stopPropagation()}
>
<Link
href={`/epd/clients/${client.id}`}
className="text-teal-600 hover:text-teal-900 p-1 rounded hover:bg-teal-50 transition-colors"
title="Bekijken"
>
<Eye className="h-4 w-4" />
</Link>
<Link
href={`/epd/clients/${client.id}/edit`}
className="text-slate-600 hover:text-slate-900 p-1 rounded hover:bg-slate-50 transition-colors"
title="Bewerken"
>
<Edit2 className="h-4 w-4" />
</Link>
<button
onClick={() => handleDelete(client.id, client.full_name)}
disabled={isDeleting === client.id}
className="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50 transition-colors disabled:opacity-50"
title="Verwijderen"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Mobile Card View */}
{filteredClients.length > 0 && (
<div className="md:hidden space-y-3">
{filteredClients.map((client) => (
<div
key={client.id}
className="bg-white rounded-lg border border-slate-200 p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center">
<span className="text-white font-medium">
{client.first_name[0]}
{client.last_name[0]}
</span>
</div>
<div>
<h3 className="font-medium text-slate-900">
{client.full_name}
</h3>
<p className="text-sm text-slate-600">
{client.age} jaar
</p>
</div>
</div>
</div>
<div className="space-y-2 mb-3 text-sm text-slate-600">
<div className="flex justify-between">
<span>Geboortedatum:</span>
<span>{new Date(client.birth_date).toLocaleDateString('nl-NL')}</span>
</div>
<div className="flex justify-between">
<span>Toegevoegd:</span>
<span>{new Date(client.created_at).toLocaleDateString('nl-NL')}</span>
</div>
</div>
<div className="flex items-center gap-2 pt-3 border-t border-slate-200">
<Link
href={`/epd/clients/${client.id}`}
className="flex-1 inline-flex items-center justify-center gap-2 px-3 py-2 bg-teal-50 text-teal-700 font-medium rounded-lg hover:bg-teal-100 transition-colors"
>
<Eye className="h-4 w-4" />
<span>Bekijken</span>
</Link>
<Link
href={`/epd/clients/${client.id}/edit`}
className="flex-1 inline-flex items-center justify-center gap-2 px-3 py-2 bg-slate-50 text-slate-700 font-medium rounded-lg hover:bg-slate-100 transition-colors"
>
<Edit2 className="h-4 w-4" />
<span>Bewerken</span>
</Link>
<button
onClick={() => handleDelete(client.id, client.full_name)}
disabled={isDeleting === client.id}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50"
title="Verwijderen"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -1,29 +0,0 @@
import { ChevronLeft } from 'lucide-react';
import Link from 'next/link';
import { ClientForm } from '../components/client-form';
export default function NewClientPage() {
return (
<div className="px-4 sm:px-6 lg:px-8 py-8 max-w-2xl mx-auto">
{/* Back Button */}
<Link
href="/epd/clients"
className="inline-flex items-center gap-2 text-sm text-slate-600 hover:text-slate-900 mb-6 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
<span>Terug naar overzicht</span>
</Link>
{/* Page Header */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-900">Nieuwe cliënt</h1>
<p className="text-sm text-slate-600 mt-1">
Voeg een nieuwe cliënt toe aan het systeem
</p>
</div>
{/* Form */}
<ClientForm />
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { redirect } from 'next/navigation';
/**
* Clients Root Redirect
* Redirects /epd/clients to /epd/patients for backward compatibility
*/
export default function ClientsRedirect() {
redirect('/epd/patients');
}

View File

@@ -1,248 +0,0 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
/* =============================================================================
Mini-ECD Design System
Gebaseerd op ux-stylesheet.md (v2, UX-geoptimaliseerd)
============================================================================= */
:root {
/* Basiskleuren */
--color-bg: #F8FAFC;
--color-surface: #FFFFFF;
--color-surface-secondary: #F1F5F9;
--color-text: #0F172A;
--color-text-secondary: #475569;
--color-border: #E2E8F0;
/* Brand & Primary */
--color-brand: #3B82F6;
--color-brand-hover: #2563EB;
--color-brand-active: #1D4ED8;
--color-brand-subtle: #EFF6FF;
/* Neutral CTA */
--color-neutral: #334155;
--color-neutral-hover: #1F2937;
/* Module-accenten */
--color-module-appointments: #16A34A;
--color-module-appointments-bg: #E8F8EF;
--color-module-appointments-border: #CDECDC;
--color-module-meds: #F59E0B;
--color-module-meds-bg: #FEF6DC;
--color-module-meds-border: #F6E7B6;
--color-module-labs: #F97316;
--color-module-labs-bg: #FFEBDC;
--color-module-labs-border: #FFD2B8;
/* Status & Feedback */
--color-success: #16A34A;
--color-success-subtle: #ECFDF5;
--color-warning: #EAB308;
--color-warning-subtle: #FEFCE8;
--color-error: #DC2626;
--color-error-subtle: #FEF2F2;
--color-info: #3B82F6;
--color-info-subtle: #EFF6FF;
/* Badges (Severity DSM-light) */
--color-badge-low-bg: #E5E7EB;
--color-badge-low-text: #374151;
--color-badge-medium-bg: #FEF3C7;
--color-badge-medium-text: #92400E;
--color-badge-high-bg: #FEE2E2;
--color-badge-high-text: #991B1B;
/* Formulieren */
--color-input-bg: #FFFFFF;
--color-input-text: #0F172A;
--color-input-placeholder: #94A3B8;
--color-input-border: #CBD5E1;
--color-input-border-hover: #94A3B8;
--color-input-focus: #3B82F6;
--color-input-focus-border: #2563EB;
--color-input-disabled-bg: #F1F5F9;
--color-input-disabled-text: #94A3B8;
--color-input-invalid-border: #DC2626;
--color-input-invalid-text: #B91C1C;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
--shadow-md: 0 2px 6px rgba(15, 23, 42, 0.08);
--shadow-lg: 0 8px 20px rgba(15, 23, 42, 0.10);
/* Fonts */
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
@theme inline {
/* Basiskleuren */
--color-bg: var(--color-bg);
--color-surface: var(--color-surface);
--color-text: var(--color-text);
--color-border: var(--color-border);
/* Brand */
--color-brand: var(--color-brand);
--color-success: var(--color-success);
--color-warning: var(--color-warning);
--color-error: var(--color-error);
--color-info: var(--color-info);
/* Shadows */
--shadow-sm: var(--shadow-sm);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
/* Fonts */
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
/* Dark mode (optioneel, MVP gebruikt licht thema) */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0a0a0a;
--color-surface: #1a1a1a;
--color-text: #ededed;
--color-text-secondary: #a1a1a1;
--color-border: #333333;
}
}
/* Global styles */
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans), system-ui, -apple-system, sans-serif;
}
/* Focus styles (toegankelijkheid) */
*:focus-visible {
outline: 2px solid var(--color-brand);
outline-offset: 2px;
}
/* AI source highlighting (gebruikt in Intake editor) */
.ai-source-highlight {
background-color: #fef08a; /* lichtgeel */
border-radius: 2px;
padding: 1px 0;
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

5
app/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation'
export default function RootPage() {
redirect('/login')
}

View File

@@ -1,36 +1,18 @@
/** /**
* Robots.txt Generator * Robots.txt Generator
* *
* Generates robots.txt for SEO. * ECD is een afgeschermd systeem — zoekmachines mogen niets indexeren.
* Next.js will automatically serve this at /robots.txt
*/ */
import type { MetadataRoute } from 'next' import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
return { return {
rules: [ rules: [
{ {
userAgent: '*', userAgent: '*',
allow: '/', disallow: '/',
disallow: [
'/api/',
'/_next/',
'/admin/',
],
},
{
userAgent: 'Googlebot',
allow: '/',
disallow: [
'/api/',
'/_next/',
],
}, },
], ],
sitemap: `${baseUrl}/sitemap.xml`,
} }
} }

View File

@@ -1,84 +0,0 @@
/**
* Sitemap Generator
*
* Generates sitemap.xml for SEO.
* Next.js will automatically serve this at /sitemap.xml
*/
import type { MetadataRoute } from 'next'
import { getAllReleases } from '@/lib/mdx/documentatie'
import { getAllPosts, getAllSeries } from '@/lib/mdx/blog'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
const currentDate = new Date()
// Fetch all documentation releases dynamically
const releases = await getAllReleases()
const releaseUrls: MetadataRoute.Sitemap = releases.map((release) => {
const releaseDate = new Date(release.frontmatter.releaseDate)
const isValidDate = !isNaN(releaseDate.getTime())
return {
url: `${baseUrl}/documentatie/${release.slug}`,
lastModified: isValidDate ? releaseDate : currentDate,
changeFrequency: 'monthly',
priority: 0.8,
}
})
// Fetch all blog posts dynamically
const posts = await getAllPosts()
const postUrls: MetadataRoute.Sitemap = posts.map((post) => {
const postDate = new Date(post.frontmatter.date)
const isValidDate = !isNaN(postDate.getTime())
return {
url: `${baseUrl}/blog/serie/${post.seriesId}/${post.slug}`,
lastModified: isValidDate ? postDate : currentDate,
changeFrequency: 'monthly',
priority: 0.7,
}
})
// Fetch all blog series
const series = await getAllSeries()
const seriesUrls: MetadataRoute.Sitemap = series.map((serie) => ({
url: `${baseUrl}/blog/serie/${serie.id}`,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 0.8,
}))
return [
{
url: baseUrl,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/blog`,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/documentatie`,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 0.9,
},
...seriesUrls,
...postUrls,
...releaseUrls,
{
url: `${baseUrl}/contact`,
lastModified: currentDate,
changeFrequency: 'monthly',
priority: 0.7,
},
]
}

View File

@@ -1,117 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import Image from 'next/image'
import { ScrollReveal } from '@/components/ui/scroll-reveal'
import aboutContent from '@/content/nl/about.json'
export function WhyMe() {
const [mounted, setMounted] = useState(false)
const { title, subtitle, image, paragraphs, stats, expertise } = aboutContent
useEffect(() => {
setMounted(true)
}, [])
return (
<section
id="over"
className="section-padding bg-gray-50 relative overflow-hidden"
>
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-64 h-64 bg-teal/5 rounded-full blur-3xl translate-x-1/2 -translate-y-1/2" />
<div className="absolute bottom-0 left-0 w-96 h-96 bg-teal/5 rounded-full blur-3xl -translate-x-1/2 translate-y-1/2" />
<div className="container-custom relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center max-w-6xl mx-auto">
{/* Left: Text Content */}
<ScrollReveal direction="left">
<div className="space-y-8">
<h2 className="text-5xl md:text-6xl lg:text-7xl font-heading font-bold text-text-primary leading-tight">
{title}
</h2>
<p className="text-xl md:text-2xl text-text-secondary font-light leading-relaxed">
{subtitle}
</p>
<div className="space-y-6 text-text-secondary text-lg leading-relaxed">
{paragraphs.map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
</div>
<div className="grid grid-cols-2 gap-4 pt-8">
{stats.map((stat, index) => (
<div key={index}>
<div className="text-3xl font-heading font-bold text-text-primary mb-1">{stat.value}</div>
<div className="text-text-secondary text-sm">{stat.label}</div>
</div>
))}
</div>
</div>
</ScrollReveal>
{/* Right: Photo */}
<ScrollReveal direction="right" delay={200}>
<div className="relative aspect-[3/4] lg:aspect-[4/5] max-w-md mx-auto lg:ml-auto">
{/* Optional subtle border/frame effect */}
<div className="absolute inset-0 rounded-2xl bg-teal/10" />
<div className="relative w-full h-full rounded-2xl overflow-hidden">
<Image
src={image.src}
alt={image.alt}
fill
className="object-cover"
priority
/>
</div>
</div>
</ScrollReveal>
</div>
{/* FASE 2: Expertise Section - Full Width with Floating Animation */}
{expertise && (
<div className="mt-16 pt-16 border-t border-gray-200">
<ScrollReveal>
<h3 className="text-2xl md:text-3xl font-heading font-bold text-center mb-3 text-text-primary">
{expertise.label}
</h3>
<p className="text-center text-text-secondary mb-12 max-w-2xl mx-auto">
De tools en methoden waarmee ik jouw AI-uitdaging aanpak
</p>
</ScrollReveal>
<div className="flex flex-wrap justify-center gap-4 max-w-5xl mx-auto">
{expertise.items.map((item, index) => (
<div
key={index}
className={`
group px-5 py-3 rounded-full font-medium text-sm
transition-all duration-300 cursor-default animate-float
border shadow-sm
${
item.highlight
? 'bg-teal-700 text-white border-teal-700 hover:bg-teal-600 hover:-translate-y-2 hover:rotate-2 hover:shadow-md'
: 'bg-white text-gray-800 border-gray-300 hover:bg-teal-50 hover:border-teal-600 hover:text-teal-800 hover:-translate-y-2 hover:rotate-1 hover:shadow-md'
}
`}
style={{
animationDelay: mounted ? `${index * 150}ms` : '0ms',
animationDuration: `${5 + (index % 3) * 0.8}s`,
}}
>
<span className="font-semibold">{item.name}</span>
<span className={`ml-2 text-xs ${item.highlight ? 'opacity-80 group-hover:opacity-100' : 'opacity-70 group-hover:opacity-100'}`}>
{item.category}
</span>
</div>
))}
</div>
</div>
)}
</div>
</section>
)
}

View File

@@ -1,95 +0,0 @@
{
"title": "Over mij",
"subtitle": "AI-gedreven development in de praktijk",
"image": {
"src": "/images/colin-office-6.webp",
"alt": "Colin van der Heijden - AI Development Specialist"
},
"paragraphs": [
"Na jaren in traditionele software development te hebben gewerkt, zag ik hoe bedrijven vast liepen in eindeloze implementatietrajecten en vendor lock-in. De belofte van AI-powered development klonk te mooi om waar te zijn - totdat ik het zelf ging testen.",
"Dit EPD prototype is geen marketingpraatje, maar een hands-on bewijs van wat er mogelijk is. Elke regel code, elk design decision, elke feature - allemaal gedocumenteerd en transparant. Want de beste manier om te laten zien wat AI-development kan, is het gewoon doen."
],
"stats": [
{
"value": "4 weken",
"label": "Van concept tot werkend prototype"
},
{
"value": "€200",
"label": "Totale build cost"
},
{
"value": "10+ jaar",
"label": "Ervaring in software development"
},
{
"value": "100%",
"label": "Transparant build in public"
}
],
"expertise": {
"label": "Tech Stack & Expertise",
"items": [
{
"name": "Gezond Verstand",
"category": "Mindset",
"highlight": true
},
{
"name": "Supabase",
"category": "Backend",
"highlight": false
},
{
"name": "Goeie werkethos",
"category": "Mindset",
"highlight": true
},
{
"name": "Gemini AI",
"category": "AI Tool",
"highlight": false
},
{
"name": "Claude AI",
"category": "AI Tool",
"highlight": false
},
{
"name": "Cursor IDE",
"category": "Development",
"highlight": false
},
{
"name": "No Bullshit",
"category": "Mindset",
"highlight": true
},
{
"name": "Vercel",
"category": "Hosting",
"highlight": false
},
{
"name": "Supabase",
"category": "Database",
"highlight": false
},
{
"name": "Bak ervaring",
"category": "Integration",
"highlight": false
},
{
"name": "Handson",
"category": "Mindset",
"highlight": true
},
{
"name": "Next.js",
"category": "Frontend",
"highlight": false
}
]
}
}

View File

@@ -1,21 +0,0 @@
{
"series": [
{
"id": "ai-speedrun",
"title": "AI Speedrun",
"description": "4 weken, €200, 1 EPD. De reis van idee naar werkend prototype.",
"color": "teal",
"order": 1,
"status": "completed"
},
{
"id": "cortex-intent",
"title": "Intent Driven Cortex",
"description": "De volgende stap: een AI-copilot die begrijpt wat je wilt.",
"color": "amber",
"order": 2,
"status": "active"
}
]
}

View File

@@ -1,33 +0,0 @@
---
title: "Kijk mee hoe ik live bouw aan EPD-systeem voor de GGZ!"
description: "Hoelang wacht jij nog op die ene feature die je EPD écht bruikbaar maakt? Ik start een experiment: in 4 weken een EPD bouwen met AI."
date: "2024-11-19"
published: true
seriesOrder: 1
tags:
- EPD
- GGZ
- AI
- Build in Public
---
*Hoelang wacht jij nog op die ene feature die je EPD écht bruikbaar maakt?*
18 maanden? 2 jaar? Of hoor je gewoon: "*Staat op de roadmap*"?
Ik ken het verhaal. Ik draai er tenslotte al meer dan 15 jaar in mee. Dure systemen die net niet doen wat je nodig hebt. Software die je dwingt om anders te werken, in plaats van andersom.
Daarom ben ik begonnen met experiment: de komende weken bouw ik live aan een EPD-systeem voor de GGZ. Niet als product dat je morgen koopt, maar om te laten zien wat er mogelijk is als je anders werkt.
Geen team van developers. Geen budget van 6 ton. Gewoon ik, moderne AI-tools, en 4 weken tijd. Van leeg scherm naar werkende software die je van intake tot behandelplan brengt.
Ik wil laten zien dat het anders kán. Sneller. Goedkoper. En belangrijker: **software die wél aansluit op hoe jij werkt**.
Ik bouw in het openbaar op aispeedrun.nl. Elke stap, elke keuze, elke fout. Zodat je zelf kunt zien wat er mogelijk is.
Meekijken mag. Meedenken ook. Als je ooit hebt gedacht: "*Waarom kan mijn EPD niet gewoon X doen*" - drop het in de comments. Ik neem de beste ideeën mee in de build!
Volg de speedrun: [aispeedrun.nl](https://aispeedrun.nl)
Laten we samen uitvinden hoe ver we komen. En als het mislukt? Dan weten we in ieder geval waarom je leverancier nóg 18 maanden nodig heeft voor die killer feature.

View File

@@ -1,48 +0,0 @@
---
title: "Dag 2: Authentication, docs en eerste workflows"
description: "14 nieuwe accounts na de launch en genoeg reacties om gemotiveerd te blijven. Vandaag: authentication, live documentatie en two-level interface design."
date: "2024-11-20"
published: true
seriesOrder: 2
tags:
- Authentication
- Supabase
- Documentation
- UI Design
---
14 nieuwe accounts na de launch gisteren en genoeg reacties om mij gemotiveerd te houden. Thanks daarvoor!
Dat triggerde me om meteen van die simpele demo-link af te stappen en het wat steviger te maken.
## Vandaag gebouwd
### Authentication die werkt
Email/password + demo account voor quick testing. Session management met Supabase Auth (JWT-based), password reset flow, en RLS policies voor data security. Basis staat.
### Live documentatie systeem
Een volledig docs platform waar je elke feature, elke commit, elke keuze kunt volgen. Op thema georganiseerd, zodat je precies kunt zien hoe bepaalde delen zijn gebouwd.
💡 **Waarom dit belangrijk is**: traditionele systemen zijn vaak black boxes. Documentatie is vaak slecht te vinden en/of achterhaald, waardoor je niet weet waarom het werkt zoals het werkt.
Dit experiment laat zien dat het anders kan. Transparant bouwen én gebruikers vanaf dag 1 betrekken bij de voortgang van je roadmap.
### Two-level interface design
- **Level 1**: Behandelaar context (dashboard, caseload, agenda, rapportage)
- **Level 2**: Cliënt dossier (intake, diagnose, behandelplan, voortgang)
Je klikt van je caseload overzicht door naar een specifiek dossier. Net zoals je werkt.
## De eerste bugs
Webpack vloekte bij de MDX integratie. AI hielp met debuggen. We draaien weer.
Dit gaat sneller dan ik had verwacht. De energie van de reacties maakt het een stuk leuker!
Volg live en bekijk de docs: [aispeedrun.nl](https://aispeedrun.nl) 🔧
Op naar de volgende, let's go! 🏃‍♂️‍➡️

View File

@@ -1,56 +0,0 @@
---
title: "Dag 5: Data die meegaat, nu echte workflows"
description: "Een cliënt verhuist. Nieuwe behandelaar. Weer vanaf nul beginnen. Dit gebeurt elke dag in de GGZ. En het hoeft niet zo te zijn."
date: "2024-11-21"
published: true
seriesOrder: 3
tags:
- FHIR
- MedMIJ
- Data Exchange
- Healthcare Standards
---
Een cliënt verhuist. Nieuwe behandelaar. Weer vanaf nul beginnen. "*Vertel nog eens wat je al geprobeerd hebt...*"
Dit gebeurt elke dag in de GGZ. En het hoeft niet zo te zijn.
## Dit is er vandaag gebouwd
### Data die echt uitwisselbaar is
Misschien niet direct de killer feature waar je als zorgprofessional op zit te wachten, maar wel een belangrijke basis.
Gebouwd op FHIR, de taal die ziekenhuizen en huisartsen wereldwijd gebruiken. Betekent dat het dossier straks gewoon praat met elke ROM-tool, PGO-app of eHealth module. Zonder gedoe.
Patient, Practitioner, CarePlan, Observations, Conditions, alles conform de internationale zorgtaal.
## Wat dit mogelijk maakt
**Inzage voor cliënten** - Via MedMIJ zie je straks je eigen behandelplan, diagnoses en ROM-scores in je PGO-app. Jouw data, jouw regie.
**Continuïteit die werkt** - Verhuizing? Doorverwijzing? Je complete dossier gaat mee. Inclusief wat werkte, ROM-trends, SMART doelen. Niet opnieuw beginnen.
**Blended care zonder gedoe** - Mindfulness app, dagboek, oefeningen - allemaal gekoppeld aan het behandelplan. Voortgang komt automatisch terug in het dossier.
## Voor de professional
- ROM-metingen automatisch naar LSP
- Behandelplannen delen met één klik
- Koppeltaal integraties voor eHealth
- Bij overdracht zie je direct: wat is geprobeerd, wat werkte, welke ROM-scores
## Wat nu komt
Nu het datamodel staat, kunnen we aan de slag met echte workflows:
→ Screening & intake flow met spraak-naar-tekst
→ Ondersteuning voor verschillende disciplines (psycholoog, psychiater, vaktherapeut)
→ AI-assistentie voor diagnostiek en behandelplannen
De eerste wensen van jullie staan op de roadmap. De eerste bugs zijn gefixt. En het tempo blijft hoog 🚀
Werk je in de GGZ? Wat zou jou helpen om zo'n systeem écht te gebruiken in je praktijk?
Volg live: [aispeedrun.nl](https://aispeedrun.nl)

View File

@@ -1,62 +0,0 @@
---
title: "Dag 7: De valkuil"
description: "Zes tabs, twaalf subtabs, formulieren voor alles. Tijdens het bouwen besef ik me: ik bouw het exact zoals ik het zelf 15 jaar lang heb gezien."
date: "2024-11-23"
published: true
seriesOrder: 4
tags:
- Intake
- Process Design
- Reflection
- GGZ
---
Dit weekend begon ik met bouwen van het screening- en intakeproces. Zes tabs, twaalf subtabs, formulieren voor alles.
Hulpvraag, contactmomenten, kindcheck, risicotaxatie, anamnese, ROM, diagnose, behandeladvies etc.
Tijdens dit proces komen heel veel onderdelen samen: registratie van de cliënt, verwerken administratieve gegevens, contact met huisarts en/of omgeving, eerste afspraken plannen, financiering starten en als kers op de taart ook nog doorverwijs- en wachtlijstproblematiek.
Wanneer je in de GGZ besluit om je werkproces te documenteren, is dit één van de lastigste onderdelen weet ik uit eigen ervaring. Tijdens het bouwen besef ik me: ik bouw het exact zoals ik het zelf 15 jaar lang heb gezien.
## De verdeling van een intakegesprek
Hieronder mijn achter-op-het-bierviltje-analyse:
- **30% administratie** gegevens die al ergens bestaan, maar die we opnieuw invullen omdat systemen niet praten
- **40% voorgeschreven documentatie** anamnese invullen in 47 tekstvakken terwijl je het gesprek al hebt gevoerd
- **20% risicomanagement** risicotaxaties die belangrijk zijn, maar waar je dezelfde vragen drie keer op verschillende plekken invult
- **10% luisteren naar de cliënt** het enige waar het eigenlijk om gaat
En dus bouw ik nu precies wat ik ken. Zes tabs. Twaalf subtabs. Ik digitaliseer het bekende.
## Wat ik ga doen
Ik ga deze module afmaken. Zoals ik 'm nu bouw. Omdat het een goede benchmark is en zodat we zien hoe het nu werkt.
Maar ik wil dit ook transformeren naar een alternatieve intake-flow. Eentje die uitgaat van menselijk contact + AI-assistentie. Eentje die begint bij de cliënt, niet bij het protocol. Een EPD dat je dwingt om eerst te luisteren.
En dan kijken we welke beter werkt. Want misschien is "hoe het altijd ging" niet hetzelfde als "hoe het moet".
## Wat er nu staat
Screening & intake module:
✅ Basisregistratie (cliëntgegevens, contactpersonen)
✅ Hulpvraag & activiteitenlog
✅ Risicotaxatie (suïcide, agressie, kindveiligheid)
✅ Anamnese (psychiatrisch, sociaal, lichamelijk)
✅ ROM-onderzoeken
✅ Diagnose met DSM-5 codes
✅ Behandeladvies + text editor
Volgende week:
→ Voice-to-text integratie voor intake gesprekken
→ AI samenvatting van gesprekken
## Mijn vraag aan jou
Welke onderdelen van jouw intake kosten jou de meeste tijd? En welke zou je schrappen als je mocht kiezen?
Volg live: [aispeedrun.nl](https://aispeedrun.nl)

View File

@@ -1,33 +0,0 @@
---
title: "Week 2: Demo time - spraakherkenning en verslaglegging"
description: "30 minuten typen na elk gesprek. Herkenbaar? Deze week heb ik spraak-naar-tekst ingebouwd. Gewoon praten na je gesprek, en het verslag schrijft zichzelf."
date: "2024-11-26"
published: true
seriesOrder: 5
tags:
- Speech-to-Text
- AI
- Demo
- Verslaglegging
---
30 minuten typen na elk gesprek. Herkenbaar?
In week 2 van deze AI Speedrun heb ik spraak-naar-tekst ingebouwd. Gewoon praten na je gesprek, en het verslag schrijft zichzelf.
## In de demo zie je
→ Live transcriptie terwijl je praat
→ Stopwoordjes worden automatisch gefilterd
→ Tekst direct in het dossier
Geen formulieren. Geen klikken door tien schermen. Gewoon praten.
Het behandeladvies voor de cliënt in deze demo: *laptop-exposure met geleidelijke afbouw*. Herkenbaar voor sommigen? 😅
## Volgende stap
AI die je verslag ook nog structureert naar hoofdklacht, anamnese en behandeladvies.
Hoeveel tijd ben jij kwijt aan verslaglegging per dag?

View File

@@ -1,45 +0,0 @@
---
title: "AI als tool of als baas?"
description: "Josh ging 3 maanden all-in met AI. Product shipte en werkte. Maar toen hij een kleine wijziging wilde maken, snapte hij zijn eigen code niet meer."
date: "2024-11-30"
published: true
seriesOrder: 6
tags:
- AI
- Reflection
- Augmentation
- Development
---
Merijn van der Zalm stuurde me gisteren Josh Anderson's verhaal door.
Josh (Product-Focused CTO, 25 jaar ervaring) ging 3 maanden all-in met AI. Product shipte en werkte. Maar toen hij een kleine wijziging wilde maken, snapte hij zijn eigen code niet meer.
Zijn collega vroeg: "*Who owns that product? You or Claude Code?*"
Zijn antwoord: *Claude Code*.
Josh was passagier geworden in zijn eigen product.
## Waarom ik dit experiment doe
Dit is waarom ik mijn eigen experiment doe. Die grens tussen "productief met AI" en "passagier in je project" ligt ergens en ik wil uitzoeken waar.
## Josh's inzicht: Augmentation vs Abdication
- **Augmentation** = AI helpt, jij blijft eigenaar
- **Abdication** = AI beslist, jij knikt
MIT studie: 95% van AI-initiatieven faalt. Dat is abdication.
De 5% die werkt? Augmentation.
## De realiteit
Ik ben geen developer. Wel 15 jaar GGZ-domeinkennis en begrip van software processen. Als ík al in staat ben om werkende functionaliteit te bouwen - solo, geen dev-achtergrond - hoe ver kun je dan als volledig dev-team komen?
Speedrun dag 12. Ik zoek die grens tussen AI als tool en AI als baas.
Thanks Merijn voor de reality check 👊
Herken je dit? Waar merk jij het verschil tussen "AI helpt je" en "AI heeft je overgenomen"?

View File

@@ -1,54 +0,0 @@
---
title: "Het is 13:45 uur. Je volgende cliënt komt om 14:00."
description: "Je weet dat je een intake moet starten, maar je zit naar je scherm te staren. Waar was die knop ook alweer? Deze week bouwde ik daar een oplossing voor."
date: "2024-12-02"
published: true
seriesOrder: 7
tags:
- AI Assistant
- UX
- Hulp
- EPD
---
Je weet dat je een intake moet starten, maar je zit naar je scherm te staren. Waar was die knop ook alweer? Cliënten? Dossier? Of toch via het menu rechtsboven?
## Je hebt vier opties
1. Een collega vragen - maar die zit zelf in gesprek.
2. De handleiding openen - tig pagina's en sheets scrollen, als je de instructie überhaupt al kan vinden
3. De servicedesk bellen - gemiddelde wachttijd 15 minuten.
4. Of mijn persoonlijke favoriet: gewoon maar wat klikken en hopen dat het goed gaat.
Geen van deze opties is ideaal. En dit gebeurt meerdere keren per dag.
## Deze week bouwde ik daar een oplossing voor
Een AI-assistent die gewoon daar zit, rechtsonder in je scherm. **Altijd beschikbaar**.
Je klikt erop en typt: "Hoe start ik een intake?"
Binnen twee seconden krijg je antwoord. In gewoon Nederlands. Precies wat je nodig hebt.
En het mooie is: je kunt doorvragen. "En hoe voeg ik daar notities aan toe?" De assistent begrijpt dat je het nog steeds over die intake hebt. Je hoeft niet steeds opnieuw te beginnen met zoeken.
## Waarom dit ertoe doet
Elk moment dat je zoekt naar een knop, is een moment dat je niet bezig bent met je cliënt.
Ik heb collega's zien huilen van frustratie. Niet omdat ze dom waren of lui. Maar omdat het systeem onduidelijk was, de tijd ontbrak om het te leren, en de druk om gewoon door te werken altijd aanwezig was.
Dat is het echte probleem. Niet de minuten. Maar de frustratie. De stress. Het gevoel dat je tekort schiet terwijl je gewoon je werk wilt doen.
## De echte reden waarom systemen falen
Het ligt niet aan de behandelaren. Het ligt ook niet altijd aan het systeem. Het ligt aan het moment waarop je hulp nodig hebt.
Die handleiding die de implementatieconsultant tijdens de training uitdeelde? Die ligt in een bureaula. Of op een netwerkschijf die je niet zo vaak opent. Of het is een PDF waarvan je niet meer weet hoe die heet.
En dan zit je daar, om 13:45, en je hebt gewoon direct antwoord nodig.
Dus bouwde ik een systeem dat uitleg geeft op het moment dat je het nodig hebt. Geen apart venster. Geen tabblad erbij. Gewoon daar, in het scherm waar je toch al bent.
Wat is het meest frustrerende moment in jouw EPD? Waar loop je tegenaan? Wat kost je elke dag weer onnodig tijd? Ik ben benieuwd!

View File

@@ -1,49 +0,0 @@
---
title: "Week 4: Tussen de aardappelen en de spruitjes door"
description: "In 4 weken een compleet EPD bouwen met AI! Kleine reality check: het zijn geen 28 dagen van 8 uur programmeren. Dit is bouwen aan de keukentafel."
date: "2024-12-06"
published: true
seriesOrder: 8
tags:
- Reality Check
- Work-Life Balance
- EPD
- Progress
---
"In 4 weken een compleet EPD bouwen met AI!"
Klinkt goed, toch? Sexy headline. Lekker disruptief.
## Kleine reality check
Het zijn inderdaad 4 weken. Maar geen 28 dagen van 8 uur programmeren. Eerder: een uurtje 's avonds als de kids slapen, zondagochtend vroeg, en tussen de sinterklaassurprises door.
Dit is geen Silicon Valley-fantasie. Dit is bouwen aan de keukentafel, tussen de aardappelen en de spruitjes door.
En het werkt... Na 3 weken staat er een EPD. Met intake, screenings, AI-behandelplannen, spraak-naar-tekst, de hele reutemeteut. FHIR-compliant, werkend, bruikbaar.
## Het contrast
Traditionele enterprise software? Een jaar implementeren voor €500k+. Met consultants die je vertellen dat het écht niet sneller kan. Change management, migratie-drama's, vendor lock-in voor 5 jaar.
En dan bouw ik (bijna) hetzelfde in 4 weken. Solo, tussen gezin en werk door.
"*Maar AVG dan? NEN7510? Integratie met bestaande systemen?*"
Terechte vragen. En nee, dat is niet niks. Dat zijn serieuze uitdagingen die tijd en geld kosten.
Maar: Je kunt nu beginnen met een werkend fundament in weken. De compliance, integraties, beveiliging dat bouw je erop. Iteratief.
In plaats van een jaar wachten op big bang, heb je in week 4 al iets werkends. Dat is het verschil.
## Geen hustle porn
Dit is geen "rise and grind" verhaal. Ik slaap gewoon. Om 18u kijk ik het Sinterklaasjournaal met de kids.
Maar moderne AI-tools + 15 jaar domeinkennis + gefocuste sessies = plots is er veel mogelijk. Niet omdat ik zo goed ben. Maar omdat de tools volwassen zijn.
Software hoeft niet meer onbereikbaar duur en traag te zijn. Het kan ook gewoon. Tussen de spruitjes door.
Week 4: demo's, behandelplan UI. Laatste week. Volg je mee? 👉 [aispeedrun.nl](https://aispeedrun.nl)

View File

@@ -1,51 +0,0 @@
---
title: "De verpleegkundige overdracht"
description: "Kun je iets slims bouwen voor een verpleegkundige die 6 overdrachten heeft per dag aan artsen aan de volgende dienst?"
date: "2024-12-08"
published: true
seriesOrder: 9
tags:
- Verpleegkunde
- Overdracht
- AI
- Demo
---
*"Kun je iets slims bouwen voor een verpleegkundige die 6 overdrachten heeft per dag aan artsen aan de volgende dienst en ondertussen ook allerlei metingen doet en moet opslaan?"*
Ja, dat kan ik!
## Het echte probleem
Verpleegkundigen lopen rondes langs 15 cliënten achter elkaar. Je wilt niet constant tussen dossiers heen en weer switchen, dat kost gewoon te veel tijd.
En de psychiater die 's ochtends binnenkomt moet nu alle 15 dossiers openen om te checken of er vannacht iets gebeurd is. Dat is 30-45 minuten scanwerk voordat je überhaupt aan je dag begint.
Tijd die je liever besteedt aan cliënten, niet aan zoeken in systemen.
## Wat ik bouwde
Twee modules die samenwerken.
**Voor de verpleegkundige** een zorgnotities-module waar alle cliënten in één sidebar staan. Quick-switch tijdens je ronde, korte notities met categorieën (Medicatie, ADL/verzorging, Gedrag, Incident), en een toggle om te markeren wat belangrijk is voor overdracht. Achteraf bewerken kan natuurlijk ook nog.
**Voor artsen en de volgende dienst** een overdracht-module met direct overzicht: welke cliënten hadden iets vannacht? Je kunt filteren op alerts (rood voor hoge risico's, groen voor overdracht notities) en ziet een tijdlijn met alle rapportages per cliënt.
En als bonus een AI-samenvatting, maar wel met een cruciaal onderdeel: elk aandachtspunt is klikbaar en linkt direct naar de bronrapportage. Zodat we kunnen zien waar informatie vandaan komt. **Geen black box, maar transparantie.**
## Het verschil
- Van 2 uur dossiers doorzoeken, wisselen en informatie filteren naar 2 minuten scannen.
- Van 15 dossiers openen naar direct zien waar je aandacht nodig is.
- Van 6x per dag dezelfde info herhalen naar één keer documenteren en klaar.
## Waarom ik dit doe
Omdat ik zie hoe zorgverleners gefrustreerd raken door systemen die hun werk vertragen in plaats van ondersteunen. Die frustratie hoeft niet.
Traditionele EPD's kosten €500k+ en 12 maanden. **Ik bouwde deze verpleegrapportage en overdracht-module afgelopen weekend met AI.**
Software hoeft niet jaren te kosten. Het kan ook volgende week al werken.
Herken je dit? Heb je een collega die hier dagelijks mee worstelt? Stuur dit door. Ik ben benieuwd of deze oplossing helpt!

View File

@@ -1,65 +0,0 @@
---
title: "Stop met klagen dat AI geen goede software bouwt"
description: "Een one-shot-prompt geeft ook een one-shot-resultaat. En verrassing... dat is 9 van de 10x niet wat je wilde. De 5 gouden regels voor werken met AI agents."
date: "2024-12-11"
published: true
seriesOrder: 10
tags:
- AI
- Tips
- Development
- Best Practices
---
"*Heb ik geprobeerd. Werkt niet.*"
Ah, de one-shot expert heeft gesproken. Een one-shot-prompt geeft ook een one-shot-resultaat. En verrassing... dat is 9 van de 10x niet wat je wilde.
Noem het one-shot-optimisme: de verwachting dat je in één vraag een werkend product krijgt. Ja, zo werkt het dus niet.
## De realiteit
En dan blijf je achter, want AI coding is geen hype meer. OpenRouter routeert miljarden AI-verzoeken voor developers. Begin dit jaar ging één op de tien over code. En nu in december de helft. Dit is mainstream, maar dan moet je wel weten hoe je ermee werkt.
AI agents zijn net hele eager juniors. Ze beginnen meteen met bouwen zonder enige context. Vraag om een login-pagina en je krijgt OAuth, 2FA, role-based access en een password recovery flow. Terwijl jij gewoon een simpel schermpje wilde.
## De 5 gouden regels voor werken met AI agents
### 1. Niet meteen programmeren
AI is getraind om *helpful* te zijn. En helpful voelt voor een AI als: snel iets opleveren. Geef een half zinnetje en er wordt al gebouwd.
Elke feature begin ik met: "Niet meteen programmeren." Eerst context, dan een plan en dan pas bouwen.
### 2. Geef altijd context
Elke chat begint voor AI als een leeg blad. Geen geheugen van wat je gisteren bouwde. Dus ja, er wordt vrolijk een nieuwe functie geschreven terwijl je die al hebt.
Daarom werk ik altijd met een PRD, functioneel ontwerp en technisch ontwerp. Voor mezelf én als context voor de AI.
### 3. Denk MVP
Vraag om een zoekfunctie en AI bouwt meteen filters, sortering, fuzzy matching, zoeksuggesties en een volledige Elasticsearch-integratie.
Ik vertel de agent altijd: "We bouwen een prototype, een MVP." Prioriteer in MVP en post-MVP. Houd het klein en bouw uit. Dit zorgt ervoor dat je niet direct 100 features tegelijk bouwt, wat je sowieso niet direct wil.
### 4. Werk in iteraties
AI probeert altijd in één keer een "compleet" antwoord te geven. Maar software bouwen is geen one-shot game.
Mijn bouwplan is geen "start met bouwen", maar een gefaseerd plan met epics en stories. De opdracht is "Maak Epic 1, Story 1."
### 5. Jij blijft de baas
AI is een eager junior, geen architect. Slim, snel, maar zonder sturing wordt er de verkeerde kant op gebouwd.
Ik ben geen programmeur, maar wel de product owner. Ik bepaal de richting, AI voert uit.
## Bonus tip
Tools zoals Claude Code en Cursor hebben een "Plan" en "Ask" modus. Gebruik dit! Laat AI eerst denken voordat je 'm laat bouwen.
Die eager junior kan heel veel. Tenminste, als je 'm goed begeleidt. Net als in het echt. 😉
Meer weten over de AI Speedrun? 👉 [aispeedrun.nl](https://aispeedrun.nl)

View File

@@ -1,47 +0,0 @@
---
title: "Dag 28: 'Dat stond niet op mijn lijstje' is geen excuus meer"
description: "Toen oud-collega Edwin zei dat mijn EPD-prototype wat traag aanvoelde, was mijn eerste reactie defensief. Na een nachtje slapen kon ik de feedback beter waarderen."
date: "2024-12-12"
published: true
seriesOrder: 11
tags:
- Performance
- Feedback
- AI
- Optimization
---
Toen oud-collega Edwin zei dat mijn EPD-prototype wat traag aanvoelde, was mijn eerste reactie defensief. "*Ja, dat was ook niet echt het doel.* 😤" Performance optimalisatie stond niet op mijn lijstje. Ik was bezig met features bouwen, niet met milliseconden tellen.
Na een nachtje slapen kon ik de feedback beter waarderen. Want iedereen die met enterprise software werkt (zeker EPD's) weet dat trage systemen de realiteit zijn. Eindeloos wachten op schermen die laden, rapportages die vastlopen, behandelaren die gefrustreerd afhaken. Ik ken het uit eigen ervaring. 😞
Maar wat verwacht je? Ik draai op de gratis tier van Vercel en Supabase. Geen dedicated servers, geen geoptimaliseerde database clusters. Dit is een prototype op een hobby-budget.
## Toch opende ik Claude
"*Review deze codebase op performance en geef concrete verbetervoorstellen.*"
Wat volgde was een masterclass die ik niet had verwacht. Baseline metingen. Bottleneck-analyse. Vier iteraties van optimalisaties: parallel data fetching, server-side pagination, database indexes, lazy loading.
## Het resultaat
| Component | Voor | Na |
|-----------|------|-----|
| Patient Dashboard | 800ms | 440ms |
| Rapportage API | 1.67s | 188ms |
| Rapportage pagina | 2.05s | 343ms |
## Maar dit gaat niet over milliseconden
AI versnelt niet alleen het typen van code. Het versnelt het denken over code. De drempel om iets te fixen is nu zo laag geworden dat feedback negeren een bewuste keuze wordt, niet een praktische beperking.
Dat verandert alles. Niet alleen hoe je software bouwt, maar ook hoe je feedback ontvangt.
"*Daar heb ik nu geen tijd voor*" betekent nu eigenlijk: "*Dat vind ik niet belangrijk genoeg.*"
Eerlijker, maar ook confronterender.
*(Oh, en deze week ook gebouwd: agenda-verbeteringen met drag & drop, behandelplan uitgebreid, en een volledige diagnose-module. Alleen nog geen tijd gehad om er een nette post van te maken.)*
Volgende week: de eindbalans. Kan AI een EPD bouwen? Het antwoord is genuanceerder dan ik vier weken geleden dacht.

View File

@@ -1,59 +0,0 @@
---
title: "De Cijfers"
description: "Vier weken geleden startte ik een experiment: hoe ver kom je met AI-tooling als je een compleet EPD-systeem wil bouwen voor de GGZ? Nu heb ik een antwoord en de cijfers."
date: "2024-12-18"
published: true
seriesOrder: 12
tags:
- Results
- Statistics
- EPD
- Conclusion
---
Vier weken geleden startte ik een experiment: hoe ver kom je met AI-tooling als je een compleet EPD-systeem wil bouwen voor de GGZ?
Nu heb ik een antwoord en de cijfers.
Mijn eerste conclusie: Met AI kun je volwaardige functionaliteit snel opleveren. Maar, en dit is belangrijk, AI verzint geen goede oplossing. Jij moet zelf bedenken waar de waarde zit.
## De cijfers
- 95 commits
- ~118.000 regels code
- 140+ documenten (waarvan 75 formele specs: PRD, FO, TO, Bouwplannen)
- 17 gebruikersdocumentatie pagina's
- 4 release notes
- 1 developer + Claude Code
- €200 budget
## 7 complete modules
1. **Authenticatie met Supabase** - login, signup, wachtwoordreset, email confirmatie
2. **Screening & intake systeem** - hulpvraag, besluitvorming en risicotaxatie
3. **Diagnose module** - ICD-10 codering en zoekfunctie
4. **Agenda** - FullCalendar (dag/week/maand views)
5. **Verpleegkundige rapportage** - met overdracht
6. **Behandelplan module** - SMART doelen en leefgebieden
7. **Client management** - CRUD operations
## 3 AI-integraties
1. **Spraakgestuurde verslaglegging** via Deepgram - gewoon dicteren in plaats van typen
2. **AI-gegenereerde samenvattingen** voor dienstoverdracht
3. **AI-assistent** die zowel helpt met systeemgebruik als inhoudelijke vragen over het dossier kan beantwoorden
## Technisch
En voor de '*het-is-vast-spaghetti-code*' critici: dit draait op Next.js 14, TypeScript, Supabase met Row Level Security, een FHIR-compliant datamodel, en production-ready componenten. Volledig Nederlandse UI. Op een hobby-project budget.
## De realisatie
Dit waren de cijfers. Experiment geslaagd zou je zeggen.
Alleen, tijdens het bouwen realiseerde ik me dat ik nog steeds een vrij traditioneel systeem aan het bouwen was. Weliswaar met slimme AI-handigheidjes, maar in de kern? Hetzelfde EPD-denken als de afgelopen 15 jaar.
Mijn aanpak is innovatief, het product eerlijk gezegd niet heel erg. AI versnelt wat je bouwt, maar wát je bouwt, dat bepaal jij. Domeinkennis wordt meer en meer relevanter dan de technische skills.
Ik heb inmiddels een idee over hoe het EPD van de toekomst eruit moet zien en dat is niet wat er nu staat.

View File

@@ -1,134 +0,0 @@
---
title: "10 dingen die ik leerde van 4 weken bouwen met AI"
description: "Gisteren de cijfers. Vandaag de learnings. Over AI als tool, over mijzelf als bouwer, en over waarom de zorg meer moet experimenteren."
date: "2024-12-19"
published: true
seriesOrder: 13
tags:
- Learnings
- AI
- Reflection
- Healthcare
---
Gisteren de cijfers. Vandaag de learnings.
10 dingen die ik leerde van 4 weken bouwen met AI. Over AI als tool, over mijzelf als bouwer, en over waarom de zorg meer moet experimenteren.
**De belangrijkste inzichten?**
→ AI verzint geen goede oplossing. Jij moet zelf bedenken waar de waarde zit.
→ Domeinkennis weegt zwaarder dan technische skills.
→ Met alle moderne tooling bouw ik nog steeds traditioneel EPD-denken.
Dat laatste was een ontnuchterende realisatie.
---
## De aanleiding
Vier weken geleden startte ik een experiment: hoe ver kom je met AI-tooling als je een compleet EPD-systeem wil bouwen voor de GGZ?
Twee dingen waren de aanleiding. Jaren van frustratie over EPD's die nét niet werken zoals de zorg werkt. En de vraag die ik steeds vaker hoorde: "*Maar kun je met AI écht software bouwen?*" Ik besloot het uit te zoeken.
Meer dan 15 jaar werk ik nu in de GGZ. En al die tijd hoor ik hetzelfde verhaal. Van collega's, van behandelaren, van managers: het EPD werkt niet zoals wij werken. Die ene feature die we nodig hebben? Staat op de roadmap. Al 18 maanden. Het budget voor aanpassingen? Tonnen. En dan nog is het niet zeker dat het komt.
Ik weet dit uit eigen ervaring, want ik heb jarenlang bij een EPD-leverancier gewerkt als consultant en product owner. Dus ik begrijp de worsteling om goede, stabiele, functionele, veilige, schaalbare, gebruiksvriendelijke software te bouwen die voldoet aan alle wet en regelgeving. Maar toch, miljoenencontracten, jarenlange implementaties. En aan het eind van de rit nog steeds workarounds in Excel!
Tegelijkertijd zie ik wat er mogelijk is met moderne AI-tooling. Ik gebruik het dagelijks. Maar om me heen zie ik collega's die sceptisch blijven. "*Leuk voor prototypes, niet voor echte software.*" "*AI-code is niet production-ready.*" "*Eén uur coden met AI is drie uur debuggen.*"
Ik wilde het tegenovergestelde bewijzen. De scepsis snap ik, maar die is gebaseerd op hoe AI een jaar of twee geleden werkte. De techniek gaat sneller dan wij ons aanpassen. Dus: niet praten, maar bouwen. In het openbaar. Documenteren wat werkt en wat niet. En kijken hoe ver ik kom, als niet-programmeur.
## Het resultaat
- 118.000 regels code
- 140+ documenten (waarvan 75 formele specs)
- 7 complete modules
- 3 AI-integraties
- 95 GIT commits
- alles op een hobby budget (+/- €200)
---
## Over AI als tool
### 1. Met AI kun je volwaardige functionaliteit snel opleveren
Dat is de belofte, en die klopt. Maar hier komt de nuance die niemand je vertelt: AI verzint geen goede oplossing. Jij moet zélf bedenken waar de waarde zit.
### 2. Verwacht niet dat het eerste antwoord het juiste is
Nooit, eigenlijk. Ga door. Hak in stukjes. Voeg samen. Ontleed totdat je iets hebt waarmee je verder komt. AI is geen orakel. Het is een klant die niet precies weet wat hij wil. Je moet doorvragen. Concretiseren. Stel je een vage vraag, dan krijg je een vaag antwoord. Wordt je scherper, geef je voorbeelden, dan wordt de output beter.
### 3. Gebruik AI als sparringpartner, niet alleen als programmeur
Laat Claude een probleem analyseren vanuit het perspectief van 'lead developer', 'product owner' én 'eindgebruiker'. Dat geeft je drie invalshoeken die helpen om blinde vlekken te spotten.
Of: simuleer een gesprek tussen consultant en klant om een pitch voor te bereiden. Of: "*Wat zou een verpleegkundige hiervan vinden?*" "*Waar zit het risico voor de psychiater?*"
Het gaat om meerdere perspectieven. Daar zit minstens zoveel waarde als in de gegenereerde code.
---
## Over mijzelf als bouwer
### 4. Domeinkennis weegt zwaarder dan technische skills
AI kan code genereren, maar je moet wel snappen wat er staat en weten hoe software werkt. Zeker bij enterprise software zoals een EPD. AI is hier de brug.
Wat AI niet kan: snappen waarom een verpleegkundige overdracht anders werkt dan een artsenbrief. Waarom een risicotaxatie op dát moment in het proces moet zitten. Waarom die ene checkbox er wel toe doet en die andere niet.
Die 15 jaar GGZ-ervaring? Die waren relevanter dan mijn technische skills.
### 5. Je proces blijft gewoon je proces
PRD, Functioneel Ontwerp, Technisch Ontwerp, dat sla je niet over omdat je AI gebruikt. Integendeel. Hoe beter je documentatie, hoe beter de AI-output. "*Garbage in, garbage out*" is nog steeds waar. Alleen nu op een hoger abstractieniveau.
### 6. Cognitive offloading? Ik had het tegenovergestelde
Er wordt gewaarschuwd dat je lui wordt als je alles aan AI overlaat. Misschien, als je besluit om zelf niet meer na te denken.
Maar ik had tijdens dit project juist cognitive overload, de productieve variant. Bouwen, demo's maken, content schrijven, contacten leggen. Alles tegelijk, in vier weken. AI maakt dat mogelijk. Niet door het denken over te nemen, maar door de executie te versnellen.
### 7. Met al die moderne tooling bouw ik nog steeds een vrij traditioneel systeem
De aanpak is innovatief. Het product? Eerlijk gezegd niet heel erg. Hetzelfde EPD-denken als de afgelopen 15 jaar. Dezelfde schermen. Dezelfde workflows. Dezelfde aannames.
Dat was een ontnuchterende realisatie.
---
## Over de zorg en organisaties
### 8. We moeten meer experimenteren
Uit gesprekken hoor ik steeds hetzelfde verhaal: pilots die blijven steken op governance. Op beleid. Op regels. Op commissies die eerst moeten vergaderen.
Allemaal relevant, daar is geen twijfel over. Maar ook allemaal redenen om níet te beginnen.
In een experiment bepaal je zelf de grenzen. Bouw eerst. Leer. En pas dán aan. Probeer niet alle kaders en risico's vooraf te bedenken. De wereld verandert snel en wacht niet op ons 100% waterdichte plan.
### 9. De waarde zit niet in de AI, maar in de data en integratie
De AI zelf is het makkelijke stuk, dat is een API call en een goede prompt. De uitdaging zit in de rest.
Specifiek voor de GGZ: waar somatische zorg veel objectieve datapunten heeft (labwaarden, vitals, scans) hebben we in de GGZ vooral ongestructureerde tekst. Behandelplannen. Decursus. Rapportages. Heteroanamneses. Observaties.
Dát is waar de complexiteit zit. En eerlijk gezegd ook waar het interessant wordt.
### 10. De toekomst is niet een beter scherm, maar geen scherm
Ephemeral UI, vergankelijke interfaces die gegenereerd worden op het moment dat je ze nodig hebt. De agent doet de rest. Geen vaste menu's, geen eindeloze klikpaden. Gewoon: zeg wat je nodig hebt, en het verschijnt.
Dat is niet wat ik gebouwd heb. Maar wel waar ik naartoe wil.
---
## Wat nu?
Dit is wat vier weken bouwen me geleerd heeft. Niet alleen over AI, maar over mijzelf als bouwer en over de zorg waar ik al 15 jaar in werk.
De volgende vraag ligt voor de hand: als ik met moderne tooling nog steeds traditioneel EPD-denken bouw, hoe ziet het EPD van de toekomst er dan wél uit?
Want dat is niet wat ik gebouwd heb (nog niet).

View File

@@ -1,18 +0,0 @@
---
title: "Introductie: Intent Driven Cortex"
description: "De volgende evolutie van het EPD: een AI-copilot die begrijpt wat je wilt bereiken."
date: "2025-01-01"
published: true
seriesOrder: 1
tags:
- AI
- Cortex
- Intent
---
## Een Nieuwe Aanpak
Placeholder voor je eerste Cortex Intent blogpost.
Vervang deze content met je nieuwe serie over het intent-driven EPD.

View File

@@ -1,15 +0,0 @@
{
"buttons": {
"readMore": "Lees verder",
"scrollDown": "Scroll naar beneden",
"followProgress": "Volg de voortgang",
"viewDemo": "Bekijk Demo",
"contact": "Contact"
},
"labels": {
"readingTime": "minuten lezen",
"lastUpdated": "Laatst bijgewerkt",
"share": "Deel"
}
}

View File

@@ -1,118 +0,0 @@
{
"hero": {
"title": "Laten we bouwen",
"subtitle": "Van idee naar werkend prototype",
"description": "Klaar om jouw software te bouwen? Vul het formulier in en we nemen contact op."
},
"form": {
"title": "Start je speedrun",
"fields": {
"name": {
"label": "Naam",
"placeholder": "Je volledige naam",
"required": true,
"error": "Naam is verplicht"
},
"email": {
"label": "Email",
"placeholder": "je@bedrijf.nl",
"required": true,
"error": "Geldig email adres is verplicht"
},
"company": {
"label": "Bedrijf",
"placeholder": "Je bedrijfsnaam (optioneel)",
"required": false
},
"projectType": {
"label": "Type project",
"placeholder": "Selecteer een type",
"required": true,
"error": "Selecteer een project type",
"options": [
"Web applicatie",
"Mobile app",
"Dashboard / Admin panel",
"API / Backend",
"MVP / Prototype",
"Anders"
]
},
"budget": {
"label": "Budget indicatie",
"placeholder": "Selecteer een range",
"required": false,
"options": [
"< €5.000",
"€5.000 - €10.000",
"€10.000 - €25.000",
"€25.000 - €50.000",
"> €50.000",
"Nog niet bekend"
]
},
"message": {
"label": "Project beschrijving",
"placeholder": "Vertel ons over je project: wat wil je bouwen en waarom?",
"required": true,
"error": "Beschrijving is verplicht",
"minLength": 20
}
},
"buttons": {
"submit": "Verstuur aanvraag",
"submitting": "Versturen..."
},
"success": {
"title": "Aanvraag ontvangen!",
"message": "We hebben je aanvraag ontvangen en nemen contact op. Check je inbox (en spam folder).",
"cta": "Terug naar home"
},
"error": {
"title": "Er ging iets mis",
"message": "We konden je aanvraag niet verwerken. Probeer het opnieuw.",
"retry": "Opnieuw proberen"
}
},
"benefits": {
"title": "Waarom AI Speedrun?",
"items": [
{
"title": "Snelle doorlooptijd",
"description": "Van eerste gesprek tot werkend prototype in korte tijd.",
"icon": "clock"
},
{
"title": "Modern tech stack",
"description": "Next.js, Supabase, AI - battle-tested en toekomstbestendig.",
"icon": "code"
},
{
"title": "Build in public",
"description": "Volg de voortgang live via GitHub en wekelijkse updates.",
"icon": "eye"
}
]
},
"faq": {
"title": "Veelgestelde vragen",
"items": [
{
"question": "Wat voor projecten bouw je?",
"answer": "Een werkend MVP/prototype met moderne tech stack (Next.js, Supabase), gehost op Vercel, met volledige source code toegang via GitHub."
},
{
"question": "Hoe lang duurt het?",
"answer": "Afhankelijk van de scope en complexiteit van het project. Week 1: requirements & design. Week 2-N: development. Laatste week: testing & launch. Je krijgt wekelijkse updates en kan de voortgang live volgen."
},
{
"question": "Wat als ik meer features wil?",
"answer": "We beginnen altijd met een MVP scope. Extra features kunnen daarna als iteraties worden toegevoegd. We helpen je prioriteren wat er in de eerste versie moet zitten."
},
{
"question": "Krijg ik de source code?",
"answer": "Ja, 100%. Alles wordt gebouwd in een GitHub repository waar je volledige toegang toe hebt. Na oplevering is het volledig van jou."
}
]
}
}

View File

@@ -1,216 +0,0 @@
{
"groups": [
{
"id": "foundation",
"title": "Foundation",
"description": "Basis setup en infrastructuur",
"order": 1
},
{
"id": "architecture",
"title": "Architecture",
"description": "Datamodel en FHIR standaarden",
"order": 2
},
{
"id": "features",
"title": "Core Features",
"description": "EPD functionaliteit",
"order": 3
},
{
"id": "cortex",
"title": "Cortex Intent Driven EPD",
"description": "AI-gestuurd commandocentrum met natuurlijke taal interface",
"order": 4
},
{
"id": "infrastructure",
"title": "Infrastructure",
"description": "Ondersteunende systemen",
"order": 5
},
{
"id": "bugs",
"title": "Bugs & Fixes",
"description": "Opgeloste bugs en troubleshooting",
"order": 6
}
],
"categories": [
{
"slug": "authentication",
"title": "Authentication",
"group": "foundation",
"description": "Login, signup en password reset",
"order": 1
},
{
"slug": "database",
"title": "Database & Schema",
"group": "foundation",
"description": "PostgreSQL schema en RLS policies",
"order": 2
},
{
"slug": "environment",
"title": "Environment Setup",
"group": "foundation",
"description": "Development en deployment configuratie",
"order": 3
},
{
"slug": "fhir-datamodel",
"title": "FHIR Datamodel",
"group": "architecture",
"description": "FHIR-compliant datamodel voor GGZ-gegevens volgens MedMIJ standaarden",
"order": 1,
"status": "active"
},
{
"slug": "fhir-api",
"title": "FHIR REST API",
"group": "architecture",
"description": "RESTful FHIR API endpoints voor data-uitwisseling",
"order": 2,
"status": "in-progress"
},
{
"slug": "dashboard",
"title": "Dashboard & Navigation",
"group": "features",
"description": "EPD layout en navigatie",
"order": 4
},
{
"slug": "client-management",
"title": "Client Management",
"group": "features",
"description": "CRUD operations voor cliënten",
"order": 5,
"status": "in_progress"
},
{
"slug": "screening-system",
"title": "Screening Systeem",
"group": "features",
"description": "Screening proces voor nieuwe cliënten met hulpvraag en besluitvorming",
"order": 6,
"status": "active"
},
{
"slug": "intake-system",
"title": "Intake Systeem",
"group": "features",
"description": "Intake gesprekken en gestructureerde intake-workflow",
"order": 7,
"status": "active"
},
{
"slug": "diagnose-systeem",
"title": "Diagnose Systeem",
"group": "features",
"description": "ICD-10 diagnoses vastleggen en beheren",
"order": 8,
"status": "completed"
},
{
"slug": "agenda",
"title": "Agenda Systeem",
"group": "features",
"description": "Behandelaar agenda met kalender, afspraken en verslaglegging integratie",
"order": 8,
"status": "completed"
},
{
"slug": "treatment-planning",
"title": "Behandelplanning",
"group": "features",
"description": "SMART doelen en voortgang monitoring",
"order": 9,
"status": "planned"
},
{
"slug": "ai-features",
"title": "AI Integrations",
"group": "features",
"description": "AI-gestuurde workflows",
"order": 9
},
{
"slug": "verpleegkundige-overdracht",
"title": "Verpleegkundige Rapportage & Overdracht",
"group": "features",
"description": "Centrale module voor verpleegkundige notities, overdracht en AI-gestuurde samenvattingen",
"order": 10,
"status": "completed"
},
{
"slug": "spraakgestuurde-verslaglegging",
"title": "Spraakgestuurde Verslaglegging",
"group": "features",
"description": "Real-time spraakherkenning voor rapportages via Deepgram",
"order": 11,
"status": "completed"
},
{
"slug": "ai-documentatie-assistent",
"title": "AI Documentatie Assistent",
"group": "features",
"description": "Chat widget voor hulp bij het gebruik van het EPD",
"order": 13,
"status": "completed"
},
{
"slug": "cortex-command-center",
"title": "Cortex Command Center",
"group": "cortex",
"description": "AI-gestuurd commandocentrum voor spraak- en tekstopdrachten met multi-intent support",
"order": 1,
"status": "active"
},
{
"slug": "voice-controlled-reporting",
"title": "Spraakgestuurd Rapporteren (v2)",
"group": "features",
"description": "AI-gedreven spraakopname voor dictaat en metingen extractie",
"order": 12,
"status": "planned"
},
{
"slug": "hosting",
"title": "Hosting & Deployment",
"group": "infrastructure",
"description": "Vercel deployment en CI/CD",
"order": 7
},
{
"slug": "interface-design",
"title": "Interface Design System",
"group": "foundation",
"description": "Complete UI/UX specificatie en design patterns",
"order": 4
},
{
"slug": "design-system",
"title": "Design System",
"group": "infrastructure",
"description": "UI components en styling",
"order": 8
},
{
"slug": "performance",
"title": "Performance",
"group": "infrastructure",
"description": "Optimalisaties en monitoring",
"order": 9
},
{
"slug": "webpack-module-resolution",
"title": "Webpack Module Resolution",
"group": "bugs",
"description": "Fix voor Server/Client Component import issues",
"order": 10
}
]
}

View File

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

View File

@@ -1,208 +0,0 @@
---
title: "AI Documentatie Assistent"
category: "ai-documentatie-assistent"
group: "features"
version: "1.0.0"
releaseDate: "2024-12-01"
status: "completed"
description: "Stel vragen over het EPD en krijg direct antwoord via de AI-gestuurde chat widget"
---
## Overview
De AI Documentatie Assistent is een slimme chatbot die u helpt bij het gebruik van het Mini-EPD. Stel vragen in gewoon Nederlands en krijg direct antwoord over functies, procedures en navigatie binnen het systeem.
**Kernvoordelen:**
- Direct antwoord op uw vragen over het EPD
- Beschikbaar op elke pagina binnen het EPD
- Begrijpt context en vervolgvragen
- Verwijst naar relevante menu's en knoppen
---
## Hoe te gebruiken
### Stap 1: Open de assistent
Klik op de **oranje knop met sparkles-icoon** rechtsonder in het scherm. Deze knop is zichtbaar op alle pagina's binnen het EPD.
**Wat u ziet:**
- Een chatvenster opent met een slide-in animatie
- Een welkomstbericht verschijnt
- Drie categorieën om uit te kiezen: Cliënten & Dossiers, Intake & Screening, Spraak & Rapportage
### Stap 2: Kies een onderwerp of stel een vraag
U kunt op twee manieren beginnen:
**Optie A: Kies een categorie**
Klik op een van de drie categorieën om voorbeeldvragen te zien:
- **Cliënten & Dossiers** - Vragen over cliëntbeheer en dossiers
- **Intake & Screening** - Vragen over intakes en screenings
- **Spraak & Rapportage** - Vragen over spraakherkenning en dicteren
Na het kiezen van een categorie ziet u drie voorbeeldvragen. Klik op een vraag om deze direct te stellen.
**Optie B: Typ uw eigen vraag**
Typ uw vraag in het tekstveld onderaan het chatvenster.
**Tips voor betere antwoorden:**
- Stel specifieke vragen over één onderwerp
- Gebruik Nederlandse termen zoals die in het EPD staan
- Bij vervolgvragen hoeft u niet te herhalen - de assistent onthoudt de context
### Stap 3: Verstuur de vraag
Verstuur uw vraag op twee manieren:
- Druk op **Enter** op uw toetsenbord
- Klik op de **oranje verstuurknop** rechts van het tekstveld
**Shift + Enter** voegt een nieuwe regel toe zonder te versturen.
### Stap 4: Lees het antwoord
Het antwoord verschijnt woord-voor-woord in het chatvenster. Dit streaming-effect toont dat de assistent actief aan het antwoorden is.
**Herkenningspunten:**
- Uw vraag staat rechts (oranje achtergrond)
- Het antwoord staat links (grijze achtergrond)
- Tijdens het typen ziet u een knipperende cursor
### Stap 5: Stel vervolgvragen
U kunt direct vervolgvragen stellen. De assistent onthoudt de vorige vragen en antwoorden binnen de sessie.
**Voorbeeld:**
1. "Hoe maak ik een intake aan?"
2. "En hoe voeg ik notities toe?" (de assistent begrijpt dat dit over intakes gaat)
### Stap 6: Sluit het venster
Klik op het **X-icoon** rechtsboven in het chatvenster om het te sluiten. De conversatie blijft bewaard zolang u ingelogd bent.
---
## Functionaliteit
### Onderwerpen waarover u kunt vragen
De assistent kent de volgende onderwerpen:
| Categorie | Voorbeeldvragen |
|-----------|-----------------|
| **Cliëntbeheer** | "Hoe zoek ik een cliënt?", "Hoe maak ik een dossier aan?" |
| **Intake** | "Hoe start ik een intake?", "Waar voeg ik notities toe?" |
| **Screening** | "Wat betekent de screeningscore?", "Welke vragenlijsten zijn er?" |
| **Behandelplan** | "Hoe maak ik een behandeldoel aan?", "Wat zijn interventies?" |
| **Spraakherkenning** | "Hoe gebruik ik de microfoon?", "Waarom werkt dicteren niet?" |
| **Inloggen** | "Hoe reset ik mijn wachtwoord?", "Waarom kan ik niet inloggen?" |
| **Interface** | "Waar vind ik het menu?", "Hoe navigeer ik naar rapportages?" |
### Wat de assistent NIET doet
De assistent is specifiek voor documentatie-hulp:
- **Geen medisch advies** - Vraag geen behandelsuggesties of diagnoses
- **Geen data toegang** - De assistent kan geen cliëntgegevens inzien
- **Geen acties uitvoeren** - De assistent kan niets wijzigen in het EPD
- **Geen externe informatie** - Alleen EPD-documentatie wordt gebruikt
### Streaming antwoorden
Antwoorden verschijnen in real-time, woord voor woord. Dit zorgt voor:
- Snellere eerste feedback (binnen 2 seconden)
- Mogelijkheid om al te lezen terwijl het antwoord nog komt
- Visuele bevestiging dat de vraag wordt verwerkt
### Sessie-geheugen
De assistent onthoudt uw conversatie tijdens de huidige sessie:
- Vervolgvragen worden begrepen in context
- Na uitloggen wordt de conversatie gewist
- Bij herladen van de pagina blijft de conversatie behouden
---
## Veelgestelde vragen
### Waar vind ik de chatknop?
De oranje knop met sparkles-icoon staat rechtsonder op elke EPD-pagina. Als u de knop niet ziet:
- Controleer of u bent ingelogd
- Scroll naar beneden (de knop is fixed gepositioneerd)
### Waarom krijg ik geen antwoord?
Mogelijke oorzaken:
- **Netwerkprobleem**: Controleer uw internetverbinding
- **Sessie verlopen**: Log opnieuw in
- **Te lange vraag**: Houd vragen onder 2000 karakters
Bij aanhoudende problemen verschijnt een rode foutmelding in het chatvenster.
### Kan de assistent mijn cliëntgegevens zien?
Nee. De assistent heeft geen toegang tot de database of cliëntdossiers. De assistent gebruikt alleen de systeemdocumentatie om vragen te beantwoorden.
### Hoe nauwkeurig zijn de antwoorden?
De antwoorden zijn gebaseerd op de officiële EPD-documentatie. De assistent:
- Geeft alleen informatie die in de documentatie staat
- Zegt eerlijk wanneer informatie ontbreekt
- Verzint nooit functies die niet bestaan
### Wordt mijn gesprek opgeslagen?
Nee. Gesprekken worden niet permanent opgeslagen:
- De conversatie bestaat alleen in uw browser-sessie
- Na uitloggen wordt alles gewist
- Gesprekken worden niet gelogd of geanalyseerd
### In welke taal kan ik vragen stellen?
De assistent is geoptimaliseerd voor **Nederlands**. U krijgt ook antwoorden in het Nederlands.
### Waarom zie ik "Even pauze"?
Dit is een demo-omgeving met een vraaglimiet van **10 vragen per minuut**. Deze limiet voorkomt overbelasting van het systeem.
**Wat u ziet:**
- Een oranje melding met uitleg over de limiet
- Een countdown timer die aftelt tot u weer vragen kunt stellen
- Een overzicht van onderwerpen waarvoor de assistent geschikt is
Wacht tot de timer op 0 staat - de chat wordt automatisch weer actief.
---
## Bekende beperkingen
- **Alleen EPD-documentatie**: Geen algemene kennis of externe bronnen
- **Geen acties**: De assistent kan niets wijzigen in het systeem
- **Sessie-gebonden**: Conversatie verdwijnt na uitloggen
- **Tekst-only**: Geen ondersteuning voor afbeeldingen of bestanden
- **Vraaglimiet**: Maximaal 10 vragen per minuut (demo-omgeving)
---
## Keyboard shortcuts
| Actie | Shortcut |
|-------|----------|
| Vraag versturen | Enter |
| Nieuwe regel | Shift + Enter |
| Chat sluiten | Klik X of Escape |
---
## Gerelateerde documentatie
- [Spraakgestuurde Verslaglegging](/documentatie/spraakgestuurde-verslaglegging) - Dicteren van rapportages
- [Interface Design System](/documentatie/interface-design) - UI patterns en navigatie
- [Authentication System](/documentatie/authentication) - Inloggen en toegangsbeheer
---
## Feedback
Heeft u suggesties voor de AI Documentatie Assistent? Ontbreekt er informatie of krijgt u onjuiste antwoorden? Meld dit bij de beheerder zodat we de documentatie kunnen verbeteren.

View File

@@ -1,198 +0,0 @@
---
title: "Authentication & User Management"
category: "authentication"
group: "foundation"
version: "0.1.0"
releaseDate: "2024-11-15"
status: "completed"
description: "Login, signup en password reset functionaliteit via Supabase Auth"
---
## Overview
De authentication flow vormt de basis van het Mini-ECD systeem. Gebruikers kunnen nu veilig inloggen, accounts aanmaken en wachtwoorden resetten via Supabase Auth integratie.
**Key features:**
- Email/password authentication
- Demo account voor quick testing
- Password reset flow
- Session management met JWT tokens
- Security via RLS policies
---
## Features
### Login Flow
![Login screen](/documentatie/authentication/login-screen.png)
*Login formulier met email/password fields en demo account optie*
De login pagina biedt twee opties:
- **Handmatige login:** Email en wachtwoord
- **Demo account:** One-click toegang met `demo@mini-ecd.demo`
**Functionaliteit:**
- Client-side validatie (email format, min 8 characters)
- Server-side authenticatie via Supabase
- Error handling met user-friendly messages
- Redirect naar `/epd/clients` na succesvolle login
**Demo:**
1. Ga naar `/login`
2. Klik op "Demo Account Proberen"
3. Automatisch ingelogd en doorgestuurd
### Signup Flow
Nieuwe gebruikers kunnen zich registreren met:
- Email adres (moet uniek zijn)
- Wachtwoord (min 8 karakters)
- Wachtwoord confirmatie
**Auto-login functionaliteit:**
- Email confirmation is uitgeschakeld voor development
- Direct ingelogd na signup
- Redirect naar EPD applicatie
**Duplicate email handling:**
- Auth hook detecteert bestaande emails
- Automatische login als email al bestaat
- User-friendly message: "Dit emailadres bestaat al. Je bent nu ingelogd!"
### Password Reset
*Coming soon - gepland voor volgende release*
---
## Technical Notes
### Architecture
**Stack:**
- Supabase Auth voor user management
- Next.js 15 Server Components
- Client-side auth helpers in `/lib/auth/client.ts`
**Security:**
- PKCE flow voor OAuth (toekomst)
- JWT tokens in httpOnly cookies
- RLS policies voor data isolatie
- No API keys in frontend code
### File Structure
```
app/
├── login/
│ └── page.tsx # Login/signup form
└── auth/
└── callback/
└── route.ts # OAuth callback (future)
lib/
├── auth/
│ ├── client.ts # loginWithPassword, signUpWithPassword
│ └── server.ts # createClient for server components
supabase/
└── functions/
└── auth-hook/
└── index.ts # Duplicate email detection
```
### Key Functions
```tsx
// Login function
export async function loginWithPassword(email: string, password: string) {
const supabase = createClient()
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
return data
}
// Signup with auto-login
export async function signUpWithPassword(email: string, password: string) {
const supabase = createClient()
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
})
if (error) throw error
return data
}
```
### Auth Hook
Edge Function voor duplicate email detection:
```typescript
// Prevents duplicate signups by auto-logging in existing users
if (event === 'signup' && existingUser) {
return new Response(
JSON.stringify({
error: {
message: 'Dit emailadres is al geregistreerd. Je wordt automatisch ingelogd.',
code: 'user_already_registered',
data: { shouldAutoLogin: true }
}
}),
{ status: 400 }
)
}
```
---
## Metrics
**Development:**
- ⏱️ Build tijd: 6 uur
- 📝 Lines of code: ~380
- 🧪 Tests: Manual testing (geautomatiseerde tests komen later)
**Performance:**
- 🚀 Login response: < 500ms
- 📦 Bundle size: +15kb (auth client)
- 💾 Database queries: 1 per login
**Cost:**
- 💰 Supabase: €0 (free tier)
- 💰 Edge Functions: €0 (within limits)
---
## What's Next
Geplande verbeteringen voor volgende releases:
- 🔜 Password reset flow
- 🔜 Email verification (production)
- 🔜 OAuth providers (Google, GitHub)
- 🔜 Multi-factor authentication
- 🔜 Session management dashboard
---
## Related Links
**Timeline:**
- [Week 1 - Foundation & Marketing](#timeline)
**Code:**
- [Authentication client library](https://github.com/yourusername/mini-ecd/blob/main/lib/auth/client.ts)
- [Auth hook function](https://github.com/yourusername/mini-ecd/blob/main/supabase/functions/auth-hook)
**Documentation:**
- [Supabase Auth Docs](https://supabase.com/docs/guides/auth)

View File

@@ -1,79 +0,0 @@
---
title: "Build Errors na Route Rename Fix"
category: "build-errors-fix"
group: "bugs"
version: "0.2.2"
releaseDate: "2025-01-19"
status: "completed"
description: "Diverse build errors opgelost na het hernoemen van /releases naar /documentatie route"
---
## Wat ging er fout?
Na het hernoemen van de `/releases` route naar `/documentatie` kregen we een serie build errors:
### 1. Bestandsnaam mismatch
**Probleem:** De route heette nu `/documentatie` maar het bestand was nog `lib/mdx/releases.ts`
**Symptoom:** TypeScript kon de module niet vinden
**Fix:** Bestand hernoemd naar `lib/mdx/documentatie.ts` en alle imports bijgewerkt
### 2. Server/Client component conflicts
**Probleem:** Componenten zonder `'use client'` directive werden gebruikt in interactieve contexten
**Symptoom:** Cryptische webpack error: "Cannot read properties of undefined (reading 'call')"
**Fix:** `'use client'` toegevoegd aan 7 componenten:
- UI componenten (buttons, bento-grid, hero sections)
- Marketing componenten (sidebar wrappers, skeletons)
### 3. Button component in BentoGrid
**Probleem:** BentoGrid importeerde Button component met complexe Radix UI dependencies
**Symptoom:** Webpack module resolution errors bij het laden van de login pagina
**Fix:** Button import verwijderd, vervangen door simpele `<a>` tag met inline styling
## Wat hebben we gefixed?
### Hernoem operatie
- ✅ `lib/mdx/releases.ts` → `lib/mdx/documentatie.ts`
- ✅ 4 bestanden bijgewerkt met nieuwe import pads
- ✅ Alle routes werken met nieuwe naam
### Component fixes
**UI Components:**
- `components/ui/ai-button.tsx` - AI geanimeerde button
- `components/ui/button.tsx` - Basis button component
- `components/ui/bento-grid.tsx` - Grid layout voor features
- `components/ui/hero-section-2.tsx` - Hero sectie met animaties
- `components/ui/sign-in.tsx` - Login formulier component
**App Components:**
- `app/epd/clients/components/client-list-skeleton.tsx` - Loading skelet
- `app/(marketing)/documentatie/components/release-sidebar-wrapper.tsx` - Sidebar wrapper
## Resultaat
**Voor de fix:**
- ❌ Build faalde met TypeScript errors
- ❌ Login pagina crashte
- ❌ Documentatie pagina niet toegankelijk
- ❌ EPD cliënten lijst crashte
**Na de fix:**
- ✅ Build slaagt in 11.5s
- ✅ Alle 25 routes werken
- ✅ 4 documentatie pagina's gegenereerd
- ✅ Geen runtime errors meer
- ✅ Applicatie deployment-ready
## Geleerde lessen
1. **Consistentie is key** - Bij het hernoemen van routes ook de onderliggende bestanden meteen aanpassen
2. **'use client' is essentieel** - Elke component met interactiviteit (hooks, state, animations) heeft deze directive nodig
3. **Simpel > Complex** - Soms is een `<a>` tag beter dan een complexe Button component met dependencies
4. **Clean cache helpt** - Bij persistente errors: `rm -rf .next` en opnieuw starten
## Impact
- **Development time:** ~2 uur debugging en fixes
- **Files changed:** 11 bestanden aangepast
- **Build time:** Geen impact (11.5s voor/na)
- **Bundle size:** Minimale toename (+0.1kb door wrappers)
- **User impact:** Geen - bugs gevonden voor deployment

View File

@@ -1,61 +0,0 @@
---
title: "Client Management"
category: "client-management"
group: "features"
version: "1.0.0"
releaseDate: "TBD"
status: "in_progress"
description: "CRUD operations voor cliënten - toevoegen, bewerken, zoeken en beheren van cliëntgegevens"
---
## Overview
Dit document beschrijft het complete client management systeem voor het Mini-ECD prototype. Het omvat alle functionaliteit voor het beheren van cliëntgegevens, van intake tot uitstroom.
**Kernfunctionaliteit:**
- Cliënt toevoegen en registreren
- Cliëntgegevens bewerken en bijwerken
- Zoeken en filteren in cliëntenlijst
- Cliëntstatus beheren (Actief, Wachtlijst, Uitgeschreven)
---
## Binnenkort beschikbaar
Deze documentatie is nog in ontwikkeling. De volgende onderwerpen worden toegevoegd:
**Cliënt Registratie:**
- Nieuwe cliënt formulier
- Verplichte velden en validatie
- BSN verificatie
- Verzekering gegevens
**Cliëntenlijst:**
- Overzicht en tabel layout
- Zoeken en filters
- Sortering en paginering
- Recent bekeken cliënten
**Cliënt Profiel:**
- Basisgegevens weergave
- Contactinformatie
- Medische informatie
- Privacy en AVG compliance
**Status Management:**
- Actieve cliënten
- Wachtlijst beheer
- Uitschrijving proces
- Archivering
---
## Gerelateerde Documentatie
**Interface:**
- [Interface Design System](/documentatie/interface-design) - UI/UX specificaties
**Toekomstige Features:**
- Intake Systeem (binnenkort)
- Behandelplanning (binnenkort)
- Database Schema (binnenkort)

View File

@@ -1,176 +0,0 @@
---
title: "Blocks"
category: "cortex-blocks"
group: "cortex"
version: "2.0.0"
releaseDate: "2025-01-03"
status: "active"
description: "Dagnotitie, Zoeken, Overdracht en andere actie-blocks"
---
## Wat zijn Blocks?
Blocks zijn de actie-schermen die Cortex opent na een commando. Elk block biedt een specifieke functionaliteit.
| Block | Functie | Opent bij |
|-------|---------|-----------|
| **Dagnotitie** | Notitie invoeren | "Notitie...", "Rapportage..." |
| **Zoeken** | Patiënt zoeken | "Zoek...", "Patiënt..." |
| **Overdracht** | Dienst overdracht | "Overdracht" |
| **Patient Dashboard** | Patiënt overzicht | Na patiënt selectie |
Elk block kan worden gesloten met `Escape` of de sluitknop.
---
## Dagnotitie Block
Het Dagnotitie block opent wanneer u een notitie wilt maken.
### Openen
Via commando:
```
"Notitie voor Jan: medicatie ingenomen"
"Rapportage: wond verzorgd, ziet er goed uit"
"Observatie Marie: onrustig gedrag na bezoek"
```
### Velden
| Veld | Beschrijving |
|------|--------------|
| **Patiënt** | Automatisch ingevuld bij @mention of actieve patiënt |
| **Categorie** | Keuze uit 5 types (zie onder) |
| **Notitie** | Vrije tekst (max 500 karakters) |
| **Overdracht** | Checkbox om in overdracht op te nemen |
### Categorieën
| Categorie | Kleur | Gebruik |
|-----------|-------|---------|
| Medicatie | Blauw | Medicatie-gerelateerde observaties |
| ADL/verzorging | Groen | Activiteiten dagelijks leven |
| Gedragsobservatie | Paars | Gedrag en stemming |
| Incident | Rood | Incidenten en bijzonderheden |
| Algemene observatie | Grijs | Overige observaties |
### Opslaan
- Klik op **Opslaan** knop
- Of druk `⌘Enter` (Mac) / `Ctrl+Enter` (Windows)
### Slimme Prefill
Als een patiënt actief is in de context bar, wordt deze automatisch ingevuld.
---
## Zoeken Block
Het Zoeken block helpt u patiënten te vinden.
### Openen
Via commando:
```
"Zoek Jansen"
"Patiënt Marie"
"Vind client 12345"
```
### Zoeken op
| Zoekveld | Voorbeeld |
|----------|-----------|
| Naam | "Jan Jansen" |
| Achternaam | "Jansen" |
| BSN | "123456789" |
| Clientnummer | "C-2024-001" |
### Resultaten
- Toont matchende patiënten met avatar en basisinfo
- Klik om te selecteren als actieve patiënt
- Dubbel-klik om direct naar dossier te gaan
---
## Overdracht Block
Het Overdracht block toont het overzicht voor dienstoverdracht.
### Openen
Via commando:
```
"Overdracht"
"Dienstoverdracht"
```
### Inhoud
Per patiënt met notities van de huidige dienst:
- Aantal notities
- Alerts en waarschuwingen
- Risico-indicatoren
### AI Samenvatting
1. Selecteer een patiënt in het overzicht
2. Klik op **"Genereer samenvatting"**
3. AI analyseert alle notities en geeft:
- Korte samenvatting (1-2 zinnen)
- Aandachtspunten met urgentie
- Actiepunten voor volgende dienst
---
## Patient Dashboard Block
Het Patient Dashboard toont het complete overzicht van een patiënt.
### Openen
- Selecteer een patiënt uit zoekresultaten
- Via commando: "Open dossier Jan"
- Via @mention gevolgd door "dossier"
### Secties
| Sectie | Inhoud |
|--------|--------|
| **Header** | Naam, foto, basisgegevens |
| **Timeline** | Recente notities en events |
| **Medicatie** | Huidige medicatie |
| **Diagnoses** | Actieve diagnoses |
| **Afspraken** | Geplande afspraken |
---
## Agenda Block
Het Agenda block toont afspraken en planning.
### Openen
Via commando:
```
"Agenda vandaag"
"Afspraken morgen"
"Agenda deze week"
```
### Weergave
- **Dagweergave**: Uren van de dag met afspraken
- **Weekweergave**: Overzicht van de week
- Kleur-gecodeerd per afspraaktype
### Afspraken Beheren
| Actie | Commando |
|-------|----------|
| Plannen | "Plan afspraak met Jan volgende week" |
| Annuleren | "Zeg Jan af voor vandaag" |
| Verzetten | "Verzet afspraak Jan naar vrijdag" |

View File

@@ -1,154 +0,0 @@
---
title: "Intent Systeem"
category: "cortex-intent-systeem"
group: "cortex"
version: "2.0.0"
releaseDate: "2025-01-03"
status: "active"
description: "Hoe Cortex opdrachten classificeert en verwerkt"
---
## Hoe Cortex Werkt
Cortex gebruikt een drie-lagen architectuur om uw opdrachten te begrijpen en uit te voeren.
---
## Layer 1: Reflex Arc
De eerste laag is een snelle, lokale pattern matcher die veelvoorkomende opdrachten direct herkent.
**Kenmerken:**
- Verwerking in < 10ms
- Geen AI/netwerk nodig
- Herkent ~80% van dagelijkse opdrachten
**Voorbeelden die direct worden herkend:**
| Opdracht | Intent |
|----------|--------|
| "Agenda vandaag" | `agenda_query` |
| "Zoek Jansen" | `zoeken` |
| "Notitie: medicatie gegeven" | `dagnotitie` |
| "Overdracht" | `overdracht` |
Wanneer de Reflex Arc niet zeker genoeg is (confidence < 0.7), escaleert het naar Layer 2.
---
## Layer 2: Orchestrator
De tweede laag gebruikt Claude AI voor complexe classificatie.
**Wanneer gebruikt:**
- Multi-intent opdrachten ("Zeg Jan af en maak notitie")
- Verwijzingen naar context ("zijn medicatie", "afspraak van morgen")
- Relatieve tijd ("volgende week", "over 3 dagen")
- Ambigue opdrachten
**Voorbeeld verwerking:**
Invoer: `"Zeg Jan af voor morgen en noteer dat hij ziek is"`
Orchestrator output:
```json
{
"intents": [
{
"type": "cancel_appointment",
"patient": "Jan",
"date": "2025-01-04"
},
{
"type": "dagnotitie",
"patient": "Jan",
"content": "hij is ziek"
}
]
}
```
---
## Layer 3: Nudge
De derde laag geeft proactieve suggesties na voltooide acties.
**Voorbeeld:**
- U maakt een notitie over wondzorg
- Nudge suggereert: "Wondcontrole inplannen over 3 dagen?"
Zie [Proactieve Suggesties](/documentatie/cortex-nudge) voor meer details.
---
## Intent Types
Cortex herkent de volgende intents:
| Intent | Beschrijving | Trigger woorden |
|--------|--------------|-----------------|
| `dagnotitie` | Notitie aanmaken | notitie, rapportage, observatie |
| `zoeken` | Patiënt zoeken | zoek, vind, patiënt |
| `overdracht` | Overdracht openen | overdracht, dienstoverdracht |
| `agenda_query` | Agenda bekijken | agenda, afspraken, planning |
| `create_appointment` | Afspraak maken | plan, maak afspraak, inplannen |
| `cancel_appointment` | Afspraak annuleren | annuleer, zeg af, cancel |
| `reschedule_appointment` | Afspraak verzetten | verzet, verplaats, reschedule |
---
## Context-Aware Classificatie
Cortex gebruikt context om opdrachten beter te begrijpen:
### Actieve Patiënt
Als er een patiënt geselecteerd is in de context bar:
| Invoer | Interpretatie |
|--------|---------------|
| "Maak notitie voor haar" | Notitie voor actieve patiënt |
| "Plan afspraak met hem" | Afspraak voor actieve patiënt |
| "Zijn medicatie" | Medicatie van actieve patiënt |
### Recente Context
Cortex onthoudt recente acties:
| Situatie | Invoer | Interpretatie |
|----------|--------|---------------|
| Net Jan gezocht | "Open dossier" | Open Jan's dossier |
| Net afspraak gemaakt | "Verzet naar vrijdag" | Verzet zojuist gemaakte afspraak |
---
## Confidence Scores
Elke classificatie heeft een confidence score (0.0 - 1.0):
| Score | Betekenis | Actie |
|-------|-----------|-------|
| ≥ 0.9 | Zeer zeker | Direct uitvoeren |
| 0.7 - 0.9 | Waarschijnlijk | Uitvoeren, toon bevestiging |
| 0.5 - 0.7 | Onzeker | Vraag verduidelijking |
| < 0.5 | Onduidelijk | Escaleer naar AI of vraag opnieuw |
---
## Foutafhandeling
### Onbekende Intent
Als Cortex een opdracht niet begrijpt:
1. Toont een vriendelijke melding
2. Suggereert mogelijke alternatieven
3. Biedt optie om te chatten voor hulp
### Meerdere Matches
Als een opdracht meerdere patiënten of opties kan matchen:
1. Toont een keuzelijst
2. Gebruiker selecteert de juiste optie
3. Actie wordt uitgevoerd

View File

@@ -1,142 +0,0 @@
---
title: "Keyboard Shortcuts"
category: "cortex-keyboard-shortcuts"
group: "cortex"
version: "2.0.0"
releaseDate: "2025-01-03"
status: "active"
description: "Sneltoetsen voor efficiënt werken"
---
## Overzicht
Cortex ondersteunt keyboard shortcuts voor sneller werken. De shortcuts werken op zowel Mac als Windows/Linux.
---
## Globale Shortcuts
Deze shortcuts werken overal in het EPD:
| Mac | Windows/Linux | Actie |
|-----|---------------|-------|
| `⌘K` | `Ctrl+K` | Open Cortex / Focus invoerveld |
| `⌘P` | `Ctrl+P` | Open patiënt sidebar |
---
## Invoerveld Shortcuts
Wanneer het Cortex invoerveld actief is:
| Mac | Windows/Linux | Actie |
|-----|---------------|-------|
| `Enter` | `Enter` | Verstuur opdracht |
| `⌘Enter` | `Ctrl+Enter` | Snel versturen (alternatief) |
| `Escape` | `Escape` | Wis invoerveld / Sluit dropdown |
| `↑` | `↑` | Vorige opdracht (history) |
| `↓` | `↓` | Volgende opdracht (history) |
---
## Block Shortcuts
Wanneer een block (formulier) open is:
| Mac | Windows/Linux | Actie |
|-----|---------------|-------|
| `⌘Enter` | `Ctrl+Enter` | Snel opslaan |
| `Escape` | `Escape` | Sluit block |
| `Tab` | `Tab` | Volgende veld |
| `Shift+Tab` | `Shift+Tab` | Vorige veld |
---
## @Mention Dropdown Shortcuts
Wanneer de @mention dropdown zichtbaar is:
| Mac | Windows/Linux | Actie |
|-----|---------------|-------|
| `↓` | `↓` | Volgende optie |
| `↑` | `↑` | Vorige optie |
| `Enter` | `Enter` | Selecteer huidige optie |
| `Escape` | `Escape` | Sluit dropdown |
| `1` | `1` | Selecteer eerste optie |
| `2` | `2` | Selecteer tweede optie |
| `3` | `3` | Selecteer derde optie |
---
## Patiënt Sidebar Shortcuts
Wanneer de patiënt sidebar open is:
| Mac | Windows/Linux | Actie |
|-----|---------------|-------|
| `Escape` | `Escape` | Sluit sidebar |
| `↓` | `↓` | Volgende patiënt in lijst |
| `↑` | `↑` | Vorige patiënt in lijst |
| `Enter` | `Enter` | Selecteer patiënt |
---
## Workflow Voorbeelden
### Snel een Notitie Maken
```
1. ⌘K → Focus op Cortex
2. @jan → Typ @mention
3. Enter → Selecteer patiënt
4. " koorts" → Typ notitie
5. Enter → Verstuur
6. ⌘Enter → Opslaan in block
```
### Snel van Patiënt Wisselen
```
1. ⌘P → Open sidebar
2. ↓↓ → Navigeer naar patiënt
3. Enter → Selecteer
4. Escape → Sluit sidebar
```
### Snel Agenda Bekijken
```
1. ⌘K → Focus op Cortex
2. "agenda" → Typ commando
3. Enter → Verstuur
```
---
## Shortcut Hints
Cortex toont shortcut hints op knoppen:
| Knop | Hint |
|------|------|
| Submit knop | `⌘↵` of `Ctrl+↵` |
| Opslaan knop | `⌘↵` of `Ctrl+↵` |
Deze hints zijn platform-specifiek (Mac of Windows/Linux).
---
## Toegankelijkheid
### Schermlezer Ondersteuning
- Alle shortcuts hebben ARIA labels
- Focus indicatoren zijn duidelijk zichtbaar
- Tab-volgorde is logisch
### Alternatieve Navigatie
Als shortcuts niet werken:
- Alle functies zijn ook bereikbaar via muis
- Knoppen hebben tooltips met shortcut info
- Voice input is beschikbaar via microfoon knop

View File

@@ -1,157 +0,0 @@
---
title: "@Mention Systeem"
category: "cortex-mention-systeem"
group: "cortex"
version: "2.0.0"
releaseDate: "2025-01-03"
status: "active"
description: "Patiënt selectie via @mentions en sidebar"
---
## Overzicht
Het @Mention systeem biedt drie manieren om patiënten te selecteren in Cortex.
---
## Methode 1: @Mention (Aanbevolen)
Typ `@` gevolgd door een naam direct in uw bericht:
1. Typ `@jan` in het invoerveld
2. Een dropdown verschijnt met zoekresultaten
3. Klik op de juiste patiënt
4. De naam wordt ingevoegd: `@Jan Jansen`
5. Ga verder met uw opdracht
**Voorbeeld:** `@jan heeft griep` → Notitie voor Jan Jansen
### Dropdown Navigatie
| Toets | Actie |
|-------|-------|
| `↓` / `↑` | Navigeer door resultaten |
| `Enter` | Selecteer huidige optie |
| `Escape` | Sluit dropdown |
| `1` `2` `3` | Selecteer optie op nummer |
### Zoeken in @Mention
De dropdown zoekt op:
- Voornaam
- Achternaam
- Combinatie
Typ minimaal 2 karakters na `@` voor resultaten.
---
## Methode 2: Patient Sidebar
Druk `⌘P` (Mac) of `Ctrl+P` (Windows) om de patiënt-sidebar te openen.
### Sidebar Onderdelen
| Sectie | Beschrijving |
|--------|--------------|
| **Zoekbalk** | Zoek op naam, BSN of clientnummer |
| **Resultaten** | Matchende patiënten |
| **Recent** | 5 laatst bezochte patiënten |
### Gebruik
1. **Zoeken**: Typ naam, BSN of clientnummer
2. **Recent**: Klik op een recente patiënt
3. **Selecteren**: Klik op een patiënt om deze actief te maken
4. **Sluiten**: Druk `Escape` of klik buiten de sidebar
De geselecteerde patiënt verschijnt in de **context bar** bovenaan.
### Recente Patiënten
De sidebar onthoudt uw 5 laatst bezochte patiënten:
- Automatisch bijgewerkt bij elke patiënt-interactie
- Snelle toegang zonder te zoeken
- Toont naam en laatste bezoek-datum
---
## Methode 3: Context-Aware
Wanneer een patiënt actief is in de context bar, begrijpt Cortex verwijzingen.
### Voornaamwoorden
| Invoer | Interpretatie |
|--------|---------------|
| "Maak notitie voor haar" | Notitie voor actieve patiënt |
| "Plan afspraak met hem" | Afspraak voor actieve patiënt |
| "Zijn medicatie" | Medicatie van actieve patiënt |
| "Haar dossier" | Dossier van actieve patiënt |
### Impliciete Referenties
| Invoer | Interpretatie |
|--------|---------------|
| "Maak notitie" | Notitie voor actieve patiënt (indien aanwezig) |
| "Open dossier" | Dossier van actieve patiënt |
| "Volgende afspraak" | Volgende afspraak met actieve patiënt |
### Wanneer Geen Patiënt Actief
Als er geen patiënt in de context bar staat en de opdracht een patiënt vereist:
1. Cortex vraagt: "Voor welke patiënt?"
2. U kunt dan een @mention gebruiken of zoeken
3. Na selectie wordt de actie uitgevoerd
---
## Context Bar
De context bar bovenaan Cortex toont de actieve patiënt.
### Elementen
| Element | Functie |
|---------|---------|
| **Avatar** | Profielfoto of initialen |
| **Naam** | Volledige naam patiënt |
| **Wissen knop** | Verwijder actieve patiënt (×) |
### Actieve Patiënt Wissen
- Klik op de **×** naast de naam
- Of selecteer een andere patiënt
- Of druk `Escape` wanneer geen block open is
---
## Best Practices
### Snelste Workflow
1. Begin met `@` + eerste letters van naam
2. Selecteer uit dropdown (pijltjes + Enter)
3. Typ uw opdracht door
**Voorbeeld flow:**
```
@jan[Enter] heeft koorts, 38.2 graden
```
### Bij Veel Patiënten met Zelfde Naam
Als er meerdere "Jan" patiënten zijn:
1. Typ meer van de achternaam: `@jan jans`
2. Of gebruik BSN: `@123456789`
3. Of gebruik clientnummer: `@C-2024-001`
### Wisselen Tussen Patiënten
Voor snelle navigatie:
1. `⌘P` opent sidebar met recente patiënten
2. Klik op andere patiënt
3. Context bar update direct
4. Ga verder met opdrachten voor nieuwe patiënt

View File

@@ -1,167 +0,0 @@
---
title: "Proactieve Suggesties"
category: "cortex-nudge"
group: "cortex"
version: "2.0.0"
releaseDate: "2025-01-03"
status: "active"
description: "Automatische suggesties na acties"
---
## Wat is Nudge?
Nudge is het proactieve suggestie-systeem van Cortex. Na bepaalde acties geeft Cortex automatisch relevante follow-up suggesties.
**Doel:**
- Belangrijke vervolgacties niet vergeten
- Protocollen ondersteunen
- Workflow stroomlijnen
---
## Hoe het Werkt
1. U voert een actie uit (bijv. notitie maken)
2. Nudge analyseert de inhoud
3. Bij een match toont Cortex een suggestie
4. U kiest of u de suggestie accepteert
---
## Huidige Nudge Regels
### Wondzorg Protocol
**Trigger:** Notitie bevat een van deze woorden:
- "wond"
- "wondzorg"
- "verbandwissel"
- "decubitus"
- "zwelling"
**Suggestie:** "Wondcontrole inplannen over 3 dagen?"
**Actie bij acceptatie:**
- Opent afspraak formulier
- Patiënt vooringevuld
- Datum +3 dagen vooringevuld
- Type: "Wondcontrole"
### Medicatie Wijziging
**Trigger:** Notitie bevat:
- "medicatie gewijzigd"
- "dosering aangepast"
- "nieuwe medicatie"
**Suggestie:** "Medicatie review inplannen over 1 week?"
### Val Incident
**Trigger:** Notitie met categorie "Incident" bevat:
- "gevallen"
- "val"
- "uitgegleden"
**Suggestie:** "Valrisico herbeoordeling inplannen?"
---
## Suggestie Interface
### Toast Notificatie
De suggestie verschijnt als een toast rechtsonder in het scherm:
```
┌────────────────────────────────────┐
│ 💡 Suggestie │
│ │
│ Wondcontrole inplannen over │
│ 3 dagen? │
│ │
│ [Nee, bedankt] [Ja, doe maar] │
└────────────────────────────────────┘
```
### Reageren
| Knop | Actie |
|------|-------|
| **Ja, doe maar** | Voert de gesuggereerde actie uit |
| **Nee, bedankt** | Sluit de suggestie |
| *Wachten* | Na 10 seconden verdwijnt automatisch |
### Keyboard
| Toets | Actie |
|-------|-------|
| `Enter` | Accepteer suggestie |
| `Escape` | Wijs suggestie af |
---
## Nudge Configuratie
### Per Gebruiker
In de toekomst kunnen gebruikers per nudge-type instellen:
- Aan/uit per protocol
- Timing aanpassen (bijv. 2 dagen i.p.v. 3)
- Altijd tonen vs. alleen bij hoge urgentie
### Per Organisatie
Organisaties kunnen eigen nudge-regels definiëren:
- Specifieke protocollen
- Compliance vereisten
- Workflow optimalisaties
---
## Nudge vs. Alerts
| Aspect | Nudge | Alert |
|--------|-------|-------|
| **Trigger** | Na actie | Continue monitoring |
| **Urgentie** | Suggestie | Waarschuwing |
| **Actie vereist** | Optioneel | Vaak verplicht |
| **Weergave** | Toast (10 sec) | Persistent |
---
## Best Practices
### Wanneer Accepteren
- Bij routine controles (wondzorg, medicatie review)
- Als het protocol dit voorschrijft
- Als u anders zou vergeten
### Wanneer Afwijzen
- Als de controle al gepland is
- Als het niet van toepassing is op deze patiënt
- Bij one-time observaties zonder follow-up
### Handmatig Triggeren
U kunt ook handmatig een suggestie-actie uitvoeren:
```
"Plan wondcontrole voor Jan over 3 dagen"
```
---
## Toekomstige Uitbreidingen
**Gepland:**
- Medicatie interactie checks
- Lab waarden follow-up
- Behandelplan milestones
- Team notificaties
**In overweging:**
- Machine learning voor patronen
- Gepersonaliseerde timing
- Cross-patient insights

View File

@@ -1,121 +0,0 @@
---
title: "Overzicht"
category: "cortex-overzicht"
group: "cortex"
version: "2.0.0"
releaseDate: "2025-01-03"
status: "active"
description: "Introductie en basis gebruik van Cortex"
---
## Wat is Cortex?
Cortex is het intelligente commandocentrum van het EPD. In plaats van door menu's te navigeren, spreekt of typt u gewoon wat u wilt doen. Cortex begrijpt uw intentie en voert de actie uit.
**Kernfunctionaliteit:**
- Dagnotities maken via spraak of tekst
- Patiënten zoeken en selecteren
- Agenda bekijken en afspraken beheren
- Overdracht voorbereiden met AI-samenvatting
- Meerdere acties in één opdracht ("Zeg Jan af en maak notitie")
- Proactieve suggesties na acties
---
## Cortex Openen
### Via Keyboard Shortcut
Druk `⌘K` (Mac) of `Ctrl+K` (Windows) om Cortex te openen vanuit elke pagina in het EPD.
### Via het Dashboard
Navigeer naar **Dashboard** om het Cortex command center te zien. Het invoerveld bevindt zich onderaan het scherm.
---
## Commando's Geven
### Tekst Invoer
1. Focus op het invoerveld (automatisch of via `⌘K`)
2. Typ uw opdracht in natuurlijke taal
3. Druk `Enter` of `⌘Enter` om te versturen
**Voorbeelden:**
- "Notitie voor Jan: medicatie ingenomen"
- "Agenda vandaag"
- "Zoek Jansen"
### Spraak Invoer
1. Klik op de **microfoon knop** naast het invoerveld
2. Spreek uw opdracht in
3. De tekst verschijnt automatisch en wordt verwerkt
### Meerdere Acties
Cortex begrijpt samengestelde opdrachten:
| Invoer | Resultaat |
|--------|-----------|
| "Zeg Jan af en maak notitie: hij is ziek" | 1. Afspraak geannuleerd 2. Notitie aangemaakt |
| "Zoek Marie en open haar dossier" | 1. Patiënt gevonden 2. Dashboard geopend |
---
## Context Bar
De context bar bovenaan het command center toont:
| Element | Beschrijving |
|---------|--------------|
| **Actieve patiënt** | Avatar en naam van geselecteerde patiënt |
| **Dienst indicator** | Huidige dienst met icoon |
| **Patiënt knop** | Opens patient sidebar (`⌘P`) |
**Diensten:**
| Dienst | Tijden | Icoon |
|--------|--------|-------|
| Nacht | 00:00 - 07:00 | Maan |
| Ochtend | 07:00 - 12:00 | Zon opkomst |
| Middag | 12:00 - 17:00 | Zon |
| Avond | 17:00 - 00:00 | Zon ondergang |
---
## Tips voor Gebruik
**Efficiënt werken:**
- Gebruik `@mention` voor snelle patiënt selectie in uw bericht
- `⌘K` werkt overal in het EPD - spring snel naar Cortex
- Recente patiënten staan in de sidebar (`⌘P`)
**Notities maken:**
- Selecteer eerst een patiënt, dan is deze automatisch ingevuld
- Gebruik `⌘Enter` om snel op te slaan
- Markeer belangrijke notities voor overdracht
**Samengestelde opdrachten:**
- Combineer acties met "en": "Zoek Jan en maak notitie"
- Cortex begrijpt "hij/zij/haar" als verwijzing naar actieve patiënt
**Bij onduidelijkheid:**
- Cortex vraagt om verduidelijking in plaats van een foutmelding
- Herformuleer uw opdracht als het resultaat niet klopt
---
## Veelgestelde Vragen
### Waarom duurt sommige opdrachten langer?
Eenvoudige opdrachten ("Agenda vandaag") worden direct verwerkt. Complexe opdrachten met meerdere acties worden door AI geanalyseerd, wat ~0.5 seconde kan duren.
### Hoe weet Cortex wie "hij" of "zij" is?
Cortex kijkt naar de actieve patiënt in de context bar. Selecteer eerst een patiënt via `@mention` of `⌘P`.
### Wat als er geen internet is?
Basiscommando's blijven werken. AI-functies (samenvatting, complexe opdrachten) zijn tijdelijk niet beschikbaar. Een banner toont de offline status.

View File

@@ -1,211 +0,0 @@
---
title: "Diagnose Systeem"
category: "diagnose-systeem"
group: "features"
version: "1.0.0"
releaseDate: "2025-12-17"
status: "completed"
description: "Vastleggen en beheren van diagnoses met ICD-10 codering voor GGZ-behandelaars"
---
## Overzicht
Het diagnose systeem biedt een complete workflow voor het registreren en beheren van diagnoses binnen de GGZ. Diagnoses worden gekoppeld aan intakes en gebruiken de ICD-10 classificatie voor gestandaardiseerde codering.
**Kernfunctionaliteit:**
- Master-detail layout voor overzichtelijk diagnosebeheer
- ICD-10 codering met zoekfunctionaliteit
- Onderscheid tussen hoofd- en nevendiagnoses
- Status tracking (actief, in remissie, opgelost, inactief)
- Koppeling aan intakes voor volledige dossiervorming
---
## Diagnose Overzicht
### Master-Detail Layout
De diagnose pagina gebruikt een master-detail layout:
- **Linker paneel (Master)**: Lijst van alle diagnoses
- **Rechter paneel (Detail)**: Formulier voor bekijken/bewerken/toevoegen
**Navigatie:**
1. Open het cliëntdossier
2. Klik op "Diagnoses" in de zijbalk
3. U ziet direct het overzicht van alle diagnoses
### Diagnose Lijst
De lijst toont diagnoses in de volgende volgorde:
1. **Hoofddiagnoses** eerst
2. **Actieve diagnoses** boven inactieve
3. **Meest recent** bovenaan binnen categorieën
**Weergegeven informatie per diagnose:**
- ICD-10 code en beschrijving
- Type (hoofd- of nevendiagnose)
- Status met kleurcodering
- Ernst niveau
- Gekoppelde intake
---
## Nieuwe Diagnose Toevoegen
### Stappen
1. Klik op **"Nieuw"** rechtsboven in de diagnoselijst
2. Vul het formulier in:
- **Intake**: Selecteer de bijbehorende intake
- **ICD-10 Code**: Zoek en selecteer de diagnose
- **Ernst**: Licht, Matig of Ernstig
- **Type**: Hoofddiagnose of Nevendiagnose
- **Status**: Actief, In remissie, Opgelost of Inactief
- **Onderbouwing**: Klinische redenering (optioneel)
3. Klik op **"Opslaan"**
### Intake Vereiste
Een diagnose moet altijd gekoppeld worden aan een intake. Als er nog geen intake bestaat:
1. Ga naar het tabblad "Intakes" van de patiënt
2. Maak eerst een intake aan
3. Keer terug naar Diagnoses om een nieuwe diagnose toe te voegen
---
## ICD-10 Code Zoeken
### Zoekfunctionaliteit
De ICD-10 combobox ondersteunt zoeken op:
- **Code** (bijv. "F32" voor depressie)
- **Beschrijving** (bijv. "depressieve episode")
- **Trefwoorden** (bijv. "angst", "stemmingsstoornis")
### Veelgebruikte Codes
Bij een lege zoekquery worden automatisch veelgebruikte GGZ-codes getoond:
- Depressieve stoornissen (F32, F33)
- Angststoornissen (F40, F41)
- Persoonlijkheidsstoornissen (F60)
- PTSS (F43.1)
- ADHD (F90)
### Zoeken
1. Klik in het ICD-10 zoekveld
2. Begin met typen (code of beschrijving)
3. Resultaten verschijnen na 200ms
4. Klik op de gewenste code om te selecteren
---
## Diagnose Types
### Hoofddiagnose
De primaire diagnose die de behandeling bepaalt:
- Eén hoofddiagnose per behandeltraject aanbevolen
- Bepaalt de zorgzwaarte en behandelindicatie
- Wordt gebruikt voor declaratie en verantwoording
### Nevendiagnose
Bijkomende diagnoses die de behandeling beïnvloeden:
- Comorbiditeit documenteren
- Relevante somatische aandoeningen
- Contextuele factoren
---
## Status en Ernst
### Klinische Status
| Status | Betekenis |
|--------|-----------|
| **Actief** | Huidige diagnose, in behandeling |
| **In remissie** | Symptomen verminderd, nog wel aanwezig |
| **Opgelost** | Diagnose niet meer van toepassing |
| **Inactief** | Historische diagnose |
### Ernst Niveau
| Ernst | Indicatie |
|-------|-----------|
| **Licht** | Beperkte functionele beperkingen |
| **Matig** | Matige impact op dagelijks functioneren |
| **Ernstig** | Aanzienlijke functionele beperkingen |
---
## Diagnose Bewerken
1. Klik op een diagnose in de lijst
2. Het detail formulier toont de huidige gegevens
3. Pas de gewenste velden aan:
- ICD-10 code wijzigen
- Status bijwerken (bijv. actief → in remissie)
- Ernst aanpassen
- Onderbouwing toevoegen of wijzigen
4. Klik op **"Opslaan"**
**Let op:** De gekoppelde intake kan niet worden gewijzigd. Verwijder de diagnose en maak een nieuwe aan als de koppeling fout is.
---
## Diagnose Verwijderen
1. Selecteer de diagnose in de lijst
2. Klik op **"Verwijderen"** linksonder in het formulier
3. Bevestig met **"Ja, verwijder"**
**Waarschuwing:** Verwijderde diagnoses kunnen niet worden hersteld. Overweeg de status te wijzigen naar "Opgelost" in plaats van verwijderen.
---
## Datamodel
### Conditions Tabel
Diagnoses worden opgeslagen in de `conditions` tabel (FHIR Condition resource):
| Veld | Beschrijving |
|------|--------------|
| `code_code` | ICD-10 code |
| `code_display` | Beschrijving |
| `code_system` | "ICD-10" |
| `clinical_status` | active/remission/resolved/inactive |
| `severity_display` | licht/matig/ernstig |
| `category` | primary-diagnosis/encounter-diagnosis |
| `note` | Onderbouwing |
| `encounter_id` | Koppeling naar intake |
| `recorded_date` | Registratiedatum |
---
## Tips voor Gebruik
**Best practices:**
- Documenteer de hoofddiagnose zodra deze vastgesteld is
- Gebruik de onderbouwing voor klinische redenering
- Update de status wanneer het beloop verandert
- Houd nevendiagnoses actueel voor volledig beeld
**Let op:**
- Minimaal één intake vereist voor het toevoegen van diagnoses
- Alle wijzigingen worden direct opgeslagen
- Gebruik correcte ICD-10 codes voor declaratie
---
## Gerelateerde Documentatie
**Workflow:**
- [Intake Systeem](/documentatie/intake-system) - Intakes waaraan diagnoses worden gekoppeld
- [Screening Systeem](/documentatie/screening-system) - Voorafgaand aan intake en diagnose
**Basis:**
- [FHIR Datamodel](/documentatie/fhir-datamodel) - Onderliggende datastructuur
- [Client Management](/documentatie/client-management) - Cliëntbeheer

File diff suppressed because it is too large Load Diff

View File

@@ -1,476 +0,0 @@
---
title: "FHIR Datamodel"
category: "fhir-datamodel"
group: "architecture"
version: "2.0.0"
releaseDate: "2024-11-21"
status: "completed"
description: "FHIR-compliant datamodel voor GGZ-gegevens volgens MedMIJ standaarden - patiënten, behandelaren, contactmomenten en behandelplannen"
---
## Overview
Het Mini-EPD gebruikt een **FHIR-compliant datamodel** voor alle GGZ-gegevens. FHIR (Fast Healthcare Interoperability Resources) is de internationale standaard voor uitwisseling van zorggegevens en maakt toekomstige integratie mogelijk met:
- **MedMIJ** - Nederlandse patiëntportalen
- **Koppeltaal** - eHealth apps en interventies
- **Landelijk Schakelpunt (LSP)** - Medicatie-uitwisseling
- **Andere zorginstellingen** - Via standaard FHIR API's
**Pragmatische aanpak:**
We implementeren **6 kern FHIR resources** die essentieel zijn voor een werkend GGZ-dossier. Dit zorgt voor data-uitwisselbaarheid zonder onnodige complexiteit.
---
## Standaard Compliance
### MedMIJ Basisgegevens GGZ 2.0
Ons datamodel volgt de **MedMIJ Basisgegevens GGZ 2.0** specificatie. Dit betekent dat alle velden en structuren compatibel zijn met het Nederlandse afsprakenstelsel voor patiëntportalen.
**Voordelen:**
- ✅ Cliënten kunnen later hun eigen dossier inzien via een PGO-app
- ✅ Gegevens kunnen veilig gedeeld worden met andere zorgaanbieders
- ✅ Automatische uitwisseling met huisartsen en andere verwijzers
- ✅ Geen vendor lock-in - data is altijd exporteerbaar
### FHIR R4 Resources
Alle tabellen zijn gebaseerd op **FHIR R4** resource definities:
- Veldnamen volgen FHIR naming conventions
- Data types komen overeen met FHIR specificaties
- Relaties tussen resources volgen FHIR reference structuur
- API endpoints kunnen direct FHIR JSON genereren
---
## Geïmplementeerde Resources
### 1. **Practitioners** - Behandelaren
**Doel:** Registratie van alle zorgprofessionals die in het systeem werken.
**Belangrijkste gegevens:**
- BIG-nummer (indien van toepassing)
- AGB-code voor facturatie
- Naam en kwalificaties
- Contactgegevens
- Actieve status
**FHIR Mapping:**
Volgt de `Practitioner` resource uit FHIR R4. Ondersteunt Nederlandse BIG en AGB identificatie systemen.
**Praktisch gebruik:**
- Toewijzen van behandelverantwoordelijkheid
- Autorisatie en toegangscontrole
- Facturatie en declaratie
- Handtekeningen op documenten
---
### 2. **Organizations** - Instellingen
**Doel:** Registratie van de GGZ-instelling en externe samenwerkingspartners.
**Belangrijkste gegevens:**
- AGB-code instelling
- KVK-nummer
- Naam en nevenvestigingen
- Adres en contactgegevens
**FHIR Mapping:**
Volgt de `Organization` resource uit FHIR R4.
**Praktisch gebruik:**
- Juridische verantwoordelijkheid
- Facturatie en contracten
- Verwijzingen naar andere instellingen
- Netwerkzorg registratie
---
### 3. **Patients** - Patiënten/Cliënten
**Doel:** Centrale registratie van alle patiënten die behandeling ontvangen.
**Belangrijkste gegevens:**
- BSN (versleuteld opgeslagen)
- Naam, geboortedatum, geslacht
- Adres en contactgegevens
- Verzekeringsgegevens
- Huisarts (naam + AGB)
- Noodcontactpersoon
**FHIR Mapping:**
Volgt de `Patient` resource uit FHIR R4. Komt overeen met de Nederlandse **ZIB Patient** (ZorgInformatieBouwsteen).
**Privacy:**
- BSN wordt versleuteld opgeslagen
- Toegang via Row Level Security (RLS)
- AVG-compliant logging van alle toegang
**Praktisch gebruik:**
- Basis voor alle andere dossiergegevens
- Identificatie bij contact
- Facturatie naar verzekering
- Communicatie met cliënt
---
### 4. **Encounters** - Contactmomenten
**Doel:** Registratie van elk contact tussen cliënt en behandelaar.
**Belangrijkste gegevens:**
- Type contact (intake, diagnostiek, behandeling, crisis)
- Status (gepland, lopend, afgerond)
- Datum en tijd
- Behandelaar en cliënt
- Locatie (polikliniek, online, kliniek)
- Aanmeldingsreden
**FHIR Mapping:**
Volgt de `Encounter` resource uit FHIR R4. Komt overeen met de Nederlandse **ZIB Contact**.
**Praktisch gebruik:**
- Timeline van alle contacten per cliënt
- Facturatie per contactmoment
- Koppeling van diagnoses aan intake
- Planning en agenda-beheer
**Waarom belangrijk:**
Alle andere gegevens (diagnoses, observaties, behandelplannen) worden gekoppeld aan een specifiek contactmoment. Dit maakt het later mogelijk om te zien: *"Deze diagnose is gesteld tijdens de intake van 15 maart 2024"*.
---
### 5. **Conditions** - Diagnoses
**Doel:** Registratie van diagnoses volgens DSM-5 of ICD-10.
**Belangrijkste gegevens:**
- DSM-5/ICD-10 code (bijv. "F32.2")
- Omschrijving (bijv. "Depressieve episode, ernstig")
- Klinische status (actief, in remissie, opgelost)
- Ernst (mild, matig, ernstig)
- Verificatie status (voorlopig, bevestigd)
- Vastgesteld door welke behandelaar
- Bij welk contactmoment
**FHIR Mapping:**
Volgt de `Condition` resource uit FHIR R4. Komt overeen met de Nederlandse **ZIB Problem**.
**Praktisch gebruik:**
- Problemlijst per cliënt
- Basis voor behandelplan
- DBC registratie en facturatie
- Rapportage en statistiek
**Voorbeeld flow:**
1. Intake: voorlopige diagnose "F32.2 - Depressieve episode, ernstig"
2. Na diagnostiek: bevestiging van diagnose
3. Na behandeling: status wijzigt naar "in remissie"
4. Bij herstel: status wordt "opgelost"
---
### 6. **Observations** - Metingen en Observaties
**Doel:** Registratie van alle metingen, scores en observaties tijdens behandeling.
**Belangrijkste gegevens:**
- Type observatie (ROM-score, risico-inschatting, vitaliteit)
- Uitkomst/waarde
- Interpretatie (normaal, afwijkend)
- Datum en tijd
- Uitgevoerd door welke behandelaar
- Gekoppeld aan welk contactmoment
**FHIR Mapping:**
Volgt de `Observation` resource uit FHIR R4. Komt overeen met de Nederlandse **ZIB LaboratoryTestResult** en **ZIB Alert**.
**Categorieën:**
- **ROM-metingen:** PHQ-9, GAD-7, OQ-45 (routine outcome monitoring)
- **Risico-inschattingen:** Suïcidaliteit, agressie, verwaarlozing
- **Middelengebruik:** Alcohol, drugs, medicatie-compliance
- **Vitale functies:** Bloeddruk, hartslag (indien relevant)
**Praktisch gebruik:**
- Voortgang monitoren met ROM-scores
- Veiligheidsbewaking (risico's)
- Wetenschappelijk onderzoek
- Kwaliteitsindicatoren
**Voorbeeld:**
- PHQ-9 vragenlijst ingevuld: score 18 → interpretatie: "Matig-ernstige depressie"
- Risico-inschatting: "Suïcidale gedachten, geen plannen" → interpretatie: "Matig risico"
---
### 7. **CarePlans** - Behandelplannen
**Doel:** Overzicht van de geplande behandeling met doelen en interventies.
**Belangrijkste gegevens:**
- Titel en beschrijving
- Status (concept, actief, afgerond, gestopt)
- Looptijd (start- en einddatum)
- Behandeldoelen (SMART geformuleerd)
- Behandelactiviteiten (interventies, frequentie)
- Welke diagnoses worden behandeld
- Regiebehandelaar
**FHIR Mapping:**
Volgt de `CarePlan` resource uit FHIR R4. Komt overeen met de Nederlandse **ZIB TreatmentDirective**.
**Pragmatische keuze:**
Doelen (Goals) en Activiteiten zijn embedded in de CarePlan als JSONB fields. Dit is eenvoudiger dan aparte tabellen en voldoet nog steeds aan FHIR structuur.
**Praktisch gebruik:**
- Multidisciplinair behandelplan opstellen
- Voortgang monitoren
- Communicatie met cliënt
- Evaluatie en bijstelling
**Koppeltaal-integratie:**
Via de CarePlan kunnen later eHealth apps gekoppeld worden. Bijvoorbeeld: *"Opdracht: 3x per week mindfulness oefening via app MindDistrict"* → automatisch gesynchroniseerd tussen EPD en app.
**Voorbeeld structuur:**
```
Behandelplan: "Behandeling depressie"
├─ Diagnose: F32.2 (Depressieve episode, ernstig)
├─ Doel 1: "PHQ-9 score < 10 binnen 12 weken"
├─ Doel 2: "Herstel dagelijks functioneren (werk/sociaal)"
├─ Activiteit 1: Individuele CGT - 1x/week, 12 sessies
├─ Activiteit 2: ROM-meting PHQ-9 - elke 4 weken
└─ Activiteit 3: Medicatie - Sertraline 50mg dagelijks
```
---
## Relaties tussen Resources
```
Patient (Patiënt)
└─── Encounters (Contactmomenten)
├─── Conditions (Diagnoses)
│ └─── ondersteund door Observations (ROM-scores)
├─── Observations (Metingen/Risico's)
└─── CarePlans (Behandelplannen)
├─── Doelen (Goals)
└─── Activiteiten (Interventies)
Uitgevoerd door: Practitioner (Behandelaar)
Binnen: Organization (Instelling)
```
**Werkwijze:**
1. **Aanmelding** → Encounter (intake) wordt aangemaakt
2. **Diagnostiek** → Conditions (diagnoses) gekoppeld aan Encounter
3. **ROM-meting** → Observations gekoppeld aan Encounter
4. **Behandelplan** → CarePlan gekoppeld aan Patient + Conditions
5. **Behandeling** → Nieuwe Encounters voor sessies
6. **Evaluatie** → Nieuwe Observations (ROM) om voortgang te meten
---
## Data-uitwisselbaarheid
### FHIR API Endpoints
Alle resources zijn beschikbaar via RESTful FHIR API endpoints:
```
GET /api/fhir/Patient → Lijst van patiënten
GET /api/fhir/Patient/{id} → Specifieke patiënt
POST /api/fhir/Patient → Nieuwe patiënt aanmaken
PUT /api/fhir/Patient/{id} → Patiënt bijwerken
GET /api/fhir/Practitioner → Lijst van behandelaren
GET /api/fhir/CarePlan/{id} → Behandelplan ophalen
POST /api/fhir/CarePlan → Behandelplan importeren
```
**Response format:**
- Content-Type: `application/fhir+json`
- Volledig FHIR R4 compliant JSON
- Ondersteunt FHIR search parameters
- Error responses via FHIR OperationOutcome
### Export/Import Scenario
**Voorbeeld - Behandelplan delen:**
1. Behandelaar maakt behandelplan in Mini-EPD
2. Export via API: `GET /api/fhir/CarePlan/abc-123`
3. FHIR JSON response bevat volledig behandelplan
4. Andere instelling importeert: `POST /api/fhir/CarePlan`
5. ✅ Behandelplan is succesvol gedeeld tussen systemen
**MedMIJ scenario (toekomst):**
1. Cliënt opent PGO-app (Persoonlijke Gezondheidsomgeving)
2. App vraagt toestemming voor toegang tot dossier
3. Mini-EPD API deelt FHIR resources met app
4. Cliënt ziet eigen behandelplan, diagnoses en ROM-scores
5. ✅ Data-uitwisselbaarheid volgens MedMIJ standaard
---
## Privacy & Beveiliging
### Toegangscontrole (Row Level Security)
Elke tabel heeft RLS policies:
- **Behandelaren** zien alleen hun eigen patiënten
- **Patiënten** kunnen later hun eigen data inzien (via patiëntenportaal)
- **Administrators** hebben beperkte toegang (geen medische data)
### Encryptie
- **BSN** wordt versleuteld opgeslagen (pgcrypto)
- **Communicatie** via HTTPS/TLS
- **Database** backups zijn versleuteld
### AVG-compliance
- **Recht op inzage** - Cliënt kan eigen data opvragen via API
- **Recht op correctie** - Data kan bijgewerkt worden
- **Recht op vergetelheid** - Data kan verwijderd worden (na wettelijke bewaartermijn)
- **Logging** - Alle toegang wordt gelogd in audit trail
- **Toestemming** - Consent management via Consents resource (toekomstig)
---
## Roadmap
### ✅ Fase 1: Foundation (Voltooid)
- Database schema met 6 FHIR resources
- TypeScript type definitions
- Data migratie van legacy tabellen
- Seed data voor demo
### ✅ Fase 2: API & UI (Voltooid)
- FHIR transform library (DB ↔ FHIR JSON)
- Patient API endpoints (GET/POST/PUT)
- Practitioner API endpoints (GET/POST)
- Patient UI (lijst, detail, formulieren)
### ⏳ Fase 3: Encounters & Conditions (In planning)
- Encounter API endpoints
- Condition API endpoints
- Observation API endpoints
- UI voor contactmomenten en diagnoses
### 🎯 Fase 4: CarePlans (Hoofddoel)
- CarePlan API endpoints
- Wizard UI voor behandelplan opstellen
- Doelen en activiteiten beheer
- Voortgang monitoring
### 🔮 Fase 5: Integraties (Toekomst)
- MedMIJ aansluiting (patiëntportaal)
- Koppeltaal aansluiting (eHealth apps)
- Landelijk Schakelpunt (medicatie)
- Swagger/OpenAPI documentatie
---
## Technische Details
### Database: PostgreSQL (Supabase)
- **Type-safe ENUMs** voor statussen (draft, active, completed, etc.)
- **Automatische timestamps** (created_at, updated_at)
- **Foreign keys** voor relaties tussen resources
- **Indexes** op veel-gebruikte velden voor performance
- **JSONB fields** voor flexibele embedded data (goals, activities)
### Veldnamen volgen FHIR
Voorbeelden:
- `name_family` → `Patient.name.family`
- `identifier_bsn` → `Patient.identifier[system=bsn].value`
- `status` → `CarePlan.status`
Dit maakt transformatie naar FHIR JSON eenvoudig en voorspelbaar.
### Transform Pattern
```typescript
// Database → FHIR
const fhirPatient = dbPatientToFHIR(dbRow);
// FHIR → Database
const dbInsert = fhirPatientToDB(fhirResource);
```
Alle transforms zijn in `lib/fhir/transforms/` met 100% type safety.
---
## Voor Functioneel Beheerders
### Wat betekent dit voor dagelijks gebruik?
**Voor behandelaren:**
- Intuïtieve UI, geen FHIR kennis nodig
- Alle data logisch gestructureerd
- Diagnoses gekoppeld aan intake-moment
- Behandelplan volgt automatisch uit diagnose
**Voor beheerders:**
- Export naar andere systemen is mogelijk
- Backups bevatten FHIR-compliant data
- Audits en rapportages zijn eenvoudig
- Geen vendor lock-in
**Voor ICT:**
- Database schema volgt internationale standaard
- API endpoints zijn FHIR-compliant
- Integraties zijn goed gedocumenteerd
- Migratie naar andere systemen is eenvoudig
### Veelgestelde Vragen
**Q: Moet ik FHIR kennen om het systeem te gebruiken?**
A: Nee, de UI abstracteert alle FHIR complexiteit. Je werkt gewoon met patiënten, contacten en behandelplannen.
**Q: Kunnen we later gemakkelijk overstappen naar een ander EPD?**
A: Ja, alle data is in FHIR format te exporteren. Geen vendor lock-in.
**Q: Is dit compatible met MedMIJ?**
A: Ja, het datamodel volgt MedMIJ Basisgegevens GGZ 2.0 specificaties.
**Q: Hoe zit het met privacy?**
A: BSN versleuteld, RLS policies, audit logging, AVG-compliant.
---
## Referenties
**FHIR Specificaties:**
- FHIR R4: https://hl7.org/fhir/R4/
- FHIR Patient: https://hl7.org/fhir/R4/patient.html
- FHIR CarePlan: https://hl7.org/fhir/R4/careplan.html
**Nederlandse Standaarden:**
- MedMIJ GGZ: https://informatiestandaarden.nictiz.nl/wiki/MedMij:V2020.01/OntwerpGGZ
- ZIBs: https://zibs.nl/
**Project Documentatie:**
- Technisch schema: `docs/archive/schemas/20241121_fhir_ggz_schema.sql` (archived)
- Bouwplan: `docs/bouwplan-pragmatisch-fhir.md`
- Datamodel details: `docs/datamodel-documentatie.md`
---
**Laatst bijgewerkt:** 21 november 2024
**Versie:** 2.0.0 (Pragmatisch FHIR)
**Status:** Actief - Epic 1 & 2 Voltooid

View File

@@ -1,203 +0,0 @@
---
title: "Intake Systeem"
category: "intake-system"
group: "features"
version: "1.0.0"
releaseDate: "2025-11-23"
status: "completed"
description: "Intake gesprekken registreren, notities maken en gestructureerde intake-workflow voor GGZ-behandelaars"
---
## Overzicht
Het intake systeem biedt een complete workflow voor het uitvoeren en documenteren van intakes in de GGZ. Na een succesvolle screening wordt een intake aangemaakt waarbij alle aspecten van de hulpvraag systematisch worden gedocumenteerd.
**Kernfunctionaliteit:**
- Nieuwe intakes aanmaken per afdeling
- Overzicht van alle intakes van een cliënt
- Gestructureerde intake met meerdere onderdelen
- Contactmomenten registreren
- Anamnese vastleggen
- Risicotaxatie uitvoeren
- Diagnose stellen
- Behandeladvies formuleren
---
## Intake Aanmaken
### Nieuwe Intake Starten
1. Open het cliëntdossier
2. Navigeer naar het tabblad "Intakes"
3. Klik op **"Nieuwe Intake"**
4. Vul het formulier in:
- **Titel**: Korte omschrijving (bijv. "Eerste intake ambulant")
- **Afdeling**: Selecteer de behandelafdeling (Volwassenen, Jeugd, Forensisch, etc.)
- **Status**: Lopend / Afgerond / Geannuleerd
- **Startdatum**: Datum van eerste contact
- **Notities**: Extra context of bijzonderheden
5. Klik op **"Intake Aanmaken"**
De intake wordt aangemaakt en u wordt doorgeleid naar de intake detail pagina.
---
## Intake Overzicht
Het intake overzicht toont alle intakes van een cliënt in chronologische volgorde.
**Weergegeven informatie:**
- Titel van de intake
- Afdeling waar de intake plaatsvindt
- Status (lopend, afgerond, geannuleerd)
- Startdatum
**Acties:**
- Klik op een intake om de details te bekijken
- Bekijk en bewerk alle onderdelen via de tabbladen
---
## Intake Onderdelen
Elke intake bestaat uit meerdere onderdelen die via tabbladen toegankelijk zijn:
### 1. Algemeen
Toont de basisinformatie van de intake:
- Titel, afdeling, status, startdatum
- Algemene notities bij de intake
### 2. Contactmomenten
Registreer alle contactmomenten tijdens de intake:
- **Datum en tijd** van het contact
- **Type contact**: Gesprek, Telefonisch, E-mail, Overig
- **Duur** in minuten
- **Notities** over het contact
**Handelingen:**
- Voeg een nieuw contactmoment toe
- Bekijk chronologisch overzicht
- Verwijder contactmomenten indien nodig
### 3. Kindcheck
Specifiek voor jeugd-intakes: vastleggen van kindcheck informatie conform GGZ-richtlijnen.
### 4. Risicotaxatie
Beoordeel risico's zoals:
- Suïcidaliteit
- Agressie
- Zelfverwaarlozing
- Middelengebruik
- Andere risicofactoren
**Per risico vastleggen:**
- **Datum** van de taxatie
- **Risicotype**
- **Ernst**: Laag, Matig, Hoog, Acuut
- **Bevindingen**: Beschrijving van het risico
- **Maatregelen**: Genomen acties
### 5. Anamnese
Systematisch verzamelen van voorgeschiedenis:
**Types anamnese:**
- Psychiatrische anamnese
- Sociale anamnese
- Medische anamnese
- Familieanamnese
- Ontwikkelingsanamnese
- Overig
**Per anamnese vastleggen:**
- **Datum** van gesprek
- **Type** anamnese
- **Inhoud**: Bevindingen en observaties
- **Notities**: Extra context
### 6. Onderzoeken
Registreer alle uitgevoerde onderzoeken:
- Psychologisch onderzoek
- Neurologisch onderzoek
- Laboratoriumonderzoek
- Beeldvorming
- Overig
**Per onderzoek:**
- **Datum**
- **Type onderzoek**
- **Uitslag**: Bevindingen
- **Interpretatie**: Conclusies
### 7. ROM (Routine Outcome Monitoring)
Vastleggen van ROM-metingen voor behandelevaluatie.
### 8. Diagnose
Stel diagnoses volgens DSM-5 classificatie:
- **Datum** van diagnose
- **Type**: Hoofddiagnose / Nevendiagnose
- **DSM-code** en omschrijving
- **Toelichting**: Onderbouwing van de diagnose
### 9. Behandeladvies
Formuleer het behandeladvies op basis van de intake:
- **Advies**: Welke behandeling wordt geadviseerd
- **Doelen**: Behandeldoelen
- **Afspraken**: Vervolgstappen
---
## Workflow
### Typische Intake Workflow
1. **Intake starten** na screening besluit
2. **Contactmomenten** plannen en vastleggen
3. **Kindcheck** uitvoeren (indien van toepassing)
4. **Risicotaxatie** uitvoeren
5. **Anamnese** afnemen (meerdere types)
6. **Onderzoeken** aanvragen en resultaten vastleggen
7. **ROM-metingen** uitvoeren
8. **Diagnose** stellen
9. **Behandeladvies** formuleren
10. Intake afronden (status wijzigen naar "Afgerond")
---
## Tips voor Gebruik
**Goede praktijk:**
- Vul onderdelen meteen na het gesprek in voor beste nauwkeurigheid
- Gebruik de notitievelden voor context die niet in standaardvelden past
- Documenteer alle contactmomenten voor volledig overzicht
- Voer risicotaxatie regelmatig uit bij wijzigende situaties
- Gebruik duidelijke datums om chronologie te behouden
**Let op:**
- Alle gegevens worden automatisch opgeslagen
- Verwijderde items kunnen niet worden hersteld
- Zorg voor complete documentatie voordat u de intake afrondt
---
## Gerelateerde Documentatie
**Workflow:**
- [Screening Systeem](/documentatie/screening-system) - Voorafgaand aan intake
**Interface:**
- [Interface Design System](/documentatie/interface-design) - UI/UX specificaties
**Basis:**
- [Authentication System](/documentatie/authentication) - Toegangsbeheer
- [Client Management](/documentatie/client-management) - Cliëntbeheer

View File

@@ -1,643 +0,0 @@
---
title: "Interface Design System"
category: "interface-design"
group: "foundation"
version: "1.0.0"
releaseDate: "2024-11-19"
status: "completed"
description: "Complete UI/UX design specificatie voor het Mini-ECD prototype met two-level navigation systeem"
---
## Overview
Het Mini-ECD interface design definieert een complete gebruikerservaring voor behandelaars met een two-level context systeem. Desktop-first, geoptimaliseerd voor professioneel gebruik in een klinische omgeving.
**Design Principes:**
- Context-aware navigation (Behandelaar vs. Client Dossier)
- Desktop-only MVP (1280px - 1920px)
- Clean, professional healthcare UI
- Maximale efficiency voor behandelaars
---
## Two-Level Context System
### Level 1: Behandelaar Context
**Wanneer:** Geen cliënt geselecteerd
**Navigatie:**
- 📊 Dashboard - Caseload overzicht en taken
- 👥 Cliënten - Zoeken en selecteren
- 📅 Agenda - Alle afspraken
- 📈 Rapportage - BI dashboards
**Use Case:** Behandelaar krijgt overzicht van de hele caseload, plant afspraken, en ziet aandachtspunten.
### Level 2: Client Dossier Context
**Wanneer:** Cliënt geselecteerd (via klik op cliënt in lijst)
**Navigatie:**
- ← Cliënten - Terug naar behandelaar context
- 📋 Dashboard - Dossier overzicht
- 💬 Intake - Gesprekken en notities
- 🧠 Diagnose - DSM classificatie
- 📝 Behandelplan - SMART doelen en interventies
- 📊 Rapportage - Cliënt-specifieke voortgang
**Use Case:** Behandelaar werkt intensief met een specifieke cliënt, bekijkt historie, maakt diagnose, stelt behandelplan op.
---
## Layout Components
### Header Bar (60px fixed)
**Structuur:**
```
┌──────────────────────────────────────────────┐
│ Mini-ECD [Bas Jansen ▼] [🔍 Zoek] │
│ ID: CL0002 │
└──────────────────────────────────────────────┘
```
**Elementen:**
- **Links:** Logo/branding
- **Center:** Context-aware client selector
- Level 1: Leeg of behandelaar naam
- Level 2: Client dropdown met ID en geboortedatum
- **Rechts:** Global search "Zoek cliënt..."
### Sidebar (240px fixed)
**Context-Aware Menu:**
**Level 1 (Behandelaar):**
```
┌──────────────┐
│ Dashboard │ ← Active
│ Cliënten │
│ Agenda │
│ Rapportage │
└──────────────┘
```
**Level 2 (Client Dossier):**
```
┌──────────────┐
│ ← Cliënten │ ← Back button
│ ──────── │
│ Dashboard │ ← Active
│ Intake │
│ Diagnose │
│ Behandelplan │
│ Rapportage │
└──────────────┘
```
**Styling:**
- Light gray background
- Active state: highlighted bg + left border accent
- Icons: 20px with 12px gap
- 4px spacing between items
### Main Content Area
**Layout:**
- White background
- 32px padding all sides
- Full scrollable height
- Responsive grid system voor cards
---
## Screen Specifications
### Level 1 Screens
#### 1A: Behandelaar Dashboard
**Content Cards:**
- Caseload overzicht (aantal actieve cliënten)
- Aandachtspunten en urgente taken
- Behandelplannen in concept status
- Recente activiteit timeline
- Aankomende afspraken (vandaag/deze week)
**Layout:**
```
┌────────────────────────────────────┐
│ Dashboard │
│ Welkom terug, [Behandelaar] │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Caseload│Taken │Afspraken│ │
│ │ 24 │ 8 │ 3 │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ Aandachtspunten │
│ • Behandelplan Bas Jansen (Concept)│
│ • Follow-up Anna de Vries │
└────────────────────────────────────┘
```
#### 1B: Cliënten Lijst
**Features:**
- Zoekbalk met filters (Afdeling, Team, Status)
- Recent bekeken cliënten (quick access)
- Tabel met: Naam | ID | Geboortedatum | Status | Laatste Contact
- "Nieuwe Cliënt" button (top right)
**Client Row:**
```
Bas Jansen CL0002 20-11-1992 [Actief] 12-10-2023
```
**Hover State:** Light background + pointer cursor
**Click Action:** → Switches to Level 2 (Client Dossier)
**Empty State:**
```
👥
Nog geen cliënten
Voeg je eerste cliënt toe om te beginnen
[+ Nieuwe Cliënt]
```
#### 1C: Behandelaar Agenda
**Features (to be detailed):**
- Week/maand kalender view
- Alle afspraken van alle cliënten
- Type, tijd, cliënt, locatie
- Nieuwe afspraak maken
#### 1D: Behandelaar Rapportage
**Features (to be detailed):**
- KPI dashboards
- Statistieken (behandelduur, outcomes)
- Trends over tijd
- Export functionaliteit
---
### Level 2 Screens
#### 2A: Client Dashboard (Dossier Overzicht)
**Header Shows:**
```
Bas Jansen ▼
ID: CL0002 | Geb: 20-11-1992
```
**Card Grid (3 columns):**
**Column 1 - Cliëntinformatie:**
```
┌────────────────────┐
│ Cliëntinformatie │
│ │
│ ID: CL0002 │
│ Naam: Bas Jansen │
│ Geboren: 20-11-1992│
│ Leeftijd: 31 jaar │
│ Verzekering: [...] │
│ BSN: [...] │
│ │
│ Bewerken → │
└────────────────────┘
```
**Column 2 - Laatste Intake:**
```
┌────────────────────┐
│ Laatste Intake │
│ 12-10-2023, 14:30 │
│ │
│ Bas komt op gesprek│
│ vanwege spannings- │
│ klachten en paniek-│
│ aanvallen... │
│ │
│ Bekijk intake → │
└────────────────────┘
```
**Column 3 - Diagnose:**
```
┌────────────────────┐
│ Diagnose [Hoog] │
│ │
│ Angststoornissen │
│ │
│ Panietstoornis met │
│ agorafobie. De │
│ frequentie van... │
│ │
│ Bekijk diagnose → │
└────────────────────┘
```
**Full Width - Behandelplan:**
```
┌────────────────────────────────────┐
│ Behandelplan │
│ │
│ [Concept] Versie 1 - 12-10-2023 │
│ │
│ SMART Doelen: │
│ • Verminderen paniekeaanvallen │
│ • Uitbreiden sociale activiteiten │
│ │
│ Bekijk behandelplan → │
└────────────────────────────────────┘
```
**Empty State:**
```
┌────────────────────────────────────┐
│ Behandelplan │
│ │
│ Geen behandelplan gevonden │
│ │
│ [+ Maak behandelplan] │
└────────────────────────────────────┘
```
**Full Width - Afspraken:**
```
┌────────────────────────────────────┐
│ Afspraken │
│ │
│ Afgelopen Afspraak │
│ 📅 14-11-2025, 16:26 - Intake │
│ │
│ ───────────────────────────────── │
│ │
│ Komende Afspraken │
│ │ 22-11-2025, 16:26 - Psycho-edu │
│ │ 29-11-2025, 16:26 - Exposure │
│ │ 06-12-2025, 16:26 - Evaluatie │
└────────────────────────────────────┘
```
#### 2B: Intake Overzicht
**Top Bar:**
```
Intakes [+ Nieuwe Intake]
```
**Intake Lijst:**
Header: `Datum | Type | Status | Samenvatting | Acties`
**Intake Items:**
```
┌────────────────────────────────────────────┐
│ 12-10-2023, 14:30 │
│ [Intake gesprek] [Afgerond] │
│ Eerste intake. Cliënt presenteert zich... │
│ [Bekijk] │
├────────────────────────────────────────────┤
│ 05-10-2023, 10:15 │
│ [Telefonische intake] [Afgerond] │
│ Korte telefonische screening... │
│ [Bekijk] │
└────────────────────────────────────────────┘
```
**Empty State:**
```
📋
Nog geen intakes
Klik op 'Nieuwe Intake' om te beginnen
[+ Nieuwe Intake]
```
#### 2C: Diagnose & Probleemprofiel
**Top Bar:**
```
Diagnose & Probleemprofiel [✨ AI Analyse]
```
**DSM-light Categorieën Grid:**
```
┌─────────┬─────────┬─────────┐
│ Stemming│ Angst │ Gedrag │
│ Hoog │ Middel │ Laag │
├─────────┼─────────┼─────────┤
│ Middel │ Cognitief│Context │
│ │ │ │
└─────────┴─────────┴─────────┘
```
**Ernst Indicatie:**
```
┌────────────────────────────┐
│ Ernst: ●●●○○ (Hoog) │
│ Bronnen: Intake 12-10-2023 │
│ │
│ Primaire diagnose: │
│ Panietstoornis met │
│ agorafobie │
└────────────────────────────┘
```
**Features:**
- AI-gegenereerde classificatie
- Manual override mogelijkheid
- Geschiedenis van diagnoses
- Notities en observaties
#### 2D: Behandelplan
**Top Bar:**
```
Behandelplan [🤖 Genereer Plan] [Nieuwe Versie]
```
**Versie Header:**
```
Versie 1 - Actief | 12-10-2023
```
**Plan Structuur:**
**1. SMART Doelen:**
```
┌────────────────────────────────────┐
│ 1. SMART Doelen │
│ │
│ • Verminderen paniekeaanvallen van │
│ 3x per week naar 1x per maand │
│ [In Progress] │
│ │
│ • Uitbreiden sociale activiteiten: │
│ minimaal 2x per week uitgaan │
│ [Not Started] │
└────────────────────────────────────┘
```
**2. Interventies:**
```
┌────────────────────────────────────┐
│ 2. Interventies │
│ │
│ • CGT - Cognitieve herstructurering│
│ Rationale: [...] │
│ │
│ • Exposure therapie (gradueel) │
│ Rationale: [...] │
└────────────────────────────────────┘
```
**3. Frequentie & Planning:**
```
┌────────────────────────────────────┐
│ 3. Frequentie & Planning │
│ │
│ • Sessies: 1x per week │
│ • Duur: 60 minuten │
│ • Behandelduur: 12-16 weken │
└────────────────────────────────────┘
```
**4. Meetmomenten:**
```
┌────────────────────────────────────┐
│ 4. Meetmomenten │
│ │
│ • Week 0: Baseline (ROM) │
│ • Week 6: Tussentijdse evaluatie │
│ • Week 12: Einde behandeling │
└────────────────────────────────────┘
```
**Versioning:**
- Status: Concept | Actief | Afgerond
- Meerdere versies mogelijk (v1, v2, etc.)
- Timestamp + behandelaar info
#### 2E: Rapportage & Voortgang
**Metrics Cards:**
```
┌──────────────────────────────┐
│ Behandelduur: 8 weken │
│ Sessies: 6 van 12 │
│ Voortgang: ████████░░░░ 67% │
└──────────────────────────────┘
```
**Features (to be detailed):**
- Progress tracking per doel
- Session attendance
- ROM instrumenten
- Grafieken en visualisaties
- Export functionaliteit (PDF, CSV)
---
## Intake Detail View (Slide-in Panel)
**Trigger:** Klik "Bekijk" op intake item
**Panel (400px from right):**
```
┌────────────────────────┐
│ Intake - 12-10-2023 ✕ │
├────────────────────────┤
│ │
│ Algemene informatie │
│ Datum: 12-10-2023 │
│ Tijd: 14:30 │
│ Type: Intake gesprek │
│ Duur: 60 minuten │
│ │
│ ────────────────── │
│ │
│ Gespreksnotities │
│ │
│ Bas komt op gesprek │
│ vanwege spannings- │
│ klachten en paniek... │
│ [full scrollable text] │
│ │
│ ────────────────── │
│ │
│ ✨ AI Samenvatting │
│ • Paniekeaanvallen │
│ • Vermijdingsgedrag │
│ • Werkstress trigger │
│ │
├────────────────────────┤
│ Verwijderen Bewerken │
└────────────────────────┘
```
**Features:**
- Sticky header + footer
- Scrollable content
- AI samenvatting (light blue bg)
- Edit + Delete actions
---
## Design Tokens
### Spacing Scale
```
xs: 4px
sm: 8px
md: 16px
lg: 24px
xl: 32px
```
### Typography Scale
```
Page title: 28px, semi-bold
Card title: 18px, semi-bold
Section title: 14px, semi-bold
Body text: 15px, normal
Small text: 13px, normal
Tiny text: 12px, normal
```
### Border Radius
```
Badges: 12px (full rounded)
Buttons: 6px
Cards: 8px
```
### Shadows
```
Card: 0 1px 3px rgba(0,0,0,0.1)
Card hover: 0 4px 6px rgba(0,0,0,0.1)
Modal: 0 20px 25px rgba(0,0,0,0.15)
```
### Component Patterns
**Card:**
- White background
- 1px border (light gray)
- 8px border radius
- 16px padding
- Subtle shadow
**Badge/Pill:**
- 13px font size
- 4px vertical, 12px horizontal padding
- Full rounded (12px)
- Contextual colors:
- Green: Success/Actief
- Yellow: Warning/Wachtlijst
- Red: High severity
- Blue: Info/Type indicator
**Button:**
- Primary: Solid bg, white text
- Secondary: Border outline
- Destructive: Red theme
- 8px vertical, 16px horizontal padding
- 6px border radius
---
## Hoe werkt de navigatie?
**Typische workflow:**
1. **Start** - Je begint op het behandelaar dashboard met overzicht van je caseload
2. **Cliënt selecteren** - Klik op "Cliënten" in het menu om je cliëntenlijst te zien
3. **Dossier openen** - Klik op een cliënt (bijv. "Bas Jansen") om het dossier te openen
4. **Context wijzigt** - Het menu toont nu cliënt-specifieke opties (Intake, Diagnose, Behandelplan)
5. **Door dossier navigeren** - Gebruik het menu om tussen secties te wisselen
6. **Terug naar overzicht** - Klik op "← Cliënten" om terug te gaan naar je caseload
7. **Snel wisselen** - Gebruik de dropdown in de header om snel naar een andere cliënt te gaan
8. **Zoeken** - Gebruik de zoekbalk rechtsboven om direct naar een cliënt te springen
---
## Toegankelijkheid
Het systeem is ontworpen om toegankelijk te zijn voor alle gebruikers:
**Toetsenbord navigatie:**
- Alle knoppen en links zijn bereikbaar met Tab
- Logische volgorde door de pagina
- Duidelijk zichtbare focus indicatie
**Kleurgebruik:**
- Voldoende contrast voor leesbaarheid
- Status badges zijn ook zonder kleur te onderscheiden
- Hover effecten zijn duidelijk zichtbaar
---
## Wat zit er NIET in de MVP?
Om snel te kunnen leveren focussen we eerst op de kern functionaliteit. Deze features komen later:
**Niet in eerste versie:**
- ❌ Mobile/tablet versie (alleen desktop)
- ❌ Dark mode
- ❌ Rechten en rollen systeem
- ❌ Geavanceerde filters en bulk acties
- ❌ Export naar PDF/CSV
- ❌ Agenda integratie (Google Calendar, etc.)
- ❌ Push notificaties
- ❌ Aangepaste velden per instelling
- ❌ Document templates
**Waarom deze keuzes?**
We bouwen eerst een solide basis voor de kernworkflow van behandelaars. Na de MVP kunnen we op basis van jullie feedback de meest waardevolle features toevoegen.
---
## Wat komt er nu?
**In ontwikkeling:**
- ✅ Design systeem gedocumenteerd
- 🔄 Component library bouwen
- 🔄 Cliënten lijst met zoeken
- ⏳ Dashboard voor behandelaars
- ⏳ Client dossier schermen
**Toekomstige verbeteringen:**
- Mobile versie voor on-the-go toegang
- Dark mode voor avondwerk
- Geavanceerde filters en sortering
- Keyboard shortcuts voor power users
- Template systeem voor intakes en plannen
- Export functionaliteit
---
## Gerelateerde Documentatie
**Basis Systeem:**
- [Authentication System](/documentatie/authentication) - Inloggen en gebruikersbeheer
- Database Schema (binnenkort)
**Features:**
- Client Management (binnenkort)
- Intake Systeem (binnenkort)
- Behandelplanning (binnenkort)
**Meer Informatie:**
- Component Library (binnenkort)
- Design Tokens (binnenkort)

View File

@@ -1,397 +0,0 @@
---
title: "Documentation System"
category: "release-notes-system"
group: "infrastructure"
version: "0.2.0"
releaseDate: "2024-11-19"
status: "completed"
description: "MDX-based documentatie systeem voor transparante build-in-public feature docs"
---
## Overview
Een volledig MDX-based documentatie systeem waarmee we eenvoudig en transparant kunnen documenteren wat er gebouwd wordt. Het systeem maakt het mogelijk om feature documentatie te schrijven in Markdown, met support voor afbeeldingen, code snippets en rich formatting.
**Waarom dit belangrijk is:**
- 📝 Makkelijk te schrijven (Markdown/MDX)
- 🎨 Rich content support (images, code, links)
- 🗂️ Thematische organisatie (niet chronologisch)
- 📱 Mobile-first responsive design
- ⚡ Performance (Static Site Generation)
- 🔍 SEO-vriendelijk met metadata per release
---
## Features
### MDX Content System
Het hart van het systeem: schrijf release notes in MDX bestanden met frontmatter metadata.
**Voorbeeld template:**
```mdx
---
title: "Feature Name"
category: "feature-slug"
group: "foundation | features | infrastructure"
version: "0.1.0"
releaseDate: "2024-11-19"
status: "completed | in_progress | planned"
description: "Short description"
---
## Overview
Content here...
```
**Voordelen:**
- Markdown syntax = makkelijk schrijven
- Frontmatter voor metadata
- MDX = React components in content mogelijk
- Version control via Git
- Automatische parsing en rendering
### Thematische Sidebar Navigatie
Releases zijn georganiseerd op functionaliteit, niet op chronologische volgorde.
**Groepen:**
- **Foundation** - Basis setup (Auth, Database, Environment)
- **Core Features** - EPD functionaliteit (Dashboard, Clients, AI)
- **Cortex Intent driven EPD** - Een EPD dat de intentie van de zorgprofessional herkent en meedenkt.
- **Infrastructure** - Ondersteunend (Hosting, Design, Performance)
**Features:**
- Collapsible sections per group
- Status indicators (completed/in progress/planned)
- Active state highlighting
- Auto-generated from MDX files
### Overview Page
Centrale pagina met overzicht van alle releases, gegroepeerd op status.
**Sections:**
- ✅ Voltooid - Afgeronde features
- 🔄 In Progress - Features in ontwikkeling
- ⏳ Gepland - Toekomstige features
**Stats cards:**
- Aantal voltooide releases
- Aantal in-progress releases
- Aantal geplande releases
### Detail Pages
Individuele pagina per release met volledige content.
**Features:**
- Rich MDX rendering
- Custom styled components
- Code syntax highlighting
- Responsive images met captions
- SEO metadata per page
- Navigation (terug naar overview, naar timeline)
### Mobile Responsive
**Desktop:**
- Fixed sidebar (256px width)
- Full content area
- Collapsible groups
**Mobile:**
- Horizontal scroll tabs boven content
- Touch-friendly navigation
- Optimized voor kleine schermen
### Custom MDX Components
Styled components voor consistente formatting:
**Typography:**
- Headings (h1-h4) met spacing
- Paragraphs met optimale line-height
- Lists (ordered/unordered)
- Blockquotes met teal accent
**Media:**
- Images met captions en responsive sizing
- Code blocks met syntax highlighting
- Tables met styled headers
**Links:**
- Internal links (Next.js Link)
- External links met proper attributes
- Teal color scheme (consistent met brand)
---
## Technical Notes
### Architecture
**Stack:**
- Next.js 15 App Router
- next-mdx-remote voor MDX rendering
- gray-matter voor frontmatter parsing
- TypeScript voor type safety
**Rendering:**
- Server-side MDX parsing
- Static Site Generation (SSG)
- Pre-rendered HTML voor performance
- Client-side hydration
### File Structure
```
# Code
app/(marketing)/documentatie/
├── layout.tsx # Sidebar wrapper
├── page.tsx # Overview
├── [category]/
│ └── page.tsx # Detail renderer
└── components/
├── release-sidebar.tsx
└── mdx-components.tsx
# Content
content/nl/documentatie/
├── _index.json # Categories metadata
├── authentication.mdx
└── release-notes-system.mdx
# Utilities
lib/mdx/
└── releases.ts # MDX parsing functions
# Templates
docs/templates/
└── release-note-template.mdx
```
### Key Functions
```typescript
// Get all release notes
export async function getAllReleases(): Promise<ReleaseNote[]> {
const files = fs.readdirSync(RELEASES_DIR)
// Parse MDX files, extract frontmatter
// Sort by group and category
return releases
}
// Get single release by slug
export async function getRelease(slug: string): Promise<ReleaseNote | null> {
const filePath = path.join(RELEASES_DIR, `${slug}.mdx`)
const { data, content } = matter(fileContent)
return { slug, frontmatter: data, content }
}
```
### Static Site Generation
```typescript
// Generate static params for all releases
export async function generateStaticParams() {
const releases = await getAllReleases()
return releases.map((release) => ({
category: release.slug,
}))
}
```
**Voordelen:**
- Pre-rendered HTML = snelle load times
- No runtime MDX parsing = beter performance
- SEO-friendly = search engines kunnen content lezen
---
## Improvements
**Developer Experience:**
- Template bestand voor consistente structuur
- TypeScript types voor frontmatter
- Auto-completion in VS Code (frontmatter schema)
**User Experience:**
- Responsive design voor alle devices
- Snelle navigatie met sidebar
- Clear visual hierarchy
- Status badges voor transparency
**Performance:**
- Static generation = < 100ms load time
- Optimized images (Next.js Image)
- Minimal JavaScript bundle (+12kb voor MDX parser)
---
## Workflow
### Nieuwe Release Note Toevoegen
**Stap 1:** Kopieer template
```bash
cp docs/templates/release-note-template.mdx \
content/nl/documentatie/new-feature.mdx
```
**Stap 2:** Vul frontmatter in
```yaml
---
title: "New Feature Name"
category: "new-feature"
group: "features"
version: "0.3.0"
releaseDate: "2024-11-20"
status: "completed"
description: "Short description here"
---
```
**Stap 3:** Schrijf content in Markdown
**Stap 4:** (Optioneel) Voeg screenshots toe
```bash
mkdir -p public/documentatie/new-feature/
# Add images: screenshot-1.png, etc.
```
**Stap 5:** Build en test
```bash
npm run build
npm run dev
# Visit /documentatie/new-feature
```
**Stap 6:** Commit en push
```bash
git add content/nl/documentatie/new-feature.mdx
git commit -m "docs: add new-feature release note"
```
---
## Integration
### Timeline Integration
Timeline component heeft nu link naar release notes:
```tsx
<a href="/documentatie">
Bekijk gedetailleerde release notes →
</a>
```
### Navigation Integration
Header navigatie heeft "Releases" link toegevoegd:
```json
{
"links": [
{ "label": "Releases", "href": "/documentatie" },
{ "label": "Contact", "href": "/contact" },
{ "label": "Login", "href": "/login" }
]
}
```
---
## Metrics
**Development:**
- ⏱️ Build tijd: 2.5 uur
- 📝 Lines of code: ~2,750
- 🧪 Build status: ✅ Success
**Performance:**
- 🚀 Page load: < 100ms (static)
- 📦 Bundle size: +12kb (MDX parser)
- 💾 Build time: +2.2s (22 static pages)
**Files Added:**
- 14 new files
- 6 components
- 3 utility functions
- 2 documentation files
- 1 template file
**Cost:**
- 💰 Dependencies: €0 (open source)
- 💰 Hosting: €0 (Vercel static)
- 💰 Build time: €0 (within limits)
---
## Screenshots
### Overview Page
*Overview pagina met status filtering (voltooid/in progress/gepland)*
### Sidebar Navigation
*Thematische sidebar met collapsible groups (Foundation, Features, Infrastructure)*
### Detail Page
*Release detail met MDX content rendering en custom components*
### Mobile View
*Responsive horizontal scroll navigation op mobile*
---
## Related Links
**Documentation:**
- [Bouwplan v1.2](/docs/specs/releasepage/bouwplan-release-notes-v1.0.md)
- [Release Note Template](/docs/templates/release-note-template.mdx)
**Timeline:**
- [Build Timeline](/#timeline) - Week overzicht
**External:**
- [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote)
- [gray-matter](https://github.com/jonschlinkert/gray-matter)
- [MDX Documentation](https://mdxjs.com/)
---
## What's Next
Mogelijke verbeteringen voor toekomstige versies:
- 🔜 RSS feed voor release notes
- 🔜 Search functionaliteit
- 🔜 Filters (features/fixes/improvements)
- 🔜 Changelog syntax highlighting
- 🔜 Email notifications bij nieuwe releases
- 🔜 Automatic changelog generation from git commits
- 🔜 Dark mode support
- 🔜 Export naar PDF/Markdown
---
## Lessons Learned
**Wat ging goed:**
- MDX = perfecte balans tussen eenvoud en kracht
- Thematische indeling > chronologische indeling
- Template systeem zorgt voor consistentie
- Static generation = excellent performance
**Wat anders zou kunnen:**
- Syntax highlighting library toevoegen (shiki/prism)
- Table of contents auto-generation
- Reading time estimation
- Related releases suggestions
**Herbruikbaar voor andere projecten:**
- MDX parsing utilities
- Release sidebar component
- Custom MDX components
- Template structure

View File

@@ -1,203 +0,0 @@
---
title: "Screening Systeem"
category: "screening-system"
group: "features"
version: "1.0.0"
releaseDate: "2025-11-23"
status: "completed"
description: "Screening proces voor nieuwe cliënten: hulpvraag, documenten en besluitvorming"
---
## Overzicht
Het screening systeem is de eerste stap in het cliënttraject. Hier wordt de hulpvraag van een nieuwe cliënt beoordeeld en wordt bepaald of de cliënt geschikt is voor intake bij de instelling.
**Kernfunctionaliteit:**
- Hulpvraag vastleggen
- Documenten uploaden (verwijsbrief, intake formulieren)
- Screeningsbesluit nemen
- Afdeling voor intake bepalen
- Activiteitenlog bijhouden
---
## Screening Pagina
De screening pagina bestaat uit vier hoofdonderdelen:
### 1. Hulpvraag
Hier legt u de zorgvraag van de cliënt vast.
**Hoe te gebruiken:**
1. Klik in het tekstveld "Hulpvraag"
2. Beschrijf de hulpvraag van de cliënt in eigen woorden
3. Neem belangrijke context en achtergrond op
4. Klik op **"Opslaan"**
**Tips:**
- Wees specifiek over de aard van de hulpvraag
- Vermeld urgentie indien van toepassing
- Noteer bijzondere omstandigheden
### 2. Screeningsbesluit
Het screeningsbesluit is alleen zichtbaar voor psychologen en behandelaars.
**Opties:**
- **Geschikt voor intake**: Cliënt wordt doorgestuurd voor intake
- **Niet geschikt / doorverwijzen**: Cliënt wordt niet aangenomen
**Stappen voor "Geschikt voor intake":**
1. Selecteer **"Geschikt voor intake"**
2. Kies de **afdeling** waar de intake moet plaatsvinden:
- Volwassenen
- Jeugd (< 18 jaar)
- Forensisch
- Verslaving
- Ouderen (65+)
- FACT
3. Voeg optioneel **notities** toe bij het besluit
4. Klik op **"Opslaan besluit"**
**Stappen voor "Niet geschikt":**
1. Selecteer **"Niet geschikt / doorverwijzen"**
2. Voeg **notities** toe met reden of alternatief
3. Klik op **"Opslaan besluit"**
**Waarschuwing:**
Als er nog geen verwijsbrief is geüpload, wordt een waarschuwing getoond. Het is sterk aan te raden om een verwijsbrief toe te voegen voordat u een definitief besluit neemt.
### 3. Documenten
Upload en beheer documenten voor de screening.
**Documenttypen:**
- Verwijsbrief (vereist voor volledig dossier)
- Intake formulier
- Medische informatie
- Andere relevante documenten
**Document uploaden:**
1. Klik op **"Document toevoegen"**
2. Selecteer het **documenttype**
3. Voeg optionele **notities** toe
4. Upload het bestand
5. Klik op **"Opslaan"**
**Documenten bekijken:**
- Klik op een document in de lijst om te openen
- Zie documenttype, datum en notities
- Download indien nodig
**Documenten verwijderen:**
- Klik op de verwijderknop bij het document
- Bevestig de verwijdering
### 4. Activiteitenlog
De activiteitenlog toont automatisch alle acties die zijn uitgevoerd tijdens de screening.
**Vastgelegde activiteiten:**
- Hulpvraag toegevoegd/gewijzigd
- Document geüpload
- Screeningsbesluit genomen
- Afdeling toegewezen
**Informatie per activiteit:**
- Type activiteit
- Tijdstip
- Details van de actie
De activiteitenlog wordt automatisch bijgewerkt en hoeft niet handmatig te worden beheerd.
---
## Workflow
### Standaard Screening Workflow
1. **Cliënt aanmaken** in het systeem
2. Naar **Screening** tabblad navigeren
3. **Hulpvraag** vastleggen op basis van intake gesprek/formulier
4. **Verwijsbrief** uploaden (indien beschikbaar)
5. Eventueel aanvullende **documenten** uploaden
6. **Screeningsbesluit** nemen:
- Beoordeel of cliënt geschikt is
- Selecteer afdeling bij goedkeuring
- Documenteer reden bij afwijzing
7. Bij goedkeuring: **Intake aanmaken** (zie [Intake Systeem](/documentatie/intake-system))
---
## Toegangsrechten
**Hulpvraag:**
- Zichtbaar voor: Alle behandelaars
- Bewerken: Alle behandelaars
**Screeningsbesluit:**
- Zichtbaar voor: Alleen psychologen en senior behandelaars
- Bewerken: Alleen psychologen en senior behandelaars
**Documenten:**
- Zichtbaar voor: Alle behandelaars met toegang tot cliënt
- Uploaden: Alle behandelaars
- Verwijderen: Alleen uploader en senior behandelaars
**Activiteitenlog:**
- Zichtbaar voor: Alle behandelaars
- Bewerken: Niet mogelijk (automatisch)
---
## Tips voor Gebruik
**Goede praktijk:**
- Upload altijd een verwijsbrief voordat u een screeningsbesluit neemt
- Wees helder en beknopt in de hulpvraag omschrijving
- Documenteer de reden bij afwijzing voor toekomstige referentie
- Controleer de activiteitenlog voor volledigheid van het proces
**Let op:**
- Screeningsbesluiten zijn definitief en vormen basis voor vervolgtraject
- Upload geen documenten met persoonlijke gegevens van derden
- Alle acties worden automatisch gelogd in de activiteitenlog
**Veelgemaakte fouten:**
- Vergeten verwijsbrief te uploaden
- Geen afdeling selecteren bij goedkeuring
- Onvolledige hulpvraag beschrijving
---
## Na Screening
### Bij goedkeuring (Geschikt voor intake):
1. Screeningsbesluit is vastgelegd met toegewezen afdeling
2. Ga naar **Intakes** tabblad
3. Klik op **"Nieuwe Intake"**
4. Selecteer de juiste afdeling (zoals bepaald bij screening)
5. Volg de intake workflow (zie [Intake Systeem](/documentatie/intake-system))
### Bij afwijzing (Niet geschikt):
1. Screeningsbesluit is vastgelegd met reden
2. Informeer cliënt over besluit en alternatieve zorgopties
3. Dossier kan worden gearchiveerd
4. Documentatie blijft beschikbaar voor eventuele vragen
---
## Gerelateerde Documentatie
**Workflow:**
- [Intake Systeem](/documentatie/intake-system) - Vervolg na goedkeuring
- [Client Management](/documentatie/client-management) - Cliëntbeheer
**Interface:**
- [Interface Design System](/documentatie/interface-design) - UI/UX specificaties
**Basis:**
- [Authentication System](/documentatie/authentication) - Toegangsbeheer

View File

@@ -1,152 +0,0 @@
---
title: "Spraakgestuurde Verslaglegging"
category: "spraakgestuurde-verslaglegging"
group: "features"
version: "1.0.0"
releaseDate: "2024-11-25"
status: "completed"
description: "Dicteer rapportages direct in het EPD met real-time spraakherkenning via Deepgram Nova-2"
---
## Overview
Met de nieuwe spraakgestuurde verslaglegging kunnen behandelaren rapportages dicteren in plaats van typen. Het systeem transcribeert spraak naar tekst in real-time, waardoor u efficiënter kunt werken en meer tijd overhoudt voor de cliënt.
**Kernvoordelen:**
- Sneller rapporteren (dicteren vs. typen)
- Hands-free invoer tijdens of direct na het consult
- Nederlandse spraakherkenning van hoge kwaliteit
- Visuele feedback tijdens opname
---
## Hoe te gebruiken
### Stap 1: Open de Rapportage
Navigeer naar een cliëntdossier en klik op **Rapportage** in het menu. U ziet de rapportage-editor met een groene microfoonknop in de werkbalk.
### Stap 2: Start de opname
Klik op de **groene microfoonknop** om de spraakopname te starten.
**Wat u ziet:**
- De knop wordt oranje met een waveform animatie
- Een klein vierkant (stop-icoon) verschijnt
- De browser vraagt eenmalig om microfoontoegang
### Stap 3: Dicteer uw rapportage
Spreek duidelijk en in een normaal tempo. Terwijl u spreekt:
- **Live preview**: Onder de editor verschijnt een oranje balk met de tekst die wordt herkend
- **Automatische interpunctie**: Punten en komma's worden automatisch toegevoegd
- **Pauzes worden herkend**: Na een korte stilte wordt de zin als definitief opgeslagen
**Tips voor optimale herkenning:**
- Spreek in een rustige omgeving
- Houd de microfoon op normale afstand
- Dicteer natuurlijk, alsof u tegen een collega spreekt
- Vermeld bij voorkeur de cliëntnaam aan het begin
### Stap 4: Stop de opname
Klik op het **oranje stop-icoon** om de opname te beëindigen. De getranscribeerde tekst blijft in de editor staan en kan handmatig worden aangepast.
### Stap 5: Bewerk en sla op
- Controleer de getranscribeerde tekst
- Pas eventuele fouten aan
- Klik op **Analyseer met AI** voor automatische categorisatie
- Klik op **Opslaan** om de rapportage vast te leggen
---
## Functionaliteit
### Real-time Transcriptie
De spraak wordt direct naar tekst omgezet terwijl u spreekt. Er is geen wachttijd na de opname - de tekst is meteen beschikbaar.
**Technische specificaties:**
- Model: Deepgram Nova-2 (geoptimaliseerd voor Nederlands)
- Latentie: < 300ms
- Nauwkeurigheid: > 95% voor duidelijke spraak
### Visuele Waveform
Tijdens de opname toont de microfoonknop een mini-waveform met 5 balkjes die meebewegen met uw stem. Dit geeft visuele bevestiging dat de microfoon actief luistert.
### Interim Preview
De oranje preview-balk toont wat het systeem denkt te horen voordat de zin definitief is. Dit verdwijnt automatisch wanneer de tekst wordt bevestigd en in de editor verschijnt.
### Automatische Conceptopslag
Uw rapportage wordt automatisch lokaal opgeslagen terwijl u werkt. Bij een onderbroken sessie (browsercrash, per ongeluk sluiten) kunt u verder waar u gebleven was.
---
## Veelgestelde vragen
### Welke browser ondersteunt spraakopname?
Spraakopname werkt in alle moderne browsers:
- Chrome (aanbevolen)
- Firefox
- Safari
- Edge
Op oudere browsers of zonder microfoon wordt de knop grijs weergegeven.
### Moet ik iets installeren?
Nee, de spraakherkenning werkt volledig in de browser. Bij eerste gebruik vraagt de browser eenmalig om toegang tot uw microfoon.
### Hoe zit het met privacy?
- Audio wordt versleuteld verzonden naar Deepgram voor transcriptie
- Audio wordt **niet** opgeslagen na transcriptie
- Alleen de getranscribeerde tekst wordt bewaard in het EPD
- Deepgram is AVG-compliant
### Wat als de herkenning fouten maakt?
De tekst in de editor is volledig bewerkbaar. Corrigeer eventuele fouten handmatig voordat u opslaat. Voor medische termen of eigennamen kan handmatige correctie nodig zijn.
### Hoeveel mag ik dicteren?
Er geldt een limiet van 5 opnamesessies per uur per gebruiker. Dit is ruim voldoende voor normale rapportage. Bij het bereiken van de limiet verschijnt een melding.
---
## Bekende beperkingen
- **Achtergrondgeluid**: Veel omgevingsgeluid kan de herkenning verstoren
- **Meerdere sprekers**: Het systeem is geoptimaliseerd voor één spreker
- **Medische termen**: Sommige specialistische termen worden mogelijk niet correct herkend
- **Dialect**: Standaard Nederlands wordt het beste herkend
---
## Keyboard shortcuts
| Actie | Shortcut |
|-------|----------|
| Focus naar editor | Tab naar rapportage sectie |
| Start/stop opname | Klik microfoonknop |
| Analyseer met AI | Alt + A |
| Opslaan | Alt + S |
---
## Gerelateerde documentatie
- [Interface Design System](/documentatie/interface-design) - UI patterns en componenten
- [Authentication System](/documentatie/authentication) - Inloggen en toegangsbeheer
---
## Feedback
Ervaart u problemen met de spraakherkenning of heeft u suggesties? Neem contact op met de beheerder of meld het via de feedbackknop in de applicatie.

View File

@@ -1,73 +0,0 @@
---
title: "Behandelplanning"
category: "treatment-planning"
group: "features"
version: "1.0.0"
releaseDate: "TBD"
status: "planned"
description: "SMART doelen formuleren, interventies plannen, voortgang monitoren en behandelplannen beheren"
---
## Overview
Het behandelplanning systeem helpt behandelaars om gestructureerde, evidence-based behandelplannen op te stellen en te monitoren. Met AI-ondersteuning en SMART-doelen methodiek.
**Kernfunctionaliteit:**
- SMART doelen formuleren
- Interventies kiezen en plannen
- Frequentie en duur bepalen
- Meetmomenten plannen
- Voortgang monitoren
- Versie beheer
---
## Binnenkort beschikbaar
Deze documentatie is nog in ontwikkeling. De volgende onderwerpen worden toegevoegd:
**Plan Structuur:**
- SMART doelen definitie
- Interventie library
- Rationale per interventie
- Behandelprotocollen
**AI Generatie:**
- Automatisch plan voorstellen
- Op basis van diagnose en intake
- Aanpasbare templates
- Best practices integratie
**Versie Beheer:**
- Status: Concept, Actief, Afgerond
- Meerdere versies per cliënt
- Wijzigingsgeschiedenis
- Goedkeuringsflow
**Voortgang Tracking:**
- Doelen status (Not Started, In Progress, Completed)
- Sessie registratie
- ROM metingen
- Evaluatie momenten
**Rapportage:**
- Voortgang visualisatie
- KPI dashboards
- Export naar PDF
- Delen met cliënt/verwijzer
---
## Gerelateerde Documentatie
**Interface:**
- [Interface Design System](/documentatie/interface-design) - UI/UX voor behandelplan schermen
**Features:**
- Client Management (binnenkort)
- Intake Systeem (binnenkort)
- AI Integrations (binnenkort)
**Basis:**
- Database Schema (binnenkort)
- [Authentication System](/documentatie/authentication) - Toegangsbeheer

View File

@@ -1,231 +0,0 @@
---
title: "Verpleegkundige Rapportage & Overdracht"
category: "verpleegkundige-overdracht"
group: "features"
version: "1.0.0"
releaseDate: "2025-12-08"
status: "completed"
description: "Centrale module voor verpleegkundige notities, overdracht en AI-gestuurde samenvattingen"
---
## Overzicht
De Verpleegkundige Rapportage & Overdracht module biedt een complete workflow voor het vastleggen van verpleegkundige notities en het voorbereiden van overdrachten aan de volgende dienst. De module bestaat uit twee hoofdschermen: **Rapportage** voor het invoeren van notities en **Overdracht** voor het overzicht met AI-samenvatting.
---
## Navigatie
Via het menu **Verpleegrapportage** met submenu:
- **Rapportage** - Invoerscherm voor verpleegkundige notities (standaard landing page)
- **Overdracht** - Overzicht van alle patiënten met rapportages en AI-samenvatting
**Routes:**
- `/epd/verpleegrapportage` → Rapportage (invoer)
- `/epd/verpleegrapportage/overdracht` → Overdracht (overzicht + AI)
---
## Rapportage Module
### Doel
Centrale werkplek voor het vastleggen van korte verpleegkundige notities tijdens de ronde.
### Layout
Master-detail layout met:
- **Links:** Ronde overzicht met alle patiënten
- **Rechts:** Invoerformulier en tijdlijn voor geselecteerde patiënt
### Patiëntenlijst (Ronde Overzicht)
Per patiënt wordt getoond:
- Naam
- **Alert badge** (rood) - Aantal hoge risico's met tooltip "X hoog risico's"
- **Overdracht badge** (groen vinkje) - Aantal notities voor overdracht met tooltip "X voor overdracht"
Bij selectie krijgt de patiënt een teal achtergrond.
### Invoerformulier
**Risico Alert:** Bij patiënten met hoge risico's verschijnt een rode waarschuwingsbanner bovenaan.
**Categorie Selectie:** Gekleurde pills voor 5 categorieën:
| Categorie | Kleur | Icoon |
|-----------|-------|-------|
| Medicatie | Blauw | Pill |
| ADL/verzorging | Groen | Utensils |
| Gedragsobservatie | Paars | User |
| Incident | Rood | AlertTriangle |
| Algemene observatie | Grijs | FileText |
**Tekstveld:**
- Auto-resize: groeit mee met de tekst (min 56px, max 200px)
- Maximum 500 karakters met teller
- Placeholder past zich aan per categorie
**Overdracht Toggle:** Groen "Overdracht" label om notitie te markeren voor overdracht.
**Verzendknop:** Teal knop met pijl-icoon, disabled wanneer leeg.
### Notities Tijdlijn
Toont alle notities van vandaag, gegroepeerd per dagdeel:
| Dagdeel | Tijden | Icoon | Kleur |
|---------|--------|-------|-------|
| Nacht | 00:00-07:00 | Moon | Paars |
| Ochtend | 07:00-12:00 | Sunrise | Amber |
| Middag | 12:00-17:00 | Sun | Geel |
| Avond | 17:00-24:00 | Sunset | Oranje |
**Per notitie:**
- Tijdstip
- Categorie badge (gekleurd)
- Overdracht toggle (groen vinkje, klikbaar)
- Inhoud
- Bewerken (potlood-icoon, verschijnt bij hover)
- Verwijderen (prullenbak-icoon, met bevestiging)
**Statistieken:** Boven de tijdlijn: totaal aantal notities en aantal voor overdracht.
**Nachtdienst logica:** Notities vóór 07:00 worden toegewezen aan de vorige dag.
---
## Overdracht Module
### Doel
Overzicht van alle patiënten met hun rapportages en AI-gestuurde samenvatting voor overdracht.
### Layout
Master-detail layout met:
- **Links:** Patiëntenlijst met filters en periode selector
- **Rechts:** Patiënt detail met rapportages tijdlijn en AI-samenvatting
### Patiëntenlijst
**Header:**
- Titel "Patiënten" met totaal aantal
- Periode selector: 24 uur (default), 3 dagen, 1 week, 2 weken
- Filter tabs: "Alle" / "Met alerts"
**Per patiënt:**
- Avatar met initialen (rood bij hoog risico)
- Naam, leeftijd, geslacht
- Alert badges:
- Rood (AlertTriangle) - Hoge risico's
- Oranje (Activity) - Afwijkende vitale waarden
- Groen (CheckCircle2) - Voor overdracht
Alle badges hebben tooltips met beschrijvende tekst.
### Patiënt Detail
**Header:**
- Naam, leeftijd, geslacht
- Quick badges (hoog risico, overdracht)
- Link naar volledig dossier
**Risico Block:** Prominent rood blok bij hoge risico's met:
- Risico type (Valrisico, Decubitus, etc.)
- Risico niveau (Zeer hoog, Hoog, Matig, Laag)
- Rationale/toelichting
**Incident Waarschuwing:** Oranje banner bij incidenten in de periode.
**Rapportages Tijdlijn:**
- Gegroepeerd per dag (Vandaag, Gisteren, datum)
- Per dag gegroepeerd per dagdeel
- Visuele tijdlijn met gekleurde nodes
- Type badges (Voortgang, Observatie, etc.)
- Categorie badges voor verpleegkundige notities
- Overdracht indicator (groen vinkje)
- Auteur en tijdstip
### AI Samenvatting
**Locatie:** Rechter kolom, sticky bij scrollen.
**Header:**
- Sparkles icoon (paars)
- Titel "AI Samenvatting"
- Geselecteerde periode label
**Genereren:**
- Knop "Genereer samenvatting"
- Loading state met spinner
- Typische duur: 3-5 seconden
**Resultaat:**
- **Samenvatting:** 1-2 zinnen met kernpunten
- **Aandachtspunten:** Max 5 items met:
- Urgentie indicator (rode rand)
- Klikbare bronverwijzing
- Type bron en datum
- **Actiepunten:** Max 3 items voor volgende dienst
**Bronverwijzingen:**
- Klikbaar: scrollt naar originele rapportage
- Highlight effect: paarse ring rond bron (2 seconden)
- Types: Verpleegkundig, Rapportage, Risicobeoordeling
**Footer:**
- Generatie tijdstip en duur
- "Vernieuwen" knop
---
## Technische Details
### Data Model
**Reports tabel** met type `verpleegkundig`:
- `content` - Notitie tekst (max 500 karakters)
- `structured_data.category` - Categorie (medicatie, adl, gedrag, incident, observatie)
- `include_in_handover` - Boolean voor overdracht
- `shift_date` - Berekende datum (nachtdienst logica)
### API Endpoints
| Endpoint | Methode | Beschrijving |
|----------|---------|--------------|
| `/api/reports` | GET | Ophalen rapportages met filters |
| `/api/reports` | POST | Nieuwe notitie aanmaken |
| `/api/reports/[id]` | PATCH | Notitie bewerken |
| `/api/reports/[id]` | DELETE | Notitie verwijderen (soft delete) |
| `/api/verpleegrapportage/[patientId]` | GET | Patiënt detail met rapportages en risico's |
| `/api/overdracht/patients` | GET | Patiënten overzicht met alerts |
| `/api/overdracht/generate` | POST | AI samenvatting genereren |
### Componenten
**Rapportage:**
- `RapportageWorkspace` - Hoofd container
- `QuickEntryForm` - Invoerformulier met auto-resize
- `TimelineItem` - Notitie in tijdlijn met edit/delete
**Overdracht:**
- `VerpleegrapportageClient` - Client wrapper met state
- `PatientList` - Patiëntenlijst met filters
- `PatientListRow` - Patiënt rij met badges
- `PatientDetail` - Detail view
- `ReportsBlock` - Rapportages tijdlijn
- `AISummaryBlock` - AI samenvatting met bronverwijzingen
---
## Gebruikerstips
1. **Markeer direct voor overdracht** - Scheelt tijd bij voorbereiding
2. **Gebruik "Met alerts" filter** - Snel patiënten vinden die aandacht nodig hebben
3. **Klik op bronverwijzingen** - Verifieer AI-informatie bij de bron
4. **Pas periode aan** - 3 dagen of 1 week voor meer context bij complexe patiënten
5. **Kies juiste categorie** - Helpt bij filteren en overzicht
---
## Gerelateerde Documentatie
- [Spraakgestuurde Verslaglegging](/documentatie/spraakgestuurde-verslaglegging) - Voice input voor rapportages
- [Client Management](/documentatie/client-management) - Patiëntbeheer
- [Interface Design System](/documentatie/interface-design) - UI patterns

View File

@@ -1,134 +0,0 @@
---
title: "Spraakgestuurd Rapporteren"
category: "voice-controlled-reporting"
group: "features"
version: "1.0.0"
releaseDate: "TBD"
status: "in_progress"
description: "AI-gedreven spraakopname voor dictaat en automatische metingen extractie in de GGZ praktijk"
---
## Overview
Behandelaren kunnen efficiënter werken door te dicteren in plaats van te typen. Het systeem transcribeert spraak naar tekst en herkent automatisch metingen en belangrijke data.
**LinkedIn Feedback:**
> "Spraakgestuurd rapporteren met AI is "hot" in ECD/EPD land. Dit kun jij natuurlijk ook! Een psychiater rapporteert (dicteert). In de gesproken tekst zit een meting (bv bloeddruk); die als meting op slaan in ECD. + De psychiater wil "praten" tegen zijn de "Clientkaart" - Toon alle bloedruk metingen van de afgelopen periode. Zonder te klikken naar het "metingen" scherm worden deze netjes getoond. Heel veel succes"
**Twee hoofdfuncties:**
- **Dicteren met data-extractie**: Spreek notities in, AI herkent automatisch metingen (ROM-scores, observaties)
- **Spraakcommando's**: Vraag data op zonder te klikken ("Toon alle PHQ-9 scores afgelopen maand")
---
## Kernfunctionaliteit
**Dictaat met AI Extractie:**
- Behandelaar dicteert behandelnotitie tijdens/na consult
- AI transcribeert en herkent automatisch:
- ROM-scores (PHQ-9, GAD-7, HONOS)
- Observaties (stemming, gedrag, risico's)
- Medicatie informatie
- Data wordt gestructureerd opgeslagen
- Narratieve tekst wordt notitie in dossier
**Spraakcommando's:**
- Hands-free data opvragen tijdens gesprek met cliënt
- Voorbeelden:
- "Toon laatste PHQ-9 score"
- "Wanneer was laatste medicatie wijziging"
- "Laat suïcidaliteit trend zien afgelopen maand"
- Directe visualisatie zonder klikken of typen
---
## Voorbeeld Scenario
**Dictaat na consult:**
```
"Gesprek met meneer Jansen vandaag.
Hij presenteert zich beter dan vorige week.
PHQ-9 score is 12, dat is een daling van 18.
Medicatie wordt goed verdragen, geen bijwerkingen.
Suïcidale gedachten zijn afgenomen naar passief niveau.
Plan: Doorgaan met huidige behandeling, volgende week
weer PHQ-9 afnemen om trend te bevestigen."
```
**AI processing:**
- Herkent: PHQ-9 = 12 (vorige keer 18)
- Herkent: Suïcidaliteit = passief (verbetering)
- Opslaan: ROM-score + observatie + behandelnotitie
- Resultaat: Gestructureerde data + leesbare notitie
**Spraakcommando tijdens consult:**
- Cliënt vraagt: "Hoe ging het de laatste weken?"
- Behandelaar zegt: "Laat PHQ-9 trend zien afgelopen maand"
- Scherm toont automatisch grafiek met scores
- Behandelaar en cliënt bespreken samen de voortgang
---
## Voordelen
**Tijdsbesparing:**
- 50% sneller dan typen
- Minder administratieve last na consult
- Meer tijd voor daadwerkelijk cliënt contact
**Kwaliteit:**
- Gestructureerde data vanaf invoer (geen handmatig invoeren achteraf)
- Minder fouten door tikwerk
- Consistente terminologie door AI
**Gebruikerservaring:**
- Natuurlijke workflow (praten vs typen)
- Hands-free tijdens consult
- Direct visualisatie van data
---
## Techniek (kort)
**Stack:**
- OpenAI Whisper API voor transcriptie (Nederlands)
- GPT-4 voor data extractie uit transcript
- Web Speech API voor spraakcommando's
- Supabase Edge Functions voor processing
**Privacy:**
- Audio automatisch verwijderd na 90 dagen
- Cliënt toestemming vereist
- End-to-end versleuteling
- AVG compliant
---
## MVP Scope
**Eerste versie:**
- ✅ Basis dicteren en transcriptie
- ✅ ROM-scores herkennen (PHQ-9, GAD-7, HONOS)
- ✅ Observaties extractie (stemming, gedrag, risico)
- ✅ Opslag in gestructureerde database
- ✅ Simpele visualisatie van trends
**Later toevoegen:**
- 🔄 Real-time transcriptie tijdens consult
- 🔄 Spraakcommando's voor data queries
- 🔄 Multi-taal support (Engels, Arabisch)
- 🔄 Medicatie informatie herkenning
- 🔄 Automatische behandelplan suggesties
---
## Gerelateerde Documentatie
**Features:**
- [GGZ Overdracht Systeem](/documentatie/nursing-handover-system) - Voice integratie
- Intake Systeem (binnenkort)
- [Interface Design System](/documentatie/interface-design) - UI patterns
**Basis:**
- [Authentication System](/documentatie/authentication) - Toegangsbeheer
- Database Schema (binnenkort)

View File

@@ -1,342 +0,0 @@
---
title: "Webpack Module Resolution Fix"
category: "webpack-module-resolution"
group: "bugs"
version: "0.2.1"
releaseDate: "2024-11-19"
status: "completed"
description: "Fix voor 'Cannot read properties of undefined (reading call)' error bij Server/Client Component imports"
---
## Overview
Opgelost: Kritieke webpack module resolution error die optrad bij het laden van de release notes sidebar. De error `Cannot read properties of undefined (reading 'call')` is een bekend probleem in Next.js 15-16 wanneer Server Components direct Client Components importeren met named exports.
**Impact:** Complete blokkade van de documentatie pagina's totdat opgelost.
**Root cause:** Incompatibiliteit tussen Next.js Server/Client Component boundary en webpack's module chunking systeem.
---
## The Problem
### Error Details
```
TypeError: Cannot read properties of undefined (reading 'call')
at eval (webpack-internal:///356:9:118)
at 356 (.next/dev/static/chunks/app/(marketing)/documentatie/layout.js:28:1)
at ReleasesLayout (app/(marketing)/documentatie/layout.tsx:24:11)
```
### What Was Happening
1. **Server Component** (`layout.tsx`) importeerde direct een **Client Component** (`release-sidebar.tsx`)
2. De Client Component had een `'use client'` directive + named export
3. Webpack probeerde de Client Component in een aparte chunk te bundelen
4. Bij het laden van de webpack chunk verwachtte de loader een functie
5. Door timing/caching issues was de export soms `undefined`
6. Dit gaf de cryptische error: `Cannot read properties of undefined (reading 'call')`
### Code Structure
**Problematische setup:**
```tsx
// layout.tsx (Server Component - async function)
import { ReleaseSidebar } from './components/release-sidebar' // ❌ PROBLEEM
// release-sidebar.tsx (Client Component)
'use client'
export function ReleaseSidebar({ ... }) { ... } // Named export
```
**Waarom dit faalt:**
- Server Component boundary
- Named export van Client Component
- Webpack module federation issues
- Fast Refresh compatibility problemen
---
## The Solution
### Wrapper Component Pattern
Maak een tussenlaag die de import isoleert en een default export gebruikt:
**Bestanden structuur:**
```
app/(marketing)/documentatie/
├── layout.tsx # Server Component
├── components/
│ ├── release-sidebar.tsx # Client Component (named export)
│ └── release-sidebar-wrapper.tsx # Wrapper (default export) ✅
```
**Implementatie:**
```tsx
// release-sidebar-wrapper.tsx (NIEUW)
import { ReleaseSidebar } from './release-sidebar'
import type { ReleaseNote, GroupMetadata, CategoryMetadata } from '@/lib/mdx/documentatie'
interface ReleaseSidebarWrapperProps {
releases: ReleaseNote[]
metadata: {
groups: GroupMetadata[]
categories: CategoryMetadata[]
}
}
export default function ReleaseSidebarWrapper(props: ReleaseSidebarWrapperProps) {
return <ReleaseSidebar {...props} />
}
// layout.tsx (GEWIJZIGD)
import ReleaseSidebar from './components/release-sidebar-wrapper' // ✅ Via wrapper
export default async function ReleasesLayout({ children }: ReleasesLayoutProps) {
const releases = await getAllReleases()
const metadata = await getCategoryMetadata()
return (
<div className="min-h-screen bg-slate-50">
<ReleaseSidebar releases={releases} metadata={metadata} />
{children}
</div>
)
}
```
---
## Why This Works
### Technical Explanation
1. **Default Export vs Named Export**
- Webpack handelt default exports betrouwbaarder af bij chunking
- Named exports kunnen leiden tot undefined module references
- Default exports hebben duidelijkere module boundaries
2. **Import Chain Isolation**
- Wrapper breekt de directe Server → Client import
- Geeft webpack een extra laag om module resolution te doen
- Voorkomt race conditions bij module loading
3. **Consistent Module Pattern**
- Default export = consistent gedrag in webpack
- Minder gevoelig voor HMR (Hot Module Reload) issues
- Betere compatibiliteit met Next.js RSC (React Server Components)
---
## Prevention: Best Practices
### 1. Use Default Exports for Client Components
```tsx
// ✅ GOED - Gebruik dit pattern
'use client'
export default function MyClientComponent() {
return <div>...</div>
}
// ❌ VERMIJD - Dit kan problemen geven
'use client'
export function MyClientComponent() {
return <div>...</div>
}
```
### 2. Create Wrappers for Layout Imports
Als je Client Components in Server Component layouts gebruikt:
```tsx
// Maak altijd een wrapper met default export
// wrapper.tsx
import { ClientComponent } from './client-component'
export default function ClientComponentWrapper(props) {
return <ClientComponent {...props} />
}
```
### 3. Split Type Imports
```tsx
// ✅ GOED - Gescheiden imports
import type { Props } from './types'
import Component from './component'
// ❌ VERMIJD - Mixed imports
import { type Props, Component } from './component'
```
### 4. Clean Cache When Issues Persist
```bash
# Bij persistente webpack errors
rm -rf .next node_modules
pnpm install
npm run dev
```
---
## Related Issues & Context
### Other Potential Problem Areas
Deze Client Components in de codebase gebruiken ook named exports en kunnen hetzelfde probleem krijgen:
**High Risk** (gebruikt in layouts):
- `app/(marketing)/components/minimal-nav.tsx`
- `app/(marketing)/components/hero-section-client.tsx`
- `app/epd/clients/components/*`
**Medium Risk:**
- `app/(marketing)/components/reading-progress.tsx`
- `app/(marketing)/components/marketing-shader.tsx`
**Action Items:**
- Monitor deze tijdens development
- Overweeg wrappers bij problemen
- Langetermijn: refactor naar default exports
### Next.js Version Context
Dit is een **bekend probleem** in:
- Next.js 15.x
- Next.js 16.x (current: 16.0.1)
**Gerelateerde issues:**
- Server/Client Component boundary bugs
- Webpack module federation met RSC
- Fast Refresh compatibility issues
---
## Testing Checklist
- [x] `/documentatie` pagina laadt zonder errors
- [x] Sidebar toont alle groups (Foundation, Features, Infrastructure, Bugs)
- [x] Navigatie tussen release pages werkt
- [x] Hard refresh werkt correct
- [x] HMR (Hot Module Reload) werkt zonder crashes
- [x] Build succeeds (`npm run build`)
- [x] Production build werkt correct
---
## Files Changed
### Modified
1. `app/(marketing)/documentatie/layout.tsx`
- Import via wrapper in plaats van direct
### Added
2. `app/(marketing)/documentatie/components/release-sidebar-wrapper.tsx`
- Nieuwe wrapper component met default export
3. `docs/troubleshooting/webpack-module-resolution-error.md`
- Uitgebreide troubleshooting guide
- 4 oplossingsmethoden gedocumenteerd
- Best practices voor preventie
---
## Performance Impact
**Before Fix:**
- ❌ Complete page crash
- ❌ Geen toegang tot documentatie
- ❌ Dev server instabiel
**After Fix:**
- ✅ Stabiele page loads
- ✅ Fast Refresh werkt correct
- ✅ Geen performance impact (wrapper is pure passthrough)
- ✅ Build tijd: +0.2s (verwaarloosbaar)
- ✅ Bundle size: +0.1kb (wrapper overhead)
---
## Lessons Learned
### What Went Wrong
1. **Aanname over import compatibility**
- Named exports werken meestal, maar niet altijd bij Server/Client boundaries
- Next.js documentatie is niet altijd duidelijk over deze edge cases
2. **Webpack als black box**
- Module resolution errors zijn cryptisch
- `Cannot read properties of undefined` geeft weinig context
- Debugging vereist diep begrip van webpack internals
3. **Cache masking issues**
- Error verscheen/verdween door cache timing
- Maakte root cause analyse moeilijker
### What Went Right
1. **Systematic debugging**
- Eliminatie van mogelijke oorzaken
- Type imports gecheckt
- MDX dependencies geverifieerd
- Webpack output geanalyseerd
2. **Pattern recognition**
- Error type wijst naar module loading issues
- Server/Client boundary is known problem area
- Default vs named export patterns bekend
3. **Documentation**
- Volledige troubleshooting guide gemaakt
- Best practices gedocumenteerd
- Toekomstige problemen sneller op te lossen
---
## Quick Reference
| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
| `undefined reading 'call'` | Named export Client in Server | Use wrapper or default export |
| Intermittent/disappears on refresh | Webpack cache | Clear .next, restart |
| Only in production build | SSR/hydration mismatch | Check dynamic imports |
| After HMR (file save) | Fast Refresh issue | Full reload or restart dev |
---
## Additional Resources
**Internal Documentation:**
- [Troubleshooting Guide](/docs/troubleshooting/webpack-module-resolution-error.md)
- [Release Notes System Docs](/documentatie/release-notes-system)
**External Links:**
- [Next.js Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components)
- [React Server Components](https://react.dev/reference/rsc/server-components)
- [Webpack Module Federation](https://webpack.js.org/concepts/module-federation/)
---
## What's Next
**Monitoring:**
- Watch andere Client Components voor zelfde issue
- Log webpack errors systematisch
- Track Next.js updates voor fixes
**Improvements:**
- Refactor high-risk components naar default exports
- Create ESLint rule om named exports in Client Components te detecteren
- Add automated tests voor Server/Client boundary issues
**Future Prevention:**
- Template voor nieuwe Client Components met default export
- Code review checklist voor Server/Client imports
- Documentation voor team over dit pattern

View File

@@ -1,80 +0,0 @@
{
"hero": {
"title": "EPD Prototype",
"subtitle": "Van 30 minuten documentatie naar 3 minuten",
"description": "Ervaar hoe AI de workflow van GGZ-professionals transformeert. Van intake tot behandelplan in seconden."
},
"demo": {
"title": "Probeer het prototype",
"description": "Log in met onderstaande demo credentials om het systeem te verkennen. Alle data is fictief en voor demonstratie doeleinden.",
"credentials": {
"email": "demo@speedrun.nl",
"password": "demo2024",
"note": "Deze credentials geven toegang tot een demo-omgeving met voorbeeldcliënten en dossiers."
}
},
"features": {
"title": "Wat kan het prototype?",
"items": [
{
"title": "AI-Gestuurde Intake",
"description": "Schrijf een intakeverslag en krijg binnen seconden een gestructureerde samenvatting van 5-8 bullets.",
"time": "< 5 seconden",
"traditional": "15-20 minuten handmatig"
},
{
"title": "Automatische DSM Classificatie",
"description": "Het systeem analyseert de intake en stelt DSM-categorieën voor met severity scoring en rationale.",
"time": "< 3 seconden",
"traditional": "10-15 minuten analyse"
},
{
"title": "Behandelplan Generatie",
"description": "Van probleemprofiel naar compleet SMART behandelplan met doelen, interventies en tijdlijn.",
"time": "< 10 seconden",
"traditional": "30-45 minuten schrijfwerk"
},
{
"title": "B1 Readability",
"description": "Herschrijf complexe teksten naar B1 Nederlands op een druk op de knop voor cliëntcommunicatie.",
"time": "< 3 seconden",
"traditional": "20-30 minuten herschrijven"
}
]
},
"comparison": {
"title": "Tijdsbesparing per cliënt",
"traditional": {
"label": "Traditioneel",
"intake": "20 min",
"profile": "15 min",
"plan": "45 min",
"total": "80 minuten"
},
"speedrun": {
"label": "Met AI Speedrun",
"intake": "5 sec",
"profile": "3 sec",
"plan": "10 sec",
"total": "18 seconden"
},
"savings": "99.6% sneller"
},
"video": {
"title": "Demo Video",
"description": "Bekijk een complete walkthrough van het EPD prototype in 5 minuten.",
"placeholder": "Video wordt binnenkort toegevoegd"
},
"cta": {
"heading": "Klaar om te starten?",
"subheading": "Log in met de demo credentials hierboven en ervaar zelf de kracht van Software on Demand.",
"primaryButton": {
"text": "Probeer het prototype",
"href": "#demo-login"
},
"secondaryButton": {
"text": "Terug naar manifesto",
"href": "/"
}
}
}

View File

@@ -1,121 +0,0 @@
{
"hero": {
"title": {
"main": "\"Staat op de roadmap voor Q3 2026\"",
"accent": "Dat hoef je nooit meer te horen."
},
"subtitle": "Ik bouw live een EPD, stap voor stap, open en transparant. Laten we samen kijken hoe ver we komen.",
"punchline": {
"question": "De vraag is niet óf AI softwareontwikkeling verandert.",
"answer": "De vraag is: accepteer je de beperkingen, of kies je voor mogelijkheden?"
},
"callToAction": {
"primary": {
"text": "🔵 VOLG DE AI SPEEDRUN",
"href": "#timeline"
},
"secondary": {
"text": "⚪ WAAROM EEN EPD?",
"href": "#over"
}
},
"backgroundImage": "https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=2072&auto=format&fit=crop"
},
"sections": [
{
"id": "opening",
"type": "paragraph",
"content": "Het traditionele SaaS-model is simpel: één gebruiker = één licentie. Logisch in een wereld waar elke gebruiker ongeveer evenveel waarde uit software haalt. Maar ook beperkend - je groei is gekoppeld aan het aantal medewerkers bij je klant. Dit model heeft de software-industrie 20 jaar gedomineerd. Vendors optimaliseren voor meer seats, meer modules, meer lock-in. Klanten betalen voor potentieel, niet voor werkelijke waarde."
},
{
"id": "problem",
"type": "paragraph",
"content": "Maar er gebeurt iets fundamenteels. AI maakt het mogelijk om software te genereren in plaats van te configureren. Niet meer kiezen uit wat bestaat, maar bouwen wat je nodig hebt. McKinsey noemt het \"Software on Demand\" - adaptieve diensten die via natuurlijke taal ontstaan. De implicaties zijn enorm."
},
{
"id": "mckinsey-insight",
"type": "insight",
"content": "McKinsey noemt het \"Software on Demand\" - adaptieve diensten die via natuurlijke taal ontstaan."
},
{
"id": "solution",
"type": "paragraph",
"content": "Waar traditionele implementaties 6-12 maanden duren, bouw je met AI een werkende applicatie in 4 weken. Niet omdat AI magisch is, maar omdat je 80% van de standaard-code niet meer hoeft te schrijven. De kostenbasis verschuift compleet - van €100k per jaar naar €50 per maand. Geen armies van consultants. Geen jarenlange developmenttrajecten. Infrastructuur die schaalt met gebruik. AI die de heavy lifting doet. En het belangrijkste: je bezit de code. Je controleert de roadmap. Aanpassing nodig? Dagen, geen kwartalen."
},
{
"id": "proof-numbers",
"type": "insight",
"content": "Van €100k per jaar naar €50 per maand. Geen armies van consultants. Geen jarenlange developmenttrajecten."
},
{
"id": "reality",
"type": "paragraph",
"content": "Dit is geen toekomstmuziek. Ik zie het nu al gebeuren - startups die complete workflows bouwen in de tijd dat enterprises nog aan het onderhandelen zijn over licenties. De vraag is niet óf dit de norm wordt, maar wanneer."
},
{
"id": "impact-vendors",
"type": "statement",
"variant": "dark",
"heading": "Voor software vendors is dit existentieel",
"content": "Hun hele businessmodel - recurring revenue op basis van seats - verdampt als klanten hun eigen oplossingen kunnen bouwen. Voor enterprises opent dit ongekende mogelijkheden. Software die past bij hoe je werkt, niet andersom. Innovatie in weken, niet jaren."
},
{
"id": "shift",
"type": "paragraph",
"content": "We staan nog aan het begin van deze shift. De grote vraag wordt: wie durft eerst? Wie accepteert dat de SAP-implementatie van 5 jaar geleden misschien wel de laatste traditionele software-aankoop was?"
},
{
"id": "experiment",
"type": "paragraph",
"content": "Tijd voor een experiment. Ik ga live bouwen hoe ver je komt met moderne AI-tools. Een EPD als testcase - daar ligt mijn ervaring, daar ken ik de pijn. Van niets naar een werkende applicatie in 4 weken, voor de maandelijkse kosten van één SaaS-licentie."
},
{
"id": "closing",
"type": "paragraph",
"content": "De AI Speedrun. Volg de voortgang. Doe suggesties. Kijk mee hoe het nieuwe development er in de praktijk uitziet. Want de beste manier om de toekomst te voorspellen, is hem bouwen."
}
],
"comparison": {
"heading": "Traditioneel vs AI Speedrun",
"items": [
{
"label": "Tijd",
"traditional": "6-12 maanden",
"aiSpeedrun": "4 weken"
},
{
"label": "Kosten",
"traditional": "€100k+ per jaar",
"aiSpeedrun": "€50 per maand"
},
{
"label": "Setup",
"traditional": "€50k consultants",
"aiSpeedrun": "€200 build cost"
},
{
"label": "Aanpassingen",
"traditional": "18 maanden",
"aiSpeedrun": "Dagen"
},
{
"label": "Code ownership",
"traditional": "Vendor lock-in",
"aiSpeedrun": "Jouw code"
}
]
},
"cta": {
"heading": "Tijd voor een experiment",
"subheading": "Bouw hem gewoon zelf. In 4 weken.",
"primaryButton": {
"text": "Volg de voortgang",
"href": "/build-log"
},
"secondaryButton": {
"text": "Bekijk Demo",
"href": "/demo"
}
}
}

Some files were not shown because too many files have changed in this diff Show More