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:
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>→</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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user