feat(verpleegrapportage): Nieuwe module + opruiming codebase
Verpleegrapportage module: - Nieuwe /epd/verpleegrapportage met patiëntenoverzicht - Rapportage invoer workspace met timeline view - Overdracht overzicht met AI-samenvatting - API endpoints voor verpleegrapportage data Opruiming: - Oude /epd/overdracht en /epd/dagregistratie verwijderd (vervangen) - Oude /api/nursing-logs verwijderd (geconsolideerd naar reports) - Verouderde design docs en reports verwijderd - Fonts verplaatst van docs/ naar public/fonts/ Bugfixes: - Risk-manager: fix constraint violation (db values vs display labels) - Overdracht API: filter op rapportages i.p.v. encounters Database: - Migratie voor consolidatie nursing_logs naar reports tabel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
# Intakes API
|
||||
|
||||
Deze custom REST API ondersteunt de intake-flow na consolidatie van `/clients/` → `/patients/`. Alle endpoints verwachten een geldige Supabase sessie (cookies) en geven JSON terug.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET `/api/intakes?patientId={uuid}`
|
||||
Haalt alle intakes voor één patiënt op (nieuwste eerst).
|
||||
|
||||
**Query parameters**
|
||||
- `patientId` _(verplicht)_ — UUID van de patiënt.
|
||||
|
||||
**Response**
|
||||
```json
|
||||
{
|
||||
"intakes": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"patient_id": "uuid",
|
||||
"title": "Intake - Aanvang zorg",
|
||||
"department": "Volwassenen",
|
||||
"status": "bezig",
|
||||
"start_date": "2025-11-22",
|
||||
"end_date": null,
|
||||
"psychologist_id": null,
|
||||
"notes": null
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/intakes`
|
||||
Maakt een nieuwe intake.
|
||||
|
||||
**Body**
|
||||
```json
|
||||
{
|
||||
"patient_id": "uuid",
|
||||
"title": "Intake - Aanvang zorg",
|
||||
"department": "Volwassenen",
|
||||
"start_date": "2025-11-22",
|
||||
"psychologist_id": "uuid?",
|
||||
"notes": "optional"
|
||||
}
|
||||
```
|
||||
|
||||
**Responses**
|
||||
- `201` + intake object bij succes
|
||||
- `400` met `details[]` bij validatiefout
|
||||
|
||||
### GET `/api/intakes/{intakeId}`
|
||||
Levert één intake. Retourneert `404` als het ID niet bestaat.
|
||||
|
||||
### PUT `/api/intakes/{intakeId}`
|
||||
Partiële update. Ondersteunt `title`, `department`, `status` (`Open`/`Afgerond`), `start_date`, `end_date`, `psychologist_id`, `notes`.
|
||||
|
||||
### DELETE `/api/intakes/{intakeId}`
|
||||
Verwijdert een intake. `204 No Content` bij succes.
|
||||
|
||||
## Fouten
|
||||
- `401`/`403`: geen sessie of onvoldoende rechten.
|
||||
- `400`: ongeldige payload (zie `details`).
|
||||
- `500`: onverwachte fout; check server logs.
|
||||
|
||||
## Implementatieverwijzing
|
||||
- Type definities: `lib/types/intake.ts`
|
||||
- Server actions: `app/epd/patients/[id]/intakes/actions.ts`
|
||||
- API broncode: `app/api/intakes/` en `app/api/intakes/[intakeId]/`
|
||||
@@ -1,304 +0,0 @@
|
||||
# Component Organisatie Strategie
|
||||
|
||||
## Overzicht
|
||||
|
||||
Dit project gebruikt de **colocation pattern** voor component organisatie, een best practice in Next.js App Router architectuur.
|
||||
|
||||
## Twee Component Locaties
|
||||
|
||||
### 1. Centrale Components (`/components`)
|
||||
|
||||
**Doel:** Herbruikbare, generieke components die door meerdere delen van de app gebruikt worden.
|
||||
|
||||
**Structuur:**
|
||||
```
|
||||
components/
|
||||
├── ui/ # Algemene UI componenten (shadcn/ui)
|
||||
│ ├── button.tsx
|
||||
│ ├── dialog.tsx
|
||||
│ ├── dropdown-menu.tsx
|
||||
│ └── ...
|
||||
├── speech-recorder-streaming.tsx # Herbruikbare feature component
|
||||
├── confidence-text.tsx # Herbruikbare display component
|
||||
└── rich-text-editor.tsx # Herbruikbare editor component
|
||||
```
|
||||
|
||||
**Criteria voor centrale components:**
|
||||
- ✅ Gebruikt in 2+ verschillende features/routes
|
||||
- ✅ Geen specifieke business logic voor één feature
|
||||
- ✅ Generiek en configureerbaar via props
|
||||
- ✅ Zou in een component library kunnen zitten
|
||||
|
||||
**Voorbeelden:**
|
||||
```typescript
|
||||
// ✅ Gebruikt in behandeladvies, rapportage, en andere features
|
||||
import { SpeechRecorderStreaming } from '@/components/speech-recorder-streaming';
|
||||
|
||||
// ✅ Generieke UI component
|
||||
import { Button } from '@/components/ui/button';
|
||||
```
|
||||
|
||||
### 2. Route-Specifieke Components (`/app/.../components`)
|
||||
|
||||
**Doel:** Feature-specifieke components die alleen gebruikt worden binnen één route of feature.
|
||||
|
||||
**Structuur:**
|
||||
```
|
||||
app/
|
||||
└── epd/
|
||||
├── components/ # Gedeeld binnen EPD module
|
||||
│ └── epd-sidebar.tsx
|
||||
└── patients/
|
||||
├── components/ # Gedeeld binnen patients feature
|
||||
│ ├── patient-list.tsx
|
||||
│ └── patient-form.tsx
|
||||
└── [id]/
|
||||
└── rapportage/
|
||||
└── components/ # Specifiek voor rapportage feature
|
||||
├── report-composer.tsx
|
||||
├── report-timeline.tsx
|
||||
└── rapportage-workspace.tsx
|
||||
```
|
||||
|
||||
**Criteria voor route-specifieke components:**
|
||||
- ✅ Gebruikt alleen binnen één feature/route
|
||||
- ✅ Bevat feature-specifieke business logic
|
||||
- ✅ Tight coupling met de parent route
|
||||
- ✅ Geen hergebruik in andere features
|
||||
|
||||
**Voorbeelden:**
|
||||
```typescript
|
||||
// ✅ Alleen gebruikt in rapportage feature
|
||||
import { ReportComposer } from './components/report-composer';
|
||||
|
||||
// ✅ Specifieke business logic voor behandeladvies
|
||||
import { TreatmentAdviceForm } from './components/treatment-advice-form';
|
||||
```
|
||||
|
||||
## Hiërarchie & Scope
|
||||
|
||||
Components worden georganiseerd op basis van hun **reuse scope**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ /components │
|
||||
│ ↳ App-wide herbruikbare components │
|
||||
│ (gebruikt in 2+ features) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ imports van
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ /app/epd/components │
|
||||
│ ↳ EPD module-wide components │
|
||||
│ (gedeeld tussen patient, intake, rapportage) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ imports van
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ /app/epd/patients/components │
|
||||
│ ↳ Patient feature components │
|
||||
│ (gedeeld tussen patient routes) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ imports van
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ /app/epd/patients/[id]/rapportage/components │
|
||||
│ ↳ Rapportage page-specifieke components │
|
||||
│ (alleen gebruikt in rapportage) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Statistieken (Huidige State)
|
||||
|
||||
- **Centrale components**: 18 components
|
||||
- **Route-specifieke components**: 56 components
|
||||
- **Duplicaten**: 0 ✅
|
||||
|
||||
## Voordelen van Deze Aanpak
|
||||
|
||||
### 1. **Betere Code Organisation**
|
||||
- Components staan dichtbij waar ze gebruikt worden
|
||||
- Makkelijker te vinden en te onderhouden
|
||||
- Duidelijke scope en ownership
|
||||
|
||||
### 2. **Betere Performance**
|
||||
- Kleinere bundles per route (code splitting)
|
||||
- Alleen relevante components worden geladen
|
||||
- Tree-shaking werkt beter
|
||||
|
||||
### 3. **Betere Developer Experience**
|
||||
- Minder zoeken in grote component directories
|
||||
- Duidelijk wanneer een component herbruikbaar is
|
||||
- Makkelijker refactoren
|
||||
|
||||
### 4. **Schaalbaarheid**
|
||||
- Nieuwe features kunnen onafhankelijk components toevoegen
|
||||
- Geen "god component folder" met 100+ bestanden
|
||||
- Teams kunnen parallel werken zonder conflicts
|
||||
|
||||
## Decision Tree: Waar plaats ik een component?
|
||||
|
||||
```
|
||||
Wordt de component gebruikt in 2+ verschillende features?
|
||||
│
|
||||
├─ Ja → Is het een generieke UI component (button, dialog, etc)?
|
||||
│ │
|
||||
│ ├─ Ja → /components/ui/{name}.tsx
|
||||
│ │
|
||||
│ └─ Nee → /components/{name}.tsx
|
||||
│
|
||||
└─ Nee → Wordt het gedeeld binnen een feature module?
|
||||
│
|
||||
├─ Ja → /app/{feature}/components/{name}.tsx
|
||||
│
|
||||
└─ Nee → /app/{feature}/{subfeature}/components/{name}.tsx
|
||||
```
|
||||
|
||||
## Voorbeelden
|
||||
|
||||
### ✅ Goed: SpeechRecorderStreaming in centrale folder
|
||||
|
||||
**Waarom?** Gebruikt in meerdere features:
|
||||
```typescript
|
||||
// app/epd/patients/[id]/rapportage/components/report-composer.tsx
|
||||
import { SpeechRecorderStreaming } from '@/components/speech-recorder-streaming';
|
||||
|
||||
// app/epd/patients/[id]/intakes/[intakeId]/behandeladvies/components/treatment-advice-form.tsx
|
||||
import { SpeechRecorderStreaming } from '@/components/speech-recorder-streaming';
|
||||
```
|
||||
|
||||
### ✅ Goed: ReportComposer in rapportage/components
|
||||
|
||||
**Waarom?** Alleen gebruikt in rapportage feature:
|
||||
```typescript
|
||||
// app/epd/patients/[id]/rapportage/page.tsx
|
||||
import { ReportComposer } from './components/report-composer';
|
||||
```
|
||||
|
||||
### ❌ Fout: Generieke Button in route folder
|
||||
|
||||
```typescript
|
||||
// ❌ NIET DOEN
|
||||
// app/epd/patients/components/button.tsx
|
||||
export function Button() { ... }
|
||||
|
||||
// ✅ WEL DOEN
|
||||
// components/ui/button.tsx
|
||||
export function Button() { ... }
|
||||
```
|
||||
|
||||
### ❌ Fout: Feature-specifieke component in centrale folder
|
||||
|
||||
```typescript
|
||||
// ❌ NIET DOEN
|
||||
// components/report-composer.tsx (alleen gebruikt in rapportage)
|
||||
|
||||
// ✅ WEL DOEN
|
||||
// app/epd/patients/[id]/rapportage/components/report-composer.tsx
|
||||
```
|
||||
|
||||
## Refactoring Workflow
|
||||
|
||||
### Wanneer een route-component herbruikbaar wordt:
|
||||
|
||||
1. **Identificeer hergebruik**
|
||||
```bash
|
||||
# Check waar component gebruikt wordt
|
||||
grep -r "import.*ComponentName" app/
|
||||
```
|
||||
|
||||
2. **Verplaats naar centrale folder**
|
||||
```bash
|
||||
mv app/feature/components/component.tsx components/
|
||||
```
|
||||
|
||||
3. **Update alle imports**
|
||||
```typescript
|
||||
// Van:
|
||||
import { Component } from '../components/component';
|
||||
|
||||
// Naar:
|
||||
import { Component } from '@/components/component';
|
||||
```
|
||||
|
||||
4. **Generaliseer indien nodig**
|
||||
- Verwijder feature-specifieke logic
|
||||
- Maak configureerbaar via props
|
||||
- Update TypeScript types
|
||||
|
||||
### Wanneer een centrale component feature-specifiek wordt:
|
||||
|
||||
(Dit komt zelden voor, maar kan gebeuren)
|
||||
|
||||
1. Check of component echt nergens anders gebruikt wordt
|
||||
2. Verplaats naar meest specifieke route waar het gebruikt wordt
|
||||
3. Update imports
|
||||
|
||||
## Related Patterns
|
||||
|
||||
### Server vs Client Components
|
||||
|
||||
```typescript
|
||||
// Server Component (default in app/)
|
||||
export default function ReportPage() { ... }
|
||||
|
||||
// Client Component (expliciet markeren)
|
||||
'use client';
|
||||
export function ReportComposer() { ... }
|
||||
```
|
||||
|
||||
Route-specifieke components kunnen zowel server als client components zijn.
|
||||
Centrale components zijn meestal client components (interactief).
|
||||
|
||||
### Composition Pattern
|
||||
|
||||
Route-specifieke components kunnen centrale components gebruiken:
|
||||
|
||||
```typescript
|
||||
// app/epd/patients/[id]/rapportage/components/report-composer.tsx
|
||||
import { SpeechRecorderStreaming } from '@/components/speech-recorder-streaming';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function ReportComposer() {
|
||||
return (
|
||||
<div>
|
||||
<SpeechRecorderStreaming />
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start route-specifiek** - Begin met components in route folders, verplaats alleen naar centraal als er echt hergebruik is
|
||||
2. **Gebruik absolute imports** - `@/components/...` voor centrale, relative voor route-specifieke
|
||||
3. **Avoid premature abstraction** - Wacht tot een component 2x gebruikt wordt voordat je het generaliseert
|
||||
4. **Keep it colocated** - Plaats components zo dichtbij mogelijk bij waar ze gebruikt worden
|
||||
5. **Document reusability** - Als een component generiek is, documenteer dan het gebruik in JSDoc
|
||||
|
||||
## Tools & Commands
|
||||
|
||||
### Find all components in a route:
|
||||
```bash
|
||||
find app/epd/patients/[id]/rapportage -name "*.tsx" -type f
|
||||
```
|
||||
|
||||
### Check component usage:
|
||||
```bash
|
||||
grep -r "import.*ComponentName" app/
|
||||
```
|
||||
|
||||
### Count components per location:
|
||||
```bash
|
||||
find components -name "*.tsx" | wc -l
|
||||
find app -path "*/components/*" -name "*.tsx" | wc -l
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Next.js App Router: Project Organization](https://nextjs.org/docs/app/building-your-application/routing/colocation)
|
||||
- [React: Thinking in React](https://react.dev/learn/thinking-in-react)
|
||||
- [Component Composition Patterns](https://www.patterns.dev/react/compound-pattern)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2024-11-24
|
||||
**Status:** Active pattern in gebruik
|
||||
@@ -1,206 +0,0 @@
|
||||
# Hoe werkt de Authenticatie Flow? 🔐
|
||||
|
||||
Een simpele uitleg van wat er gebeurt wanneer gebruikers zich aanmelden.
|
||||
|
||||
---
|
||||
|
||||
## 📧 Email Confirmatie Flow (nieuwe gebruikers)
|
||||
|
||||
### Stap 1: Gebruiker meldt zich aan
|
||||
```
|
||||
Gebruiker vult in op /login:
|
||||
├─ Email: jan@example.com
|
||||
└─ Wachtwoord: Geheim123!
|
||||
```
|
||||
|
||||
### Stap 2: Supabase stuurt email
|
||||
```
|
||||
Supabase maakt account aan → Stuurt bevestigingsmail
|
||||
|
||||
De email bevat een link zoals:
|
||||
https://aispeedrun.nl/auth/callback?token=xyz123&type=signup
|
||||
└─────┬─────┘
|
||||
Dit is de redirect URL!
|
||||
```
|
||||
|
||||
### Stap 3: Gebruiker klikt op link in email
|
||||
```
|
||||
Browser gaat naar: /auth/callback?token=xyz123
|
||||
|
||||
De callback route doet:
|
||||
1. ✅ Controleert de token
|
||||
2. ✅ Activeert het account
|
||||
3. ✅ Logt gebruiker in
|
||||
4. → Stuurt door naar /epd/clients (omdat ze al wachtwoord hebben)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Wachtwoord Reset Flow
|
||||
|
||||
### Stap 1: Gebruiker klikt "Wachtwoord vergeten?"
|
||||
```
|
||||
Gaat naar: /reset-password
|
||||
Vult in: jan@example.com
|
||||
```
|
||||
|
||||
### Stap 2: Supabase stuurt reset email
|
||||
```
|
||||
Email bevat link:
|
||||
https://aispeedrun.nl/auth/callback?token=abc789&type=recovery&next=/update-password
|
||||
└─────┬─────┘ └────┬────┘
|
||||
Callback route Waar naartoe daarna?
|
||||
```
|
||||
|
||||
### Stap 3: Gebruiker klikt link
|
||||
```
|
||||
/auth/callback ontvangt de token
|
||||
├─ Controleert token ✅
|
||||
├─ Logt gebruiker tijdelijk in
|
||||
└─ Redirect naar: /update-password (van de 'next' parameter)
|
||||
```
|
||||
|
||||
### Stap 4: Nieuw wachtwoord instellen
|
||||
```
|
||||
Op /update-password:
|
||||
├─ Gebruiker vult nieuw wachtwoord in
|
||||
├─ Wachtwoord wordt opgeslagen
|
||||
└─ Redirect naar /login → Gebruiker kan inloggen!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✉️ Magic Link Flow (oude methode)
|
||||
|
||||
### Stap 1: Gebruiker vraagt magic link aan
|
||||
```
|
||||
Vult alleen email in (geen wachtwoord)
|
||||
```
|
||||
|
||||
### Stap 2: Email met magic link
|
||||
```
|
||||
Link: https://aispeedrun.nl/auth/callback?token=magic456
|
||||
```
|
||||
|
||||
### Stap 3: Eerste keer inloggen
|
||||
```
|
||||
/auth/callback detecteert: "nieuwe magic link gebruiker"
|
||||
└─ Redirect naar /set-password (optioneel wachtwoord instellen)
|
||||
├─ Wachtwoord instellen → /epd/clients
|
||||
└─ Overslaan → /epd/clients (blijf magic link gebruiken)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Waarom de Redirect URLs belangrijk zijn
|
||||
|
||||
Supabase moet weten welke URLs **veilig** zijn om naar terug te sturen.
|
||||
|
||||
### Zonder redirect URLs in Supabase:
|
||||
```
|
||||
❌ Link in email: https://aispeedrun.nl/auth/callback?token=xyz
|
||||
↓
|
||||
Supabase zegt: "Deze URL ken ik niet, BLOCKED!"
|
||||
↓
|
||||
Gebruiker ziet error 😞
|
||||
```
|
||||
|
||||
### Met redirect URLs in Supabase:
|
||||
```
|
||||
✅ Link in email: https://aispeedrun.nl/auth/callback?token=xyz
|
||||
↓
|
||||
Supabase zegt: "Deze URL staat in mijn lijst, OK!"
|
||||
↓
|
||||
Gebruiker wordt ingelogd en doorgestuurd 🎉
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 De Site URL vs Redirect URLs
|
||||
|
||||
### Site URL (1 URL)
|
||||
```
|
||||
Dit is je "hoofd" URL waar Supabase denkt dat je app draait.
|
||||
|
||||
Supabase gebruikt dit voor:
|
||||
├─ {{ .ConfirmationURL }} in emails (de basis)
|
||||
└─ Default redirects
|
||||
|
||||
Development: http://localhost:3000
|
||||
Production: https://aispeedrun.nl
|
||||
```
|
||||
|
||||
### Redirect URLs (meerdere URLs mogelijk)
|
||||
```
|
||||
Dit is de "whitelist" van URLs waar Supabase naartoe MAG redirecten.
|
||||
|
||||
Je moet ALLE mogelijke auth callbacks toevoegen:
|
||||
├─ /auth/callback → Email confirmaties, magic links
|
||||
├─ /update-password → Na password reset
|
||||
├─ /set-password → Nieuwe users (optioneel wachtwoord)
|
||||
└─ /reset-password → Password reset pagina
|
||||
|
||||
Voor zowel localhost als productie!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Simpel Gezegd
|
||||
|
||||
1. **Site URL** = Waar draait je app?
|
||||
- Tijdens development: `http://localhost:3000`
|
||||
- Live op internet: `https://aispeedrun.nl`
|
||||
|
||||
2. **Redirect URLs** = Welke paginas mag Supabase bezoeken na login/reset?
|
||||
- Voeg ALLE auth-gerelateerde URLs toe
|
||||
- Voor zowel development als productie
|
||||
|
||||
3. **Email links** = Gebouwd met Site URL + token
|
||||
- Als Site URL = localhost → emails gaan naar localhost ❌
|
||||
- Als Site URL = aispeedrun.nl → emails gaan naar je website ✅
|
||||
|
||||
---
|
||||
|
||||
## 📝 Voorbeeld Flow in de Praktijk
|
||||
|
||||
```
|
||||
[Gebruiker]
|
||||
↓ Registreert op /login
|
||||
[Jouw App]
|
||||
↓ POST naar Supabase "maak account"
|
||||
[Supabase]
|
||||
↓ Stuurt email naar gebruiker
|
||||
↓ Email link = [Site URL]/auth/callback?token=xyz
|
||||
[Email Inbox]
|
||||
↓ Gebruiker klikt link
|
||||
[Browser]
|
||||
↓ Gaat naar aispeedrun.nl/auth/callback?token=xyz
|
||||
[Supabase]
|
||||
↓ Checkt: staat "aispeedrun.nl/auth/callback" in Redirect URLs?
|
||||
↓ JA ✅ → Verifieert token
|
||||
[Jouw App - /auth/callback route]
|
||||
↓ Token geldig? → Login gebruiker
|
||||
↓ Nieuwe gebruiker met wachtwoord?
|
||||
↓ JA → Redirect naar /epd/clients
|
||||
[Gebruiker is ingelogd! 🎉]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❓ Veelgestelde Vragen
|
||||
|
||||
### Waarom krijg ik localhost links in productie emails?
|
||||
→ Je Site URL staat nog op `http://localhost:3000` in Supabase. Wijzig naar `https://aispeedrun.nl`
|
||||
|
||||
### Waarom krijg ik "Invalid Redirect URL" errors?
|
||||
→ De URL staat niet in je Redirect URLs lijst. Voeg hem toe in Supabase Dashboard.
|
||||
|
||||
### Kan ik zowel localhost als productie tegelijk gebruiken?
|
||||
→ JA! Voeg beide toe aan Redirect URLs. Wissel alleen de Site URL afhankelijk van waar je test.
|
||||
|
||||
### Moet ik www. ook toevoegen?
|
||||
→ Als je site bereikbaar is via `www.aispeedrun.nl`, voeg dan ook die URLs toe.
|
||||
|
||||
---
|
||||
|
||||
**Hopelijk is het nu duidelijk! 🚀**
|
||||
@@ -1,183 +0,0 @@
|
||||
# Auth Hook Setup Guide
|
||||
|
||||
## Overzicht
|
||||
|
||||
Deze hook detecteert duplicate emails VOOR een user wordt aangemaakt,
|
||||
waardoor gebruikers direct feedback krijgen als hun email al geregistreerd is.
|
||||
|
||||
**Voordelen:**
|
||||
- ✅ Server-side validatie (kan niet omzeild worden)
|
||||
- ✅ Duidelijke foutmeldingen voor gebruikers
|
||||
- ✅ Betrouwbaar (werkt ongeacht password)
|
||||
- ✅ Case-insensitive email matching
|
||||
- ✅ Email normalisatie (lowercase + trim)
|
||||
|
||||
## Setup (Eerste Keer)
|
||||
|
||||
### Stap 1: Deploy Migration
|
||||
|
||||
**Optie A: Via Supabase Dashboard (Aanbevolen)**
|
||||
|
||||
1. Ga naar: [Supabase Dashboard → SQL Editor](https://supabase.com/dashboard/project/dqugbrpwtisgyxscpefg/sql)
|
||||
2. Open het migration bestand: `supabase/migrations/20251119094908_auth_hook_duplicate_email.sql`
|
||||
3. Kopieer de volledige inhoud
|
||||
4. Plak in de SQL Editor
|
||||
5. Klik "RUN" om de functie aan te maken
|
||||
|
||||
**Optie B: Via Supabase CLI (Als geconfigureerd)**
|
||||
|
||||
```bash
|
||||
npx supabase db push
|
||||
```
|
||||
|
||||
### Stap 2: Verificatie & Instructies
|
||||
|
||||
Run het setup script om te verifiëren dat de functie bestaat:
|
||||
|
||||
```bash
|
||||
pnpm run setup:auth-hook
|
||||
```
|
||||
|
||||
Dit script:
|
||||
- ✅ Checkt of de functie bestaat
|
||||
- 📋 Geeft instructies voor Dashboard configuratie
|
||||
- 🔗 Biedt directe links naar relevante Dashboard pagina's
|
||||
|
||||
### Stap 3: Configureer Hook Link
|
||||
|
||||
**⚠️ Deze stap moet handmatig via Dashboard** (Supabase ondersteunt dit nog niet via API):
|
||||
|
||||
1. Ga naar: [Supabase Dashboard → Auth → Hooks](https://supabase.com/dashboard/project/dqugbrpwtisgyxscpefg/auth/hooks)
|
||||
2. Klik **"Add a new hook"** of **"Enable Hooks"**
|
||||
3. Vul in:
|
||||
- **Hook Type:** "Send a hook on before a user is created" (`before-user-created`)
|
||||
- **Select hook:** "Postgres Function"
|
||||
- **Schema:** `public`
|
||||
- **Function Name:** `hook_check_duplicate_email`
|
||||
4. Klik **"Create hook"** of **"Save"**
|
||||
|
||||
### Stap 4: Test
|
||||
|
||||
Test de hook door:
|
||||
|
||||
1. Ga naar je signup pagina: `http://localhost:3000/login`
|
||||
2. Probeer te registreren met een **bestaand** emailadres (bijv. `demo@mini-ecd.demo`)
|
||||
3. Je zou een error moeten zien: _"Dit emailadres is al geregistreerd. Probeer in te loggen of gebruik 'Wachtwoord vergeten?'."_
|
||||
4. Probeer te registreren met een **nieuw** emailadres
|
||||
5. Dit zou normaal moeten werken (verificatie email verzonden)
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test Case | Scenario | Expected Result |
|
||||
|-----------|----------|-----------------|
|
||||
| TC1 | Signup met nieuw email | ✅ Account aangemaakt, email verzonden |
|
||||
| TC2 | Signup met bestaand email | ❌ Error: "Dit emailadres is al geregistreerd..." |
|
||||
| TC3 | Signup met bestaand email (case variant: `Email@Example.com`) | ❌ Error (case-insensitive match) |
|
||||
| TC4 | Signup met lege/NULL email | ❌ Error: "Email adres is verplicht." |
|
||||
| TC5 | Hook disabled → signup met bestaand email | ⚠️ Oude gedrag (geen error, maar ook geen email) |
|
||||
|
||||
## Herhaalbaarheid
|
||||
|
||||
- ✅ **Functie code** staat in migrations (version controlled)
|
||||
- ⚠️ **Hook link** moet per omgeving handmatig worden geconfigureerd
|
||||
- ✅ **Documentatie** staat in Git
|
||||
- ✅ **Setup script** voor validatie en instructies
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Wat Doet de Hook?
|
||||
|
||||
De `hook_check_duplicate_email` functie:
|
||||
|
||||
1. Ontvangt signup event van Supabase Auth
|
||||
2. Haalt email adres uit event payload
|
||||
3. Valideert email (niet NULL/empty)
|
||||
4. Normaliseert email (lowercase + trim)
|
||||
5. Checkt of email al bestaat in `auth.users` table (case-insensitive)
|
||||
6. Als email bestaat → return error object
|
||||
7. Als email nieuw is → return empty object (allow signup)
|
||||
|
||||
### Security
|
||||
|
||||
- **Security Definer:** Functie draait met elevated permissions
|
||||
- **Search Path:** Expliciet ingesteld op `public, auth` voor veilige schema access
|
||||
- **Permissions:** Alleen `supabase_auth_admin` kan de functie uitvoeren
|
||||
- **Email Enumeration Protection:** Werkt samen met bestaande email confirmation
|
||||
|
||||
### Performance
|
||||
|
||||
- ⚡ Direct database check (geen extra HTTP calls)
|
||||
- ⚡ Indexed lookup op `auth.users.email`
|
||||
- ⚡ Minimale overhead (< 10ms typisch)
|
||||
|
||||
## Toekomstige Verbeteringen
|
||||
|
||||
Zodra Supabase Management API Auth Hooks ondersteunt, kunnen we:
|
||||
|
||||
- [ ] Hook link volledig automatiseren
|
||||
- [ ] Setup script uitbreiden met API calls
|
||||
- [ ] CI/CD pipeline voor hook configuratie
|
||||
- [ ] Automated tests voor hook functionaliteit
|
||||
|
||||
## Flexibiliteit
|
||||
|
||||
De functie is geschreven in standaard PostgreSQL, waardoor:
|
||||
|
||||
- ✅ Werkt met elke auth provider die Postgres functies ondersteunt
|
||||
- ✅ Makkelijk te migreren naar andere auth systemen
|
||||
- ✅ Geen vendor lock-in voor de logica zelf
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Function does not exist" error
|
||||
|
||||
**Probleem:** De hook functie is niet aangemaakt in de database.
|
||||
|
||||
**Oplossing:**
|
||||
1. Controleer of migration is uitgevoerd via Dashboard of CLI
|
||||
2. Run `pnpm run setup:auth-hook` voor verificatie
|
||||
3. Check Supabase logs voor SQL errors
|
||||
|
||||
### Hook lijkt niet te werken
|
||||
|
||||
**Probleem:** Signup met bestaand email geeft geen error.
|
||||
|
||||
**Mogelijke oorzaken:**
|
||||
1. Hook link niet geconfigureerd in Dashboard → Ga naar Auth → Hooks
|
||||
2. Hook is disabled → Check hook status in Dashboard
|
||||
3. Email confirmation staat uit → Check Auth → Email Templates
|
||||
|
||||
**Verificatie:**
|
||||
```sql
|
||||
-- Check of functie bestaat
|
||||
SELECT routine_name
|
||||
FROM information_schema.routines
|
||||
WHERE routine_schema = 'public'
|
||||
AND routine_name = 'hook_check_duplicate_email';
|
||||
|
||||
-- Test functie handmatig
|
||||
SELECT hook_check_duplicate_email('{"user": {"email": "demo@mini-ecd.demo"}}'::jsonb);
|
||||
```
|
||||
|
||||
### Wrong error message
|
||||
|
||||
**Probleem:** Error message klopt niet of is in het Engels.
|
||||
|
||||
**Oplossing:**
|
||||
1. Check of je de laatste versie van de migration hebt gebruikt
|
||||
2. Update functie via SQL Editor met correcte error messages
|
||||
3. Rebuild client error handling (`app/login/page.tsx`)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Supabase Auth Hooks Documentation](https://supabase.com/docs/guides/auth/auth-hooks)
|
||||
- [Bouwplan: Auth Hook Implementation](./bouwplan-auth-hook-duplicate-email-v1.0.md)
|
||||
- [Main README](../README.md)
|
||||
|
||||
## Support
|
||||
|
||||
Voor vragen of problemen:
|
||||
1. Check deze documentatie
|
||||
2. Check Supabase logs in Dashboard
|
||||
3. Run `pnpm run setup:auth-hook` voor diagnostics
|
||||
4. Review `supabase/migrations/20251119094908_auth_hook_duplicate_email.sql`
|
||||
@@ -1,416 +0,0 @@
|
||||
# 🔐 Authentication Setup Guide
|
||||
|
||||
**Project:** AI Speedrun - Mini-ECD Prototype
|
||||
**Epic:** E2 - Database & Auth
|
||||
**Story:** E2.S3 - Demo auth flow
|
||||
**Last Updated:** 2024-11-15
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the authentication implementation for the EPD prototype, including magic link login and demo user accounts.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### 1. Magic Link (Primary Method)
|
||||
|
||||
Users can sign in using email-only authentication:
|
||||
|
||||
1. User enters email on `/login`
|
||||
2. Supabase sends magic link to email
|
||||
3. User clicks link → auto-logged in
|
||||
4. **New users:** Account is automatically created on first magic link request
|
||||
|
||||
**Benefits:**
|
||||
- No password to remember
|
||||
- More secure than traditional passwords
|
||||
- Better UX for demo environment
|
||||
- Auto-creates accounts (no separate signup flow needed)
|
||||
|
||||
---
|
||||
|
||||
### 2. Demo Accounts (For Presentations)
|
||||
|
||||
Pre-configured demo accounts for public demos and presentations:
|
||||
|
||||
| Email | Password | Access Level | Purpose |
|
||||
|-------|----------|--------------|---------|
|
||||
| demo@mini-ecd.demo | Demo2024! | interactive | Main demo account - full CRUD |
|
||||
| readonly@mini-ecd.demo | Demo2024! | read_only | View-only for public demos |
|
||||
| presenter@mini-ecd.demo | Demo2024! | presenter | Live presentations |
|
||||
|
||||
**Access Levels:**
|
||||
- `read_only`: Can view all data, cannot create/edit/delete
|
||||
- `interactive`: Full CRUD access to all features
|
||||
- `presenter`: Full access + special presenter features (future)
|
||||
|
||||
---
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Environment Variables
|
||||
|
||||
Ensure these are set in your `.env.local`:
|
||||
|
||||
```bash
|
||||
# Supabase
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://dqugbrpwtisgyxscpefg.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
# Service role key (for admin operations)
|
||||
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
|
||||
```
|
||||
|
||||
### 2. Create Demo Users
|
||||
|
||||
Run the seed script to create demo user accounts:
|
||||
|
||||
```bash
|
||||
# Make sure you have tsx installed
|
||||
pnpm add -D tsx
|
||||
|
||||
# Run the seed script
|
||||
tsx scripts/seed-demo-users.ts
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
🌱 Starting demo user seed...
|
||||
|
||||
Creating user: demo@mini-ecd.demo...
|
||||
✅ Created auth user: xxx-xxx-xxx
|
||||
✅ Created demo_users entry
|
||||
✨ demo@mini-ecd.demo ready!
|
||||
|
||||
Creating user: readonly@mini-ecd.demo...
|
||||
✅ Created auth user: xxx-xxx-xxx
|
||||
✅ Created demo_users entry
|
||||
✨ readonly@mini-ecd.demo ready!
|
||||
|
||||
Creating user: presenter@mini-ecd.demo...
|
||||
✅ Created auth user: xxx-xxx-xxx
|
||||
✅ Created demo_users entry
|
||||
✨ presenter@mini-ecd.demo ready!
|
||||
|
||||
✅ Demo user seed complete!
|
||||
```
|
||||
|
||||
### 3. Configure Supabase Auth Settings
|
||||
|
||||
Go to Supabase Dashboard → Authentication → Settings:
|
||||
|
||||
#### Email Templates
|
||||
|
||||
Customize the magic link email template:
|
||||
|
||||
**Subject:** "Login to Mini-ECD"
|
||||
|
||||
**Body:**
|
||||
```html
|
||||
<h2>Je magic link is klaar!</h2>
|
||||
<p>Klik op de knop hieronder om in te loggen bij Mini-ECD:</p>
|
||||
<p><a href="{{ .ConfirmationURL }}">Login naar EPD</a></p>
|
||||
<p>Of kopieer deze link naar je browser:</p>
|
||||
<p>{{ .ConfirmationURL }}</p>
|
||||
<p><small>Deze link is 1 uur geldig.</small></p>
|
||||
```
|
||||
|
||||
#### Redirect URLs
|
||||
|
||||
Add these redirect URLs under "Redirect URLs":
|
||||
|
||||
```
|
||||
http://localhost:3000/auth/callback
|
||||
https://yourdomain.com/auth/callback
|
||||
```
|
||||
|
||||
#### Email Auth Settings
|
||||
|
||||
- ✅ Enable Email provider
|
||||
- ✅ Confirm email: OFF (for demo convenience)
|
||||
- ✅ Secure email change: ON
|
||||
- ⏱️ Rate limits: Default (4 emails per hour)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
app/
|
||||
login/
|
||||
page.tsx # Login UI (magic link + demo login)
|
||||
auth/
|
||||
callback/
|
||||
route.ts # Handles magic link callback
|
||||
logout/
|
||||
route.ts # Logout endpoint
|
||||
|
||||
lib/
|
||||
auth/
|
||||
client.ts # Client-side auth helpers
|
||||
server.ts # Server-side auth helpers
|
||||
database.types.ts # Generated Supabase types
|
||||
|
||||
middleware.ts # Route protection
|
||||
scripts/
|
||||
seed-demo-users.ts # Demo user creation script
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Client-Side (React Components)
|
||||
|
||||
```typescript
|
||||
import {
|
||||
loginWithMagicLink,
|
||||
loginWithPassword,
|
||||
logout,
|
||||
getUser,
|
||||
isDemoUser
|
||||
} from '@/lib/auth/client'
|
||||
|
||||
// Magic link login
|
||||
async function handleMagicLink(email: string) {
|
||||
const result = await loginWithMagicLink(email)
|
||||
console.log(result.message) // "Check je email voor de magic link!"
|
||||
}
|
||||
|
||||
// Demo account login
|
||||
async function handleDemoLogin() {
|
||||
await loginWithPassword('demo@mini-ecd.demo', 'Demo2024!')
|
||||
router.push('/clients')
|
||||
}
|
||||
|
||||
// Check current user
|
||||
const user = await getUser()
|
||||
const isDemo = await isDemoUser()
|
||||
|
||||
// Logout
|
||||
await logout() // Redirects to /login
|
||||
```
|
||||
|
||||
### Server-Side (API Routes, Server Components)
|
||||
|
||||
```typescript
|
||||
import {
|
||||
requireAuth,
|
||||
getUser,
|
||||
canWrite,
|
||||
getDemoUserInfo
|
||||
} from '@/lib/auth/server'
|
||||
|
||||
// Require authentication in API route
|
||||
export async function GET() {
|
||||
const session = await requireAuth() // Throws if not authenticated
|
||||
// ... handle request
|
||||
}
|
||||
|
||||
// Check write permissions
|
||||
export async function POST() {
|
||||
const hasWriteAccess = await canWrite()
|
||||
|
||||
if (!hasWriteAccess) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Read-only demo account cannot create data' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// ... create resource
|
||||
}
|
||||
|
||||
// Get demo user info
|
||||
const demoInfo = await getDemoUserInfo()
|
||||
if (demoInfo) {
|
||||
console.log(`Access level: ${demoInfo.access_level}`)
|
||||
console.log(`Usage count: ${demoInfo.usage_count}`)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route Protection
|
||||
|
||||
Routes are protected via `middleware.ts`:
|
||||
|
||||
### Public Routes (No Auth Required)
|
||||
- `/` - Landing page
|
||||
- `/login` - Login page
|
||||
- `/epd` - EPD demo info
|
||||
- `/contact` - Contact form
|
||||
- `/auth/callback` - Auth callback
|
||||
|
||||
### Protected Routes (Auth Required)
|
||||
- `/clients` - Client list
|
||||
- `/clients/*` - Client details, intake, etc.
|
||||
- Any other route not in public list
|
||||
|
||||
**Behavior:**
|
||||
- ✅ Unauthenticated → Redirect to `/login?redirect=/original-path`
|
||||
- ✅ Authenticated on `/login` → Redirect to `/clients`
|
||||
- ✅ Session auto-refreshed in middleware
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
### ✅ Implemented
|
||||
|
||||
1. **RLS Policies**: All database queries filtered by `auth.uid()`
|
||||
2. **Session Management**: Auto-refresh tokens via middleware
|
||||
3. **Secure Cookies**: HTTP-only, secure flags set
|
||||
4. **CSRF Protection**: Built-in Next.js CSRF protection
|
||||
5. **Rate Limiting**: Supabase default (4 emails/hour)
|
||||
6. **Demo User Tracking**: Usage count and last login tracked
|
||||
|
||||
### 🔒 Production Enhancements
|
||||
|
||||
For production deployment:
|
||||
|
||||
1. **Email Confirmation**: Enable email confirmation
|
||||
2. **Password Requirements**: Enforce strong passwords
|
||||
3. **MFA**: Add multi-factor authentication
|
||||
4. **Session Timeout**: Implement auto-logout after inactivity
|
||||
5. **IP Whitelisting**: Restrict demo accounts to specific IPs
|
||||
6. **Audit Logging**: Enhanced tracking of all auth events
|
||||
|
||||
---
|
||||
|
||||
## Demo User Management
|
||||
|
||||
### Checking Demo Status
|
||||
|
||||
```typescript
|
||||
// Check if user is demo user
|
||||
const isDemo = await isDemoUser()
|
||||
|
||||
// Get access level
|
||||
const accessLevel = await getDemoAccessLevel()
|
||||
// Returns: 'read_only' | 'interactive' | 'presenter' | null
|
||||
```
|
||||
|
||||
### Restricting Actions
|
||||
|
||||
```typescript
|
||||
// In API route
|
||||
const demoInfo = await getDemoUserInfo()
|
||||
|
||||
if (demoInfo?.access_level === 'read_only') {
|
||||
return NextResponse.json(
|
||||
{ error: 'This demo account is read-only' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Resetting Demo Accounts
|
||||
|
||||
To reset a demo account (clear data, reset usage):
|
||||
|
||||
```sql
|
||||
-- Reset usage count
|
||||
UPDATE demo_users
|
||||
SET usage_count = 0, last_login_at = NULL
|
||||
WHERE access_level = 'interactive';
|
||||
|
||||
-- Or via Supabase Dashboard: Authentication → Users → Delete user data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Magic link not arriving
|
||||
|
||||
**Causes:**
|
||||
- Email in spam folder
|
||||
- Rate limit exceeded (4 emails/hour)
|
||||
- Email provider blocking Supabase emails
|
||||
|
||||
**Solutions:**
|
||||
1. Check spam folder
|
||||
2. Wait 1 hour and try again
|
||||
3. Use demo account instead
|
||||
4. Configure custom SMTP in Supabase
|
||||
|
||||
### Issue: "Invalid login credentials"
|
||||
|
||||
**Causes:**
|
||||
- Wrong email/password for demo account
|
||||
- Demo user not created yet
|
||||
|
||||
**Solutions:**
|
||||
1. Check credentials match exactly (case-sensitive)
|
||||
2. Run seed script: `tsx scripts/seed-demo-users.ts`
|
||||
3. Verify in Supabase Dashboard → Authentication → Users
|
||||
|
||||
### Issue: Redirect loop on /login
|
||||
|
||||
**Causes:**
|
||||
- Middleware configuration error
|
||||
- Session cookie issues
|
||||
|
||||
**Solutions:**
|
||||
1. Clear browser cookies
|
||||
2. Check middleware.ts public routes config
|
||||
3. Verify `NEXT_PUBLIC_SUPABASE_URL` is correct
|
||||
|
||||
### Issue: "Row violates RLS policy" errors
|
||||
|
||||
**Causes:**
|
||||
- User not properly authenticated
|
||||
- Session expired
|
||||
- RLS policies misconfigured
|
||||
|
||||
**Solutions:**
|
||||
1. Logout and login again
|
||||
2. Check `auth.uid()` returns valid UUID
|
||||
3. Verify RLS policies allow user access
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Magic Link Flow
|
||||
- [ ] Can enter email on /login
|
||||
- [ ] Magic link email received
|
||||
- [ ] Clicking link redirects to /clients
|
||||
- [ ] Session persists after page refresh
|
||||
- [ ] New users auto-created on first login
|
||||
|
||||
### Demo Account Flow
|
||||
- [ ] Can login with demo@mini-ecd.demo
|
||||
- [ ] Can login with readonly@mini-ecd.demo
|
||||
- [ ] Interactive account can create/edit data
|
||||
- [ ] Read-only account blocked from editing
|
||||
- [ ] Demo usage tracked in demo_users table
|
||||
|
||||
### Route Protection
|
||||
- [ ] /clients redirects to /login when not authenticated
|
||||
- [ ] /login redirects to /clients when authenticated
|
||||
- [ ] Public routes accessible without auth
|
||||
- [ ] Session auto-refreshes
|
||||
|
||||
### Logout
|
||||
- [ ] Logout clears session
|
||||
- [ ] Redirects to /login
|
||||
- [ ] Cannot access protected routes after logout
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Supabase Auth Documentation](https://supabase.com/docs/guides/auth)
|
||||
- [Next.js Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware)
|
||||
- Build Plan: `docs/specs/bouwplan-ai-speedrun-marketing-first-v1.1.md` Epic 2
|
||||
- Technical Design: `docs/specs/to-mini-ecd-v1_2.md` § 5.7
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Implemented and Ready for Testing
|
||||
**Next Steps:** E2.S4 - Seed data script (clients + dossiers)
|
||||
@@ -1,304 +0,0 @@
|
||||
# 🔒 Row Level Security (RLS) Documentation
|
||||
|
||||
**Project:** AI Speedrun - Mini-ECD Prototype
|
||||
**Epic:** E2 - Database & Auth
|
||||
**Story:** E2.S2 - RLS policies implementeren
|
||||
**Last Updated:** 2024-11-15
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the Row Level Security (RLS) implementation for the EPD core database tables. RLS is PostgreSQL's security feature that restricts which rows users can access in database queries.
|
||||
|
||||
### Security Model
|
||||
|
||||
- **Authentication Required:** All data access requires a valid Supabase authentication session
|
||||
- **Authorization:** Checked via `auth.uid()` function which returns the authenticated user's UUID
|
||||
- **MVP Level:** All authenticated users can access all data (suitable for demo/single-org)
|
||||
- **Production Path:** Ready to extend with `org_id` filtering for multi-tenancy
|
||||
|
||||
---
|
||||
|
||||
## Tables & Policies
|
||||
|
||||
### 1. Clients Table
|
||||
|
||||
**Purpose:** Basic client information
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view clients | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create clients | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update clients | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete clients | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Production Enhancement:**
|
||||
```sql
|
||||
-- Add organization filtering
|
||||
CREATE POLICY "Users can view own org clients"
|
||||
ON clients FOR SELECT
|
||||
USING (
|
||||
auth.uid() IS NOT NULL AND
|
||||
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Intake Notes Table
|
||||
|
||||
**Purpose:** TipTap/ProseMirror JSON content storage
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view intake notes | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create intake notes | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update intake notes | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete intake notes | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Security Features:**
|
||||
- Full-text search index with Dutch language support
|
||||
- Cascade delete when parent client is deleted
|
||||
- Automatic `updated_at` trigger
|
||||
|
||||
---
|
||||
|
||||
### 3. Problem Profiles Table
|
||||
|
||||
**Purpose:** DSM-light categorization with severity scoring
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view problem profiles | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create problem profiles | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update problem profiles | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete problem profiles | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Data Constraints:**
|
||||
- Category: Must be one of 6 DSM-light categories
|
||||
- Severity: Must be 'laag', 'middel', or 'hoog'
|
||||
- Cascade delete with parent client
|
||||
- SET NULL on source note deletion
|
||||
|
||||
---
|
||||
|
||||
### 4. Treatment Plans Table
|
||||
|
||||
**Purpose:** Treatment plans with JSONB structure and versioning
|
||||
**RLS Enabled:** ✅ Yes
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view treatment plans | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create treatment plans | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can update treatment plans | UPDATE | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can delete treatment plans | DELETE | `auth.uid() IS NOT NULL` |
|
||||
|
||||
**Versioning:**
|
||||
- Each client can have multiple versions (v1, v2, etc.)
|
||||
- Status: 'concept' (editable) or 'gepubliceerd' (locked)
|
||||
- UNIQUE constraint on (client_id, version)
|
||||
|
||||
---
|
||||
|
||||
### 5. AI Events Table
|
||||
|
||||
**Purpose:** Telemetry and debugging for AI API calls
|
||||
**RLS Enabled:** ✅ Yes
|
||||
**Special:** Append-only (no UPDATE/DELETE for regular users)
|
||||
|
||||
#### Policies:
|
||||
|
||||
| Policy Name | Operation | Rule |
|
||||
|------------|-----------|------|
|
||||
| Authenticated users can view AI events | SELECT | `auth.uid() IS NOT NULL` |
|
||||
| Authenticated users can create AI events | INSERT | `auth.uid() IS NOT NULL` |
|
||||
| ~~UPDATE~~ | ❌ | Not allowed (audit trail) |
|
||||
| ~~DELETE~~ | ❌ | Not allowed (audit trail) |
|
||||
|
||||
**Immutability:**
|
||||
- Regular users cannot modify or delete AI events
|
||||
- Ensures audit trail integrity
|
||||
- Service role can bypass RLS for admin cleanup
|
||||
|
||||
---
|
||||
|
||||
## Testing RLS
|
||||
|
||||
### Test 1: Verify RLS is Enabled
|
||||
|
||||
```sql
|
||||
SELECT tablename, rowsecurity as rls_enabled
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename;
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
All 5 tables should show `rls_enabled: true`
|
||||
|
||||
### Test 2: Check Policy Count
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
tablename,
|
||||
COUNT(*) as policy_count,
|
||||
STRING_AGG(cmd, ', ' ORDER BY cmd) as commands
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
GROUP BY tablename;
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
- `ai_events`: 2 policies (INSERT, SELECT)
|
||||
- Other tables: 4 policies each (DELETE, INSERT, SELECT, UPDATE)
|
||||
|
||||
### Test 3: Verify Authentication Check
|
||||
|
||||
```sql
|
||||
SELECT tablename, policyname, cmd, qual
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND qual NOT LIKE '%auth.uid()%';
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
Empty (all policies use `auth.uid()` checks)
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Integration
|
||||
|
||||
TypeScript types are auto-generated and available at `lib/database.types.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
// Usage with Supabase client
|
||||
const supabase = createClient<Database>(url, key)
|
||||
|
||||
// Type-safe queries
|
||||
const { data: clients } = await supabase
|
||||
.from('clients')
|
||||
.select('*')
|
||||
|
||||
// Insert with type checking
|
||||
const { data: newClient } = await supabase
|
||||
.from('clients')
|
||||
.insert({
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
birth_date: '1990-01-01'
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### ✅ Current Implementation
|
||||
|
||||
1. **Secure by Default:** RLS enabled on all tables
|
||||
2. **Authentication Required:** All policies check `auth.uid() IS NOT NULL`
|
||||
3. **Separation of Concerns:** Separate policies for each operation (SELECT, INSERT, UPDATE, DELETE)
|
||||
4. **Audit Trail:** AI events are append-only
|
||||
5. **Foreign Key Constraints:** Automatic cleanup with CASCADE/SET NULL
|
||||
6. **Type Safety:** Generated TypeScript types prevent runtime errors
|
||||
|
||||
### 🔄 Production Enhancements
|
||||
|
||||
When moving to production with multiple organizations:
|
||||
|
||||
1. **Add Organization Column:**
|
||||
```sql
|
||||
ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
|
||||
```
|
||||
|
||||
2. **Update Policies with Org Filtering:**
|
||||
```sql
|
||||
CREATE POLICY "Users can view own org data"
|
||||
ON clients FOR SELECT
|
||||
USING (
|
||||
auth.uid() IS NOT NULL AND
|
||||
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
|
||||
);
|
||||
```
|
||||
|
||||
3. **Add Role-Based Access:**
|
||||
```sql
|
||||
CREATE POLICY "Admins can view all"
|
||||
ON clients FOR SELECT
|
||||
USING (
|
||||
auth.uid() IS NOT NULL AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'superadmin')
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
4. **Implement Row-Level Ownership:**
|
||||
```sql
|
||||
CREATE POLICY "Users can update own records"
|
||||
ON intake_notes FOR UPDATE
|
||||
USING (author = auth.uid());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "new row violates row-level security policy"
|
||||
|
||||
**Cause:** Trying to insert/update data that doesn't satisfy RLS WITH CHECK
|
||||
**Solution:** Ensure user is authenticated and data meets policy requirements
|
||||
|
||||
### Issue: No data returned despite existing rows
|
||||
|
||||
**Cause:** User not authenticated or RLS USING clause filters out all rows
|
||||
**Solution:** Verify `auth.uid()` returns a valid UUID
|
||||
|
||||
### Issue: Service role queries still restricted
|
||||
|
||||
**Cause:** Using anon key instead of service role key
|
||||
**Solution:** Use `SUPABASE_SERVICE_ROLE_KEY` for admin operations
|
||||
|
||||
```typescript
|
||||
// Service role bypasses RLS
|
||||
const supabase = createClient(url, serviceRoleKey)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration History
|
||||
|
||||
| Migration | Date | Changes |
|
||||
|-----------|------|---------|
|
||||
| `20241115000002_create_epd_core_tables.sql` | 2024-11-15 | Initial RLS policies (demo-level) |
|
||||
| `20241115000003_enhance_rls_policies.sql` | 2024-11-15 | Granular policies per operation + ai_events immutability |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Supabase RLS Documentation](https://supabase.com/docs/guides/auth/row-level-security)
|
||||
- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
||||
- Technical Design: `docs/specs/to-mini-ecd-v1_2.md` § 2.4
|
||||
- Build Plan: `docs/specs/bouwplan-ai-speedrun-marketing-first-v1.1.md` Epic 2
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Implemented and Tested
|
||||
**Next Steps:** E2.S3 - Demo auth flow
|
||||
@@ -1,494 +0,0 @@
|
||||
# Datamodel Mini-ECD: FHIR-compliant GGZ Dossier
|
||||
|
||||
**Versie:** 1.1
|
||||
**Datum:** 21 november 2024
|
||||
**Status:** In ontwikkeling
|
||||
|
||||
---
|
||||
|
||||
## Overzicht
|
||||
|
||||
Het Mini-ECD gebruikt een datamodel gebaseerd op **FHIR (Fast Healthcare Interoperability Resources)**, de internationale standaard voor uitwisseling van zorggegevens. Dit maakt toekomstige integratie met MedMIJ (patiëntportalen) en Koppeltaal (eHealth apps) mogelijk zonder grote aanpassingen.
|
||||
|
||||
Het datamodel bestaat uit **13 kernonderdelen** die samen het complete GGZ-traject ondersteunen: van aanmelding tot behandelplan, inclusief doelen, toestemmingen en belangrijke waarschuwingen.
|
||||
|
||||
---
|
||||
|
||||
## De 13 bouwstenen van het dossier
|
||||
|
||||
### 1. **Behandelaren** (`practitioners`)
|
||||
**Wat is het?**
|
||||
Alle zorgprofessionals die in het systeem werken: psychologen, psychiaters, gz-psychologen, verpleegkundigen, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- BIG-nummer (indien geregistreerd)
|
||||
- AGB-code
|
||||
- Naam en voorletters
|
||||
- Kwalificaties (bijv. "GZ-psycholoog", "Psychotherapeut")
|
||||
- Contactgegevens
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Practitioner** resource. Dit maakt het mogelijk om behandelaren later uit te wisselen met andere systemen (bijvoorbeeld voor verwijzingen).
|
||||
|
||||
---
|
||||
|
||||
### 2. **Instellingen** (`organizations`)
|
||||
**Wat is het?**
|
||||
De GGZ-organisaties zelf: jouw instelling, maar ook externe organisaties waarmee je samenwerkt.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- AGB-code instelling
|
||||
- KVK-nummer
|
||||
- Naam en eventuele nevenvestigingen
|
||||
- Contactgegevens en adres
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Organization** resource. Nodig voor facturatie, verwijzingen en juridische verantwoordelijkheid.
|
||||
|
||||
---
|
||||
|
||||
### 3. **Cliënten** (`patients`)
|
||||
**Wat is het?**
|
||||
De patiënten/cliënten die behandeling krijgen.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- BSN (verplicht)
|
||||
- Naam, geboortedatum, geslacht
|
||||
- Adres en contactgegevens
|
||||
- Verzekeringsgegevens
|
||||
- Huisarts (naam + AGB-code)
|
||||
- Noodcontactpersoon
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Patient** resource. Dit is de basis voor alle andere gegevens in het dossier. Het correspondeert met de Nederlandse **ZIB Patient** (ZorgInformatieBouwsteen).
|
||||
|
||||
**Privacy:**
|
||||
BSN wordt versleuteld opgeslagen en is alleen toegankelijk voor geautoriseerde behandelaren.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Contactmomenten** (`encounters`)
|
||||
**Wat is het?**
|
||||
Elk contact tussen cliënt en behandelaar: intakegesprek, behandelsessie, telefonisch consult, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type contact (intake, diagnostiek, behandeling, crisis)
|
||||
- Status (gepland, bezig, afgerond)
|
||||
- Wanneer (start- en eindtijd)
|
||||
- Wie (behandelaar + cliënt)
|
||||
- Waar (polikliniek, online, kliniek)
|
||||
- Waarom (aanmeldingsreden, klachten)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Encounter** resource. Dit is cruciaal omdat alle andere gegevens (diagnoses, observaties, behandelplannen) gekoppeld worden aan een specifiek contactmoment. Hierdoor kun je later zien: "Deze diagnose is gesteld tijdens de intake van 15 maart 2024".
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB Contact**.
|
||||
|
||||
---
|
||||
|
||||
### 5. **Diagnoses** (`conditions`)
|
||||
**Wat is het?**
|
||||
De vastgestelde diagnoses volgens DSM-5 of ICD-10. Dit kunnen zowel definitieve diagnoses zijn als voorlopige diagnoses.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- DSM-5 code (bijv. "F32.2")
|
||||
- Omschrijving (bijv. "Depressieve episode, ernstig")
|
||||
- Status (actief, in remissie, opgelost)
|
||||
- Ernst (mild, matig, ernstig)
|
||||
- Zekerheid (voorlopig, bevestigd, uitgesloten)
|
||||
- Wanneer ontstaan / wanneer opgelost
|
||||
- Wie stelde de diagnose vast
|
||||
- Bij welk contactmoment
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Condition** resource. Dit onderscheidt tussen "encounter diagnosis" (gesteld tijdens een specifiek contact) en "problem list item" (langlopend probleem op de problemlijst).
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB Problem**.
|
||||
|
||||
**Voorbeeld:**
|
||||
Een cliënt meldt zich aan met depressieve klachten. Na de intake wordt voorlopig "F32.2 - Depressieve episode, ernstig" vastgesteld. Na behandeling verandert de status naar "in remissie".
|
||||
|
||||
---
|
||||
|
||||
### 6. **Observaties & Metingen** (`observations`)
|
||||
**Wat is het?**
|
||||
Alle metingen, scores, risico-inschattingen en observaties tijdens de behandeling.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Wat werd geobserveerd (bijv. "Suïcidaliteit", "PHQ-9 score", "Bloeddruk")
|
||||
- Uitkomst (bijv. "Hoog risico", "Score: 18 punten", "120/80")
|
||||
- Interpretatie (normaal, afwijkend hoog, afwijkend laag)
|
||||
- Wanneer gemeten
|
||||
- Door wie
|
||||
- Bij welk contactmoment
|
||||
|
||||
**Categorieën:**
|
||||
- **ROM-metingen**: PHQ-9, GAD-7, OQ-45, etc.
|
||||
- **Risico-inschattingen**: Suïcidaliteit, agressie, verwaarlozing
|
||||
- **Middelengebruik**: Alcohol, drugs, medicatie
|
||||
- **Vitale functies**: Bloeddruk, hartslag (indien relevant)
|
||||
- **Sociale anamnese**: Werk, relatie, financiën
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Observation** resource. Dit is een zeer flexibele resource die allerlei soorten metingen kan bevatten. Door standaard codes te gebruiken (SNOMED, LOINC) kunnen deze later gedeeld worden met andere systemen.
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB Alert** en **ZIB LaboratoryTestResult**.
|
||||
|
||||
**Voorbeeld:**
|
||||
- ROM-vragenlijst PHQ-9 ingevuld: score 18 (matig-ernstige depressie)
|
||||
- Risico-inschatting: "Suïcidale gedachten aanwezig, geen concrete plannen" → interpretatie: matig risico
|
||||
|
||||
---
|
||||
|
||||
### 7. **Medicatie** (`medication_statements`)
|
||||
**Wat is het?**
|
||||
De medicatie die de cliënt gebruikt of heeft gebruikt. Dit kan voorgeschreven zijn door de psychiater, maar ook medicatie van de huisarts.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Medicijnnaam (bijv. "Sertraline 50mg tablet")
|
||||
- ATC-code (internationale medicijncode)
|
||||
- Status (actief, gestopt, gepland)
|
||||
- Dosering (bijv. "1 tablet 's ochtends")
|
||||
- Toedieningsweg (oraal, intraveneus, etc.)
|
||||
- Startdatum / stopdatum
|
||||
- Reden van gebruik (bijv. "Depressie")
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **MedicationStatement** resource. Dit registreert wat de patiënt daadwerkelijk gebruikt (niet wat voorgeschreven is - dat zou een MedicationRequest zijn).
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB MedicationUse** en is onderdeel van het **MedicatieProces 9.0**.
|
||||
|
||||
**Let op:**
|
||||
Voor volledige medicatiegeschiedenis moet later gekoppeld worden met het Landelijk Schakelpunt (LSP) of andere medicatieservices.
|
||||
|
||||
---
|
||||
|
||||
### 8. **Behandelplannen** (`care_plans`)
|
||||
**Wat is het?**
|
||||
Het overzicht van de geplande behandeling: wat gaan we doen, waarom, en met welk doel?
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Titel (bijv. "Behandelplan depressie")
|
||||
- Beschrijving van de aanpak
|
||||
- Status (concept, actief, afgerond, gestopt)
|
||||
- Looptijd (startdatum - einddatum)
|
||||
- Behandeldoelen (bijv. "PHQ-9 score < 10", "Herstel dagelijks functioneren")
|
||||
- Welke diagnoses worden behandeld
|
||||
- Wie is de regiebehandelaar
|
||||
- Welk zorgteam is betrokken
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **CarePlan** resource. Dit is de container voor alle behandelactiviteiten en koppelt diagnoses aan interventies.
|
||||
|
||||
Dit correspondeert met de Nederlandse **ZIB TreatmentDirective**.
|
||||
|
||||
**Koppeltaal-integratie:**
|
||||
Dit is ook de resource die Koppeltaal gebruikt om eHealth-apps te koppelen aan de behandeling. Bijvoorbeeld: "Opdracht: 3x per week mindfulness oefening via app X".
|
||||
|
||||
---
|
||||
|
||||
### 9. **Behandelactiviteiten** (`care_plan_activities`)
|
||||
**Wat is het?**
|
||||
De concrete activiteiten binnen een behandelplan: gesprekken, medicatie, huiswerkopdrachten, ROM-metingen, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Omschrijving (bijv. "Individuele CGT sessies", "ROM-meting PHQ-9")
|
||||
- Status (nog niet gestart, gepland, bezig, afgerond)
|
||||
- Planning (bijv. "1x per week, 12 sessies")
|
||||
- Uitvoerende behandelaar
|
||||
- Locatie (polikliniek, online, kliniek)
|
||||
- Voortgang (vrije tekst updates)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit **CarePlan.activity**. Dit is onderdeel van de CarePlan resource en beschrijft de "wat en wanneer" van de behandeling.
|
||||
|
||||
**Voorbeeld activiteiten:**
|
||||
- Individuele CGT: 1x/week, 12 sessies
|
||||
- Medicatie: Sertraline 50mg dagelijks
|
||||
- ROM-meting: Elke 4 weken PHQ-9 invullen
|
||||
- Huiswerk: Dagboek bijhouden
|
||||
|
||||
---
|
||||
|
||||
### 10. **Toestemmingen & Wilsverklaringen** (`consents`)
|
||||
**Wat is het?**
|
||||
Alle toestemmingen van de cliënt: voor behandeling, voor gegevensuitwisseling (AVG), wilsverklaringen (niet-reanimeren, euthanasie-verklaring, etc.).
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type toestemming (behandeling, privacy/AVG, wilsverklaring, onderzoek)
|
||||
- Status (actief, ingetrokken, afgewezen)
|
||||
- Categorie (niet-reanimeren, advance directive, noodgevallen-only)
|
||||
- Datum en wie gaf toestemming
|
||||
- Geldigheid (startdatum - einddatum)
|
||||
- Wat mag wel/niet (toegang, delen, correctie)
|
||||
- Met wie mag gedeeld worden (specifieke behandelaren, organisaties)
|
||||
- Documenten (ondertekende verklaring als PDF)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Consent** resource. Dit correspondeert met de Nederlandse **ZIB AdvanceDirective**.
|
||||
|
||||
**AVG-compliance:**
|
||||
Dit is cruciaal voor AVG-naleving. Hiermee registreer je:
|
||||
- Toestemming voor behandeling (informed consent)
|
||||
- Toestemming voor delen met huisarts/andere zorgverleners
|
||||
- Intrekking van toestemming
|
||||
- Wilsverklaringen die juridisch bindend zijn
|
||||
|
||||
**Voorbeelden:**
|
||||
- "Toestemming behandeling depressie" (informed consent)
|
||||
- "Geen toestemming delen met huisarts" (privacy)
|
||||
- "Niet-reanimeren verklaring" (wilsverklaring)
|
||||
- "Toestemming opname behandelgegevens in landelijke uitwisseling" (MedMIJ)
|
||||
|
||||
---
|
||||
|
||||
### 11. **Waarschuwingen & Alerts** (`flags`)
|
||||
**Wat is het?**
|
||||
Belangrijke waarschuwingen die behandelaren **direct** moeten zien bij het openen van een dossier. Denk aan veiligheidsrisico's, allergieën, of gedragswaarschuwingen.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type waarschuwing (veiligheid, klinisch, gedrag, infectie, allergie)
|
||||
- Alert inhoud (bijv. "Suïciderisico", "Agressie naar hulpverleners")
|
||||
- Prioriteit (hoog, middel, laag)
|
||||
- Status (actief, inactief)
|
||||
- Geldigheid (startdatum - einddatum)
|
||||
- Wie maakte de alert
|
||||
- Gerelateerde diagnoses of observaties
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **Flag** resource. Dit correspondeert met de Nederlandse **ZIB Alert**.
|
||||
|
||||
**Verschil met Observations:**
|
||||
Observations zijn metingen/bevindingen. Flags zijn **actieve waarschuwingen** die aandacht vragen.
|
||||
|
||||
**Categorieën:**
|
||||
- **Safety (veiligheid)**: Suïciderisico, zelfverwaarlozing, valrisico
|
||||
- **Clinical (klinisch)**: Ernstige allergie voor medicatie, infectiegevaar
|
||||
- **Behavioral (gedrag)**: Agressie naar hulpverleners, grensoverschrijdend gedrag
|
||||
- **Administrative**: Geen-toon status (privacy), wanbetaler
|
||||
|
||||
**Voorbeeld flags:**
|
||||
- 🔴 "HOOG SUÏCIDERISICO - Concrete plannen, middelen aanwezig"
|
||||
- 🟠 "Agressie naar vrouwelijke hulpverleners - Alleen mannelijke behandelaar"
|
||||
- 🟡 "Allergie: Penicilline - anafylactische shock"
|
||||
- ⚪ "Geen toestemming contact familie - Privacy verzoek"
|
||||
|
||||
**In de UI:**
|
||||
Flags worden prominent weergegeven (rood banner bovenaan dossier) zodat ze niet gemist kunnen worden.
|
||||
|
||||
---
|
||||
|
||||
### 12. **Documenten** (`document_references`)
|
||||
**Wat is het?**
|
||||
Alle documenten in het dossier: intakeverslagen, behandelplannen, brieven aan huisarts, ROM-rapporten, etc.
|
||||
|
||||
**Belangrijkste gegevens:**
|
||||
- Type document (intakeverslag, behandelplan, brief, rapport)
|
||||
- Status (concept, definitief, vervangen)
|
||||
- Datum
|
||||
- Auteur (behandelaar)
|
||||
- Gekoppeld aan welk contactmoment
|
||||
- Content (Markdown tekst, PDF, of link naar bestand)
|
||||
|
||||
**Waarom FHIR?**
|
||||
In FHIR heet dit een **DocumentReference** resource. Dit zorgt ervoor dat documenten doorzoekbaar zijn en gekoppeld kunnen worden aan specifieke momenten in de behandeling.
|
||||
|
||||
**MedMIJ-integratie:**
|
||||
Via MedMIJ kunnen cliënten later hun eigen documenten ophalen in een persoonlijke gezondheidsomgeving (PGO-app).
|
||||
|
||||
---
|
||||
|
||||
## Hoe hangen deze onderdelen samen?
|
||||
|
||||
```
|
||||
Cliënt (Patient)
|
||||
│
|
||||
├─── heeft Toestemmingen (Consents) ⚠️ AVG-compliant
|
||||
│
|
||||
├─── heeft Waarschuwingen (Flags) 🚨 Altijd zichtbaar
|
||||
│
|
||||
└─── heeft Contactmomenten (Encounters)
|
||||
│
|
||||
├─── leidt tot Diagnoses (Conditions)
|
||||
│ └─── ondersteund door Observaties (Observations)
|
||||
│
|
||||
├─── gebruikt Medicatie (MedicationStatements)
|
||||
│
|
||||
├─── krijgt Behandelplan (CarePlan)
|
||||
│ ├─── met Doelen (Goals) 🎯 Meetbaar
|
||||
│ └─── met Activiteiten (CarePlanActivities)
|
||||
│
|
||||
└─── resulteert in Documenten (DocumentReferences)
|
||||
|
||||
Uitgevoerd door Behandelaar (Practitioner)
|
||||
Binnen Instelling (Organization)
|
||||
```
|
||||
|
||||
**Nieuwe verbindingen:**
|
||||
- **Goals** zijn gekoppeld aan **CarePlan** en **Conditions**
|
||||
- **Goals** worden gemeten via **Observations** (ROM-scores)
|
||||
- **Flags** zijn gekoppeld aan **Conditions** en **Observations** (wat veroorzaakt de alert)
|
||||
- **Consents** bepalen wie **DocumentReferences** mag inzien
|
||||
|
||||
---
|
||||
|
||||
## Waarom FHIR gebruiken?
|
||||
|
||||
### **1. Toekomstbestendig**
|
||||
FHIR is de internationale standaard voor zorggegevens. Alle moderne zorgsystemen ondersteunen dit. Door vanaf dag 1 FHIR-compliant te bouwen, kunnen we later makkelijk integreren met:
|
||||
- MedMIJ (patiëntportalen)
|
||||
- Koppeltaal (eHealth apps)
|
||||
- Landelijk Schakelpunt (LSP)
|
||||
- Andere GGZ-instellingen
|
||||
- Huisartseninformatiesystemen
|
||||
|
||||
### **2. Herbruikbaarheid**
|
||||
Elk onderdeel ("resource") kan apart uitgewisseld worden. Bijvoorbeeld:
|
||||
- Huisarts vraagt diagnoses op via FHIR API
|
||||
- Cliënt haalt eigen medicatielijst op via MedMIJ
|
||||
- eHealth app ontvangt behandelplan via Koppeltaal
|
||||
|
||||
### **3. Geen vendor lock-in**
|
||||
Omdat we een open standaard gebruiken, zijn we niet afhankelijk van één leverancier. Data kan altijd geëxporteerd en geïmporteerd worden in FHIR-formaat.
|
||||
|
||||
### **4. Bewezen technologie**
|
||||
FHIR wordt wereldwijd gebruikt door duizenden ziekenhuizen, klinieken en zorginstellingen. Alle grote EPD-leveranciers ondersteunen het.
|
||||
|
||||
---
|
||||
|
||||
## MedMIJ & Koppeltaal: Wat betekent dit?
|
||||
|
||||
### **MedMIJ - Patiëntportalen**
|
||||
MedMIJ is het Nederlandse afsprakenstelsel waarmee patiënten hun medische gegevens kunnen ophalen in een PGO-app (Persoonlijke Gezondheidsomgeving).
|
||||
|
||||
**Voor GGZ is de "Basisgegevens GGZ 2.0" specificatie relevant:**
|
||||
- 24 zorginformatiebouwstenen (ZIBs)
|
||||
- Inclusief: diagnoses, medicatie, behandelplan, contactmomenten
|
||||
|
||||
**Ons datamodel ondersteunt dit omdat:**
|
||||
- Alle velden volgen de MedMIJ FHIR profielen
|
||||
- DSM-5 codes zijn opgenomen
|
||||
- Juridische status kan vastgelegd worden
|
||||
- Medicatie volgens MedicatieProces 9.0
|
||||
|
||||
**In de toekomst kunnen we:**
|
||||
- Een FHIR API bouwen die MedMIJ-compliant is
|
||||
- Cliënten toegang geven tot hun eigen dossier via een PGO-app
|
||||
- Automatisch gegevens uitwisselen met andere zorgaanbieders
|
||||
|
||||
### **Koppeltaal - eHealth Apps**
|
||||
Koppeltaal is de standaard waarmee GGZ-instellingen eHealth apps kunnen koppelen aan hun EPD.
|
||||
|
||||
**Voorbeeld:**
|
||||
Behandelaar schrijft voor: "Doe dagelijks de mindfulness oefening in app MindDistrict"
|
||||
→ Koppeltaal zorgt dat dit automatisch in het EPD en in de app komt te staan
|
||||
→ Voortgang komt automatisch terug in het EPD
|
||||
|
||||
**Ons datamodel ondersteunt dit omdat:**
|
||||
- CarePlan resource volgt Koppeltaal specificaties
|
||||
- Activities kunnen gekoppeld worden aan externe apps
|
||||
- Status updates worden automatisch verwerkt
|
||||
|
||||
---
|
||||
|
||||
## Privacy & Beveiliging
|
||||
|
||||
### **Encryptie**
|
||||
- BSN wordt versleuteld opgeslagen
|
||||
- Communicatie via HTTPS/TLS
|
||||
|
||||
### **Toegangscontrole (RLS)**
|
||||
- Behandelaren zien alleen hun eigen cliënten
|
||||
- Cliënten kunnen later hun eigen data inzien (via patiëntenportaal)
|
||||
- Auditlog houdt bij wie wat wanneer heeft bekeken
|
||||
|
||||
### **AVG-compliance**
|
||||
- Recht op inzage: cliënt kan eigen data opvragen
|
||||
- Recht op vergetelheid: data kan verwijderd worden
|
||||
- Logging: alle acties worden gelogd
|
||||
- Bewaartermijnen: automatische archivering na X jaar
|
||||
|
||||
---
|
||||
|
||||
## Technische implementatie
|
||||
|
||||
### **Database: PostgreSQL (Supabase)**
|
||||
- Type-safe met ENUMs voor statussen
|
||||
- Automatische timestamps (created_at, updated_at)
|
||||
- Foreign keys voor relaties
|
||||
- Indexes voor performance
|
||||
|
||||
### **Veldnamen volgen FHIR**
|
||||
Bijvoorbeeld:
|
||||
- `name_family` → Patient.name.family
|
||||
- `code_code` → Condition.code.coding.code
|
||||
- `clinical_status` → Condition.clinicalStatus
|
||||
|
||||
Dit maakt het later makkelijk om FHIR JSON te genereren.
|
||||
|
||||
### **Later: FHIR API endpoints**
|
||||
```
|
||||
GET /fhir/Patient/{id}
|
||||
GET /fhir/Encounter?patient={id}
|
||||
GET /fhir/Condition?patient={id}
|
||||
GET /fhir/CarePlan?patient={id}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Wat betekent dit voor gebruikers?
|
||||
|
||||
### **Voor behandelaren:**
|
||||
- Alle data is logisch gestructureerd
|
||||
- Diagnoses zijn gekoppeld aan intake-moment
|
||||
- Behandelplan volgt automatisch uit diagnose
|
||||
- ROM-scores zijn zichtbaar in tijdlijn
|
||||
|
||||
### **Voor cliënten (in toekomst):**
|
||||
- Eigen dossier inzien via app
|
||||
- Behandelplan en afspraken zien
|
||||
- ROM-vragenlijsten invullen via app
|
||||
- Resultaten direct naar behandelaar
|
||||
|
||||
### **Voor beheerders:**
|
||||
- Export naar andere systemen is mogelijk
|
||||
- Backups bevatten FHIR-compliant data
|
||||
- Audits en rapportages zijn eenvoudig
|
||||
- Geen vendor lock-in
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
### **Fase 1: MVP (nu)**
|
||||
✅ Database schema met alle FHIR resources
|
||||
✅ Intake → Diagnose → Behandelplan workflow
|
||||
✅ Basis toegangscontrole
|
||||
|
||||
### **Fase 2: Basis functionaliteit**
|
||||
🔲 UI voor alle resources
|
||||
🔲 AI-assistentie voor intake
|
||||
🔲 ROM-metingen integratie
|
||||
|
||||
### **Fase 3: Integraties**
|
||||
🔲 FHIR API endpoints
|
||||
🔲 MedMIJ aansluiting (patiëntportaal)
|
||||
🔲 Koppeltaal aansluiting (eHealth apps)
|
||||
🔲 LSP medicatie-uitwisseling
|
||||
|
||||
---
|
||||
|
||||
## Referenties
|
||||
|
||||
- **FHIR Specificatie:** https://hl7.org/fhir/
|
||||
- **MedMIJ GGZ:** https://informatiestandaarden.nictiz.nl/wiki/MedMij:V2020.01/OntwerpGGZ
|
||||
- **Koppeltaal:** https://www.koppeltaal.nl/
|
||||
- **ZIBs (ZorgInformatieBouwstenen):** https://zibs.nl/
|
||||
- **DSM-5 Codes:** American Psychiatric Association
|
||||
- **MedicatieProces 9.0:** https://informatiestandaarden.nictiz.nl/wiki/mp:V9
|
||||
|
||||
---
|
||||
|
||||
**Laatst bijgewerkt:** 21 november 2024
|
||||
**Auteur:** Colin Lit (ikbenlit.nl)
|
||||
**Project:** AI Speedrun - Mini-ECD
|
||||
@@ -1,214 +0,0 @@
|
||||
# 🎨 Design Review: Dot Shader Background Component
|
||||
|
||||
**Datum:** 15-11-2024
|
||||
**Review Team:** Design & UX
|
||||
**Vraag:** Kan het dot-shader-background component gebruikt worden voor marketingpagina en/of EPD app?
|
||||
|
||||
---
|
||||
|
||||
## 📋 Context Analyse
|
||||
|
||||
### Manifesto Kernwaarden
|
||||
- **Innovatie & Disruptie:** "AI gaat software eten" - technologische vooruitgang centraal
|
||||
- **Snelheid:** "4 weken vs 12 maanden" - efficiency en moderniteit
|
||||
- **Bewijs door doen:** "Beste manier om toekomst te voorspellen is hem bouwen"
|
||||
- **Digitale transformatie:** Software on Demand als nieuwe realiteit
|
||||
|
||||
### UX Stylesheet Principes
|
||||
- **Lage cognitieve belasting:** Focus op content, niet op decoratie
|
||||
- **Toegankelijkheid:** WCAG AA contrast, geen afleidende elementen
|
||||
- **"Less is more":** Accentpalet bewust klein gehouden → minder visuele ruis
|
||||
- **Functioneel design:** Elke visuele keuze moet doel dienen
|
||||
|
||||
### Shader Component Eigenschappen
|
||||
- **Technisch:** Three.js shader met WebGL rendering
|
||||
- **Visueel:** Geanimeerde dots met mouse trail interactie
|
||||
- **Performance:** High-performance mode, GPU-accelerated
|
||||
- **Theme support:** Dark/light mode compatible
|
||||
- **Subtiel:** Lage opacity (0.025-0.15), niet dominant
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Design Team Advies
|
||||
|
||||
### ✅ **AANBEVELING: Strategische Inzet**
|
||||
|
||||
Het design team adviseert **gecontroleerd gebruik** van het shader component, met duidelijke context-specifieke richtlijnen:
|
||||
|
||||
---
|
||||
|
||||
## 📍 Marketing Website: **JA, met voorwaarden**
|
||||
|
||||
### Waarom het werkt:
|
||||
1. **Manifesto alignment:** Het component straalt technologische innovatie uit - perfect voor "Software on Demand" messaging
|
||||
2. **Differentiatie:** Visueel onderscheidend van traditionele SaaS-landing pages
|
||||
3. **Engagement:** Mouse trail interactie verhoogt tijd op pagina
|
||||
4. **Credibility:** Toont technische vaardigheid zonder te overdrijven
|
||||
|
||||
### Implementatie Richtlijnen:
|
||||
|
||||
**Hero Section (Primaire Inzet)**
|
||||
- ✅ Shader als full-screen achtergrond achter hero content
|
||||
- ✅ Zeer lage opacity (0.02-0.05) - subtiel, niet dominant
|
||||
- ✅ Content overlay met sterke contrast (wit op donker, of donker op licht)
|
||||
- ✅ Performance: Lazy load alleen wanneer hero in viewport
|
||||
|
||||
**Secties (Secundaire Inzet)**
|
||||
- ⚠️ Optioneel in "How it Works" of "Technology" secties
|
||||
- ❌ NIET in comparison tables, ROI calculator, of formulier secties
|
||||
- ❌ NIET op mobile (performance + UX overwegingen)
|
||||
|
||||
**Technische Aanpassingen Nodig:**
|
||||
```typescript
|
||||
// Marketing-specifieke configuratie
|
||||
const marketingConfig = {
|
||||
dotOpacity: 0.03, // Zeer subtiel
|
||||
gridSize: 80, // Fijner grid voor eleganter effect
|
||||
disableMouseTrail: false, // Interactiviteit behouden
|
||||
performanceMode: 'high', // GPU optimization
|
||||
mobileFallback: 'gradient' // Fallback voor mobile
|
||||
}
|
||||
```
|
||||
|
||||
**Contrast Check:**
|
||||
- Tekst op shader achtergrond moet voldoen aan WCAG AA (4.5:1)
|
||||
- Gebruik semi-transparante overlay indien nodig
|
||||
- Test met verschillende tekstgroottes
|
||||
|
||||
---
|
||||
|
||||
## 🏥 EPD App: **NEE, tenzij zeer subtiel**
|
||||
|
||||
### Waarom het risicovol is:
|
||||
1. **Cognitieve belasting:** Medische professionals hebben focus nodig - animaties zijn afleidend
|
||||
2. **UX Stylesheet conflict:** Direct tegenstrijdig met "lage cognitieve belasting" principe
|
||||
3. **Toegankelijkheid:** Kan problemen veroorzaken voor gebruikers met motion sensitivity
|
||||
4. **Performance:** Elke milliseconde telt in productie-omgevingen
|
||||
|
||||
### Uitzondering: Onboarding/Welcome Screen
|
||||
- ✅ **Alleen** op eerste login/welcome screen
|
||||
- ✅ Zeer korte duur (3-5 seconden), dan fade-out
|
||||
- ✅ Optioneel: "Skip animation" knop voor toegankelijkheid
|
||||
- ❌ **NOOIT** tijdens actieve workflows (intake, profiel, behandelplan)
|
||||
|
||||
**Implementatie Als Uitzondering:**
|
||||
```typescript
|
||||
// EPD-specifieke configuratie (alleen welcome)
|
||||
const epdWelcomeConfig = {
|
||||
dotOpacity: 0.01, // Extreem subtiel
|
||||
gridSize: 120, // Zeer fijn grid
|
||||
disableMouseTrail: true, // Geen interactiviteit
|
||||
autoFadeOut: true, // Fade na 3 seconden
|
||||
skipButton: true, // Toegankelijkheid
|
||||
reducedMotion: true // Respecteer prefers-reduced-motion
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Stylesheet Matching Analyse
|
||||
|
||||
### Kleuren Compatibiliteit
|
||||
|
||||
**Light Theme Match:**
|
||||
- ✅ Shader bg: `#F4F5F5` matcht stylesheet `#F8FAFC` (zeer dichtbij)
|
||||
- ✅ Shader dots: `#e1e1e1` matcht border kleur `#E2E8F0` (harmonisch)
|
||||
- ⚠️ Aanpassing nodig: Shader moet exact `#F8FAFC` gebruiken voor consistency
|
||||
|
||||
**Dark Theme Match:**
|
||||
- ✅ Shader bg: `#121212` is acceptabel voor dark mode
|
||||
- ✅ Shader dots: `#FFFFFF` met lage opacity werkt goed
|
||||
- ⚠️ Stylesheet heeft geen dark mode spec gedefinieerd - dit moet eerst worden uitgewerkt
|
||||
|
||||
**Aanbevolen Aanpassingen:**
|
||||
```typescript
|
||||
// Update shader theme colors om exact te matchen
|
||||
const getThemeColors = () => {
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return {
|
||||
dotColor: '#E2E8F0', // Match border color
|
||||
bgColor: '#F8FAFC', // Match app background
|
||||
dotOpacity: 0.03 // Zeer subtiel voor marketing
|
||||
}
|
||||
case 'dark':
|
||||
return {
|
||||
dotColor: '#475569', // Match secondary text
|
||||
bgColor: '#0F172A', // Match primary text (inverted)
|
||||
dotOpacity: 0.02 // Nog subtieler voor dark
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚖️ "Less is More" vs "Digital Innovation"
|
||||
|
||||
### Design Team Consensus:
|
||||
|
||||
**Marketing Website:**
|
||||
- **"Less is more"** geldt voor **content en copy** - niet voor visuele impact
|
||||
- Shader component kan **strategisch** gebruikt worden om innovatie te communiceren
|
||||
- **Voorwaarde:** Het moet de boodschap versterken, niet afleiden
|
||||
- **Test:** A/B test met en zonder shader - meet engagement metrics
|
||||
|
||||
**EPD App:**
|
||||
- **"Less is more"** is hier **absoluut** - elke pixel moet functioneel zijn
|
||||
- Shader component is **decoratief** en voegt geen functionele waarde toe
|
||||
- **Uitzondering:** Welcome screen kan één keer indruk maken, daarna weg
|
||||
|
||||
---
|
||||
|
||||
## 📊 Risico Analyse
|
||||
|
||||
| Risico | Kans | Impact | Mitigatie |
|
||||
|--------|------|--------|-----------|
|
||||
| **Performance impact** | Medium | Hoog | Lazy loading, mobile fallback, performance monitoring |
|
||||
| **Toegankelijkheid issues** | Medium | Hoog | `prefers-reduced-motion` respecteren, skip optie |
|
||||
| **Cognitieve overload** | Hoog | Medium | Zeer lage opacity, alleen hero section |
|
||||
| **Stylesheet mismatch** | Laag | Laag | Kleuren aanpassen naar exacte stylesheet waarden |
|
||||
| **Mobile performance** | Hoog | Medium | Automatische fallback naar gradient |
|
||||
|
||||
---
|
||||
|
||||
## ✅ Finale Aanbeveling
|
||||
|
||||
### Marketing Website: **GO** ✅
|
||||
- Implementeer in hero section met zeer lage opacity
|
||||
- Pas kleuren aan naar exacte stylesheet waarden
|
||||
- Voeg mobile fallback toe
|
||||
- Monitor performance metrics
|
||||
- Test toegankelijkheid met screen readers
|
||||
|
||||
### EPD App: **NO GO** ❌
|
||||
- **Behalve:** Welcome screen (één keer, met skip optie)
|
||||
- Focus op functioneel design volgens UX stylesheet
|
||||
- Gebruik subtiele gradients of solid colors voor achtergronden
|
||||
|
||||
### Implementatie Prioriteit:
|
||||
1. **Week 1:** Marketing hero section met shader (als MVP)
|
||||
2. **Week 2:** Performance optimalisatie + mobile fallback
|
||||
3. **Week 4:** A/B test resultaten evalueren
|
||||
4. **Post-launch:** Beslissing over permanente implementatie
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Design Principes Samenvatting
|
||||
|
||||
**Voor Marketing:**
|
||||
> "Innovatie moet zichtbaar zijn, maar niet opdringerig"
|
||||
|
||||
**Voor EPD:**
|
||||
> "Elke visuele keuze moet de gebruiker helpen, niet afleiden"
|
||||
|
||||
**Algemeen:**
|
||||
> "Technologie moet de boodschap dienen, niet domineren"
|
||||
|
||||
---
|
||||
|
||||
**Design Team Sign-off:**
|
||||
✅ Marketing website: Goedkeuring met voorwaarden
|
||||
❌ EPD app: Afwijzing (behalve welcome screen)
|
||||
📝 Stylesheet aanpassingen: Vereist voor consistency
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
# 🚀 Marketing Shader Implementation Guide
|
||||
|
||||
**Doel:** Praktische implementatie van dot-shader component voor marketing website hero section.
|
||||
|
||||
---
|
||||
|
||||
## 📐 Design Specificaties
|
||||
|
||||
### Hero Section Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [Shader Background - zeer subtiel] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ Hero Content Overlay │ │
|
||||
│ │ - Headline (wit/donker) │ │
|
||||
│ │ - Subheadline │ │
|
||||
│ │ - CTA Buttons │ │
|
||||
│ │ - Live Metrics Counter │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Visuele Hiërarchie
|
||||
1. **Shader:** Opacity 0.02-0.03 (bijna onzichtbaar, maar aanwezig)
|
||||
2. **Content Overlay:** Semi-transparant of solid (afhankelijk van contrast)
|
||||
3. **Tekst:** Hoog contrast (wit op donker, of donker op licht)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technische Implementatie
|
||||
|
||||
### Stap 1: Aangepaste Marketing Variant
|
||||
|
||||
```typescript
|
||||
// components/ui/marketing-shader-background.tsx
|
||||
'use client'
|
||||
|
||||
import { DotScreenShader } from './dot-shader-background'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface MarketingShaderProps {
|
||||
variant?: 'hero' | 'section'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MarketingShader({ variant = 'hero', className }: MarketingShaderProps) {
|
||||
const { theme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [reducedMotion, setReducedMotion] = 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)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}, [])
|
||||
|
||||
// Fallback voor mobile of reduced motion
|
||||
if (!mounted || reducedMotion) {
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-br from-slate-50 via-blue-50/30 to-slate-100 ${className}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Desktop: shader component
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
|
||||
<DotScreenShader />
|
||||
{/* Subtle overlay voor betere tekst leesbaarheid */}
|
||||
<div className="absolute inset-0 bg-white/40 dark:bg-slate-900/40 pointer-events-none" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Stap 2: Hero Section Component
|
||||
|
||||
```typescript
|
||||
// app/(marketing)/components/hero-section.tsx
|
||||
import { MarketingShader } from '@/components/ui/marketing-shader-background'
|
||||
import { LiveMetricsCounter } from './live-metrics-counter'
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
{/* Shader Background */}
|
||||
<MarketingShader variant="hero" className="z-0" />
|
||||
|
||||
{/* Content Overlay */}
|
||||
<div className="relative z-10 container mx-auto px-4 py-20">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
{/* Headline */}
|
||||
<h1 className="text-5xl md:text-7xl font-bold text-slate-900 dark:text-white mb-6">
|
||||
Software on Demand
|
||||
</h1>
|
||||
|
||||
{/* Subheadline */}
|
||||
<p className="text-xl md:text-2xl text-slate-700 dark:text-slate-300 mb-8">
|
||||
Van €100.000 en 12 maanden naar €200 en 4 weken
|
||||
</p>
|
||||
|
||||
{/* Live Metrics */}
|
||||
<LiveMetricsCounter />
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-12">
|
||||
<button className="px-8 py-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
|
||||
Bekijk Demo
|
||||
</button>
|
||||
<button className="px-8 py-4 bg-slate-200 text-slate-900 rounded-lg hover:bg-slate-300 transition">
|
||||
Lees Manifesto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Stap 3: Shader Component Aanpassingen
|
||||
|
||||
```typescript
|
||||
// Aanpassingen in dot-shader-background.tsx voor marketing gebruik
|
||||
|
||||
// In Scene component, update getThemeColors:
|
||||
const getThemeColors = () => {
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return {
|
||||
dotColor: '#E2E8F0', // Match UX stylesheet border color
|
||||
bgColor: '#F8FAFC', // Match UX stylesheet app background
|
||||
dotOpacity: 0.03 // Zeer subtiel voor marketing
|
||||
}
|
||||
case 'dark':
|
||||
return {
|
||||
dotColor: '#475569', // Match secondary text
|
||||
bgColor: '#0F172A', // Match primary text (inverted)
|
||||
dotOpacity: 0.02 // Nog subtieler
|
||||
}
|
||||
default:
|
||||
return {
|
||||
dotColor: '#E2E8F0',
|
||||
bgColor: '#F8FAFC',
|
||||
dotOpacity: 0.03
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update gridSize voor eleganter effect
|
||||
const gridSize = 80 // Fijner dan standaard 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Styling Integratie
|
||||
|
||||
### Tailwind Classes voor Content Overlay
|
||||
|
||||
```css
|
||||
/* Zorg voor goede contrast over shader */
|
||||
.hero-content {
|
||||
/* Optioneel: semi-transparante achtergrond voor tekst */
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
/* Of: solid achtergrond met padding */
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
### Contrast Check
|
||||
|
||||
**Test Scenario's:**
|
||||
1. Wit tekst op shader achtergrond → Minimaal 4.5:1 contrast
|
||||
2. Donker tekst op shader achtergrond → Minimaal 4.5:1 contrast
|
||||
3. Met overlay → Contrast moet verbeteren
|
||||
|
||||
**Tools:**
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- Browser DevTools → Accessibility panel
|
||||
|
||||
---
|
||||
|
||||
## 📱 Mobile Responsive
|
||||
|
||||
### Breakpoint Strategie
|
||||
|
||||
```typescript
|
||||
// components/ui/marketing-shader-background.tsx
|
||||
|
||||
export function MarketingShader({ variant, className }: MarketingShaderProps) {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768) // Tailwind md breakpoint
|
||||
}
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [])
|
||||
|
||||
// Mobile: gradient fallback (performance)
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-br from-slate-50 via-blue-50/20 to-slate-100 ${className}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Desktop: shader
|
||||
return <DotScreenShader />
|
||||
}
|
||||
```
|
||||
|
||||
**Reden:**
|
||||
- Mobile GPU performance beperkt
|
||||
- Batterij impact
|
||||
- UX: Gebruikers verwachten snelle laadtijden op mobile
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Performance Optimalisatie
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// Lazy load shader alleen wanneer hero in viewport
|
||||
const MarketingShader = dynamic(
|
||||
() => import('@/components/ui/marketing-shader-background').then(mod => mod.MarketingShader),
|
||||
{
|
||||
ssr: false, // Client-side only
|
||||
loading: () => <div className="absolute inset-0 bg-slate-50" />
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Intersection Observer
|
||||
|
||||
```typescript
|
||||
// components/ui/marketing-shader-background.tsx
|
||||
export function MarketingShader({ variant, className }: MarketingShaderProps) {
|
||||
const [shouldRender, setShouldRender] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setShouldRender(true)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
observer.observe(ref.current)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
if (!shouldRender) {
|
||||
return <div ref={ref} className={`absolute inset-0 bg-slate-50 ${className}`} />
|
||||
}
|
||||
|
||||
return <DotScreenShader />
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ♿ Toegankelijkheid
|
||||
|
||||
### Reduced Motion Support
|
||||
|
||||
```typescript
|
||||
// Automatisch detecteren en respecteren
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
// Geen animaties, statische gradient
|
||||
return <StaticGradientBackground />
|
||||
}
|
||||
```
|
||||
|
||||
### Skip Animation Knop
|
||||
|
||||
```typescript
|
||||
// Optioneel: knop om animatie uit te zetten
|
||||
<button
|
||||
onClick={() => setAnimationEnabled(false)}
|
||||
className="sr-only focus:not-sr-only"
|
||||
>
|
||||
Skip animatie
|
||||
</button>
|
||||
```
|
||||
|
||||
### ARIA Labels
|
||||
|
||||
```typescript
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
aria-hidden="true" // Decoratief element, niet voor screen readers
|
||||
role="presentation"
|
||||
>
|
||||
<DotScreenShader />
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring & Metrics
|
||||
|
||||
### Performance Tracking
|
||||
|
||||
```typescript
|
||||
// Track shader performance
|
||||
useEffect(() => {
|
||||
const startTime = performance.now()
|
||||
|
||||
// Na render
|
||||
requestAnimationFrame(() => {
|
||||
const renderTime = performance.now() - startTime
|
||||
if (renderTime > 100) {
|
||||
console.warn('Shader render tijd hoog:', renderTime)
|
||||
// Mogelijk fallback activeren
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
```
|
||||
|
||||
### A/B Test Setup
|
||||
|
||||
```typescript
|
||||
// Variant A: Met shader
|
||||
// Variant B: Zonder shader (gradient)
|
||||
|
||||
const useShader = () => {
|
||||
// Cookie/localStorage based variant assignment
|
||||
const variant = localStorage.getItem('hero-variant') || 'A'
|
||||
return variant === 'A'
|
||||
}
|
||||
```
|
||||
|
||||
**Metrics te meten:**
|
||||
- Time to Interactive (TTI)
|
||||
- First Contentful Paint (FCP)
|
||||
- Bounce rate
|
||||
- Engagement tijd
|
||||
- Conversion rate
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
### Pre-Launch
|
||||
- [ ] Shader opacity op 0.02-0.03 (zeer subtiel)
|
||||
- [ ] Kleuren matchen exact UX stylesheet
|
||||
- [ ] Mobile fallback geïmplementeerd
|
||||
- [ ] Reduced motion support
|
||||
- [ ] Contrast check gedaan (WCAG AA)
|
||||
- [ ] Performance test (< 100ms render tijd)
|
||||
- [ ] Lazy loading geïmplementeerd
|
||||
|
||||
### Post-Launch
|
||||
- [ ] Performance monitoring actief
|
||||
- [ ] A/B test resultaten analyseren
|
||||
- [ ] Gebruikersfeedback verzamelen
|
||||
- [ ] Accessibility audit uitgevoerd
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Conclusie
|
||||
|
||||
Het shader component kan **strategisch** gebruikt worden op de marketing website, mits:
|
||||
1. Zeer lage opacity (0.02-0.03)
|
||||
2. Alleen in hero section
|
||||
3. Mobile fallback aanwezig
|
||||
4. Toegankelijkheid gewaarborgd
|
||||
5. Performance geoptimaliseerd
|
||||
|
||||
**Resultaat:** Innovatieve uitstraling zonder UX compromissen.
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { Timeline } from "@/components/ui/timeline";
|
||||
|
||||
export function TimelineDemo() {
|
||||
const data = [
|
||||
{
|
||||
title: "2024",
|
||||
content: (
|
||||
<div>
|
||||
<p className="text-neutral-800 dark:text-neutral-200 text-xs md:text-sm font-normal mb-8">
|
||||
Built and launched Aceternity UI and Aceternity UI Pro from scratch
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Image
|
||||
src="https://assets.aceternity.com/templates/startup-1.webp"
|
||||
alt="startup template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/templates/startup-2.webp"
|
||||
alt="startup template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/templates/startup-3.webp"
|
||||
alt="startup template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/templates/startup-4.webp"
|
||||
alt="startup template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Early 2023",
|
||||
content: (
|
||||
<div>
|
||||
<p className="text-neutral-800 dark:text-neutral-200 text-xs md:text-sm font-normal mb-8">
|
||||
I usually run out of copy, but when I see content this big, I try to
|
||||
integrate lorem ipsum.
|
||||
</p>
|
||||
<p className="text-neutral-800 dark:text-neutral-200 text-xs md:text-sm font-normal mb-8">
|
||||
Lorem ipsum is for people who are too lazy to write copy. But we are
|
||||
not. Here are some more example of beautiful designs I built.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Image
|
||||
src="https://assets.aceternity.com/pro/hero-sections.png"
|
||||
alt="hero template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/features-section.png"
|
||||
alt="feature template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/pro/bento-grids.png"
|
||||
alt="bento template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/cards.png"
|
||||
alt="cards template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Changelog",
|
||||
content: (
|
||||
<div>
|
||||
<p className="text-neutral-800 dark:text-neutral-200 text-xs md:text-sm font-normal mb-4">
|
||||
Deployed 5 new components on Aceternity today
|
||||
</p>
|
||||
<div className="mb-8">
|
||||
<div className="flex gap-2 items-center text-neutral-700 dark:text-neutral-300 text-xs md:text-sm">
|
||||
✅ Card grid component
|
||||
</div>
|
||||
<div className="flex gap-2 items-center text-neutral-700 dark:text-neutral-300 text-xs md:text-sm">
|
||||
✅ Startup template Aceternity
|
||||
</div>
|
||||
<div className="flex gap-2 items-center text-neutral-700 dark:text-neutral-300 text-xs md:text-sm">
|
||||
✅ Random file upload lol
|
||||
</div>
|
||||
<div className="flex gap-2 items-center text-neutral-700 dark:text-neutral-300 text-xs md:text-sm">
|
||||
✅ Himesh Reshammiya Music CD
|
||||
</div>
|
||||
<div className="flex gap-2 items-center text-neutral-700 dark:text-neutral-300 text-xs md:text-sm">
|
||||
✅ Salman Bhai Fan Club registrations open
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Image
|
||||
src="https://assets.aceternity.com/pro/hero-sections.png"
|
||||
alt="hero template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/features-section.png"
|
||||
alt="feature template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/pro/bento-grids.png"
|
||||
alt="bento template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
<Image
|
||||
src="https://assets.aceternity.com/cards.png"
|
||||
alt="cards template"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg object-cover h-20 md:h-44 lg:h-60 w-full shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="min-h-screen w-full">
|
||||
<div className="absolute top-0 left-0 w-full">
|
||||
<Timeline data={data} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
# WCAG AA Compliance Report
|
||||
|
||||
**Date:** 2024-11-17
|
||||
**Project:** Mini-EPD Prototype
|
||||
**Design System:** Teal-first (v2.1)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the WCAG AA compliance status for the teal-first design system colors. All tested combinations meet or exceed WCAG AA standards for their intended use cases.
|
||||
|
||||
## WCAG AA Requirements
|
||||
|
||||
- **Normal text** (< 18pt or < 14pt bold): **4.5:1 minimum**
|
||||
- **Large text** (>= 18pt or >= 14pt bold): **3:1 minimum**
|
||||
- **UI components** (borders, focus indicators, icons): **3:1 minimum**
|
||||
|
||||
---
|
||||
|
||||
## Test Results Summary
|
||||
|
||||
| Category | Pass Rate | Status |
|
||||
|----------|-----------|--------|
|
||||
| Normal text (4.5:1) | 7/11 (64%) | ✅ PASS |
|
||||
| Large text (3:1) | 11/11 (100%) | ✅ PASS |
|
||||
| UI components (3:1) | 11/11 (100%) | ✅ PASS |
|
||||
|
||||
---
|
||||
|
||||
## Color Definitions
|
||||
|
||||
### Teal (Brand)
|
||||
|
||||
| Shade | Hex | Primary Use | Contrast on White |
|
||||
|-------|-----|-------------|-------------------|
|
||||
| teal-600 | `#0D9488` | UI components, buttons (bg) | 3.74:1 ⚠️ Large text only |
|
||||
| teal-700 | `#0F766E` | **PRIMARY** - Text, links | **5.47:1** ✅ AA Normal |
|
||||
| teal-800 | `#115E59` | Hover states | Higher contrast |
|
||||
|
||||
**Usage Guidelines:**
|
||||
- ✅ **Use teal-700** for body text, headings, links on white/light backgrounds
|
||||
- ⚠️ **Use teal-600** only for UI components (borders, icons) or large text (>= 18pt)
|
||||
- ✅ **White text on teal-700** passes AA Normal (5.47:1)
|
||||
|
||||
### Amber (AI Features)
|
||||
|
||||
| Shade | Hex | Primary Use | Contrast on White |
|
||||
|-------|-----|-------------|-------------------|
|
||||
| amber-600 | `#D97706` | AI buttons, large text | 3.19:1 ⚠️ Large text only |
|
||||
| amber-700 | `#B45309` | AI button hover, text | **5.02:1** ✅ AA Normal |
|
||||
|
||||
**Usage Guidelines:**
|
||||
- ✅ **Use amber-700** for text on light backgrounds
|
||||
- ⚠️ **Use amber-600** for buttons with **large text** (>= 18pt) or as gradient start
|
||||
- ✅ **AIButton component** uses amber-600→amber-700 gradient (meets AA for buttons)
|
||||
|
||||
---
|
||||
|
||||
## Detailed Test Results
|
||||
|
||||
### ✅ PASSING (AA Normal Text - 4.5:1)
|
||||
|
||||
| Foreground | Background | Ratio | Use Case |
|
||||
|------------|------------|-------|----------|
|
||||
| teal-700 | white | **5.47:1** | Primary text, links |
|
||||
| white | teal-700 | **5.47:1** | Buttons, badges |
|
||||
| teal-700 | slate-50 | **5.23:1** | Text on gray surfaces |
|
||||
| teal-700 | teal-50 | **5.25:1** | Text on teal subtle bg |
|
||||
| white | amber-700 | **5.02:1** | AI button hover |
|
||||
| amber-700 | amber-50 | **4.84:1** | AI subtle text |
|
||||
| teal-700 | white | **5.47:1** | Focus rings |
|
||||
|
||||
### ⚠️ PASSING (AA Large Text - 3:1)
|
||||
|
||||
| Foreground | Background | Ratio | Use Case |
|
||||
|------------|------------|-------|----------|
|
||||
| teal-600 | white | 3.74:1 | UI components, large text |
|
||||
| white | teal-600 | 3.74:1 | Buttons (large text) |
|
||||
| white | amber-600 | 3.19:1 | AI buttons (large text) |
|
||||
| amber-600 | white | 3.19:1 | UI components |
|
||||
|
||||
---
|
||||
|
||||
## Component-Specific Guidelines
|
||||
|
||||
### Buttons
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT: Teal-700 background
|
||||
<button className="bg-teal-700 text-white">
|
||||
Primary Action
|
||||
</button>
|
||||
|
||||
// ⚠️ CAUTION: Teal-600 requires large text
|
||||
<button className="bg-teal-600 text-white text-lg">
|
||||
Large Button
|
||||
</button>
|
||||
|
||||
// ✅ CORRECT: AIButton uses amber-600→amber-700 gradient
|
||||
<AIButton>Generate Summary</AIButton>
|
||||
```
|
||||
|
||||
### Text Links
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT: Teal-700 for links
|
||||
<a className="text-teal-700 hover:text-teal-800">
|
||||
Read more
|
||||
</a>
|
||||
|
||||
// ❌ INCORRECT: Teal-600 fails for normal text
|
||||
<a className="text-teal-600">Fails AA</a>
|
||||
```
|
||||
|
||||
### Focus States
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT: Teal-700 focus ring
|
||||
<input className="focus:ring-2 focus:ring-teal-700" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSS Variables
|
||||
|
||||
The following CSS variables have been updated for WCAG AA compliance:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Brand & Primary (Teal-first Design System) */
|
||||
--color-brand: #0F766E; /* teal-700 - PRIMARY (5.47:1 on white - WCAG AA) */
|
||||
--color-brand-hover: #115E59; /* teal-800 */
|
||||
--color-brand-active: #0D9488; /* teal-600 */
|
||||
|
||||
/* AI Features (Amber) */
|
||||
--color-ai: #D97706; /* amber-600 - PRIMARY AI (3.19:1 on white - WCAG AA large) */
|
||||
--color-ai-hover: #B45309; /* amber-700 */
|
||||
|
||||
/* Info color (uses brand) */
|
||||
--color-info: #0F766E; /* teal-700 - Brand consistency (WCAG AA) */
|
||||
|
||||
/* Input focus states */
|
||||
--color-input-focus: #0F766E; /* teal-700 - Focus states (WCAG AA) */
|
||||
--color-input-focus-border: #115E59; /* teal-800 */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### ✅ Current State
|
||||
- All primary text uses teal-700 (5.47:1 contrast) ✅
|
||||
- All focus rings use teal-700 (5.47:1 contrast) ✅
|
||||
- AI buttons use amber-600→amber-700 gradient ✅
|
||||
- All UI components meet 3:1 minimum ✅
|
||||
|
||||
### 📋 Future Enhancements
|
||||
- Consider using teal-800 for even higher contrast in critical areas
|
||||
- Monitor user feedback on amber button readability
|
||||
- Test with color blindness simulators
|
||||
- Add automated contrast testing to CI/CD pipeline
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Run contrast tests:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/test-contrast.ts
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- [UX Implementation Plan v2.0](../specs/ux-implementation-plan-v2.md)
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ WCAG AA Compliant
|
||||
**Last Updated:** 2024-11-17
|
||||
**Next Review:** Before production launch
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user