From 43c7cb898b56c0e49269cd0c5493be60f703dda5 Mon Sep 17 00:00:00 2001 From: colinislit Date: Tue, 2 Dec 2025 16:44:31 +0100 Subject: [PATCH] feat(agenda): Epic 0 - Database schema for agenda module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - E0.S1: Add encounter_id and intake_id columns to reports table - E0.S2: Add performance indices for reports and encounters - E0.S3: Regenerate TypeScript types πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../agenda/bouwplan-agenda-module-v1.0.md | 506 +++++ docs/specs/agenda/fo-agenda-module-v1_0.md | 1233 ++++++++++++ docs/specs/agenda/prd-agenda-module-v1_0.md | 695 +++++++ docs/specs/agenda/to-agenda-module-v1_0.md | 1778 +++++++++++++++++ lib/supabase/database.types.ts | 114 +- supabase/.temp/cli-latest | 2 +- .../20251202_add_agenda_indices.sql | 28 + ...251202_add_encounter_intake_to_reports.sql | 52 + 8 files changed, 4371 insertions(+), 37 deletions(-) create mode 100644 docs/specs/agenda/bouwplan-agenda-module-v1.0.md create mode 100644 docs/specs/agenda/fo-agenda-module-v1_0.md create mode 100644 docs/specs/agenda/prd-agenda-module-v1_0.md create mode 100644 docs/specs/agenda/to-agenda-module-v1_0.md create mode 100644 supabase/migrations/20251202_add_agenda_indices.sql create mode 100644 supabase/migrations/20251202_add_encounter_intake_to_reports.sql diff --git a/docs/specs/agenda/bouwplan-agenda-module-v1.0.md b/docs/specs/agenda/bouwplan-agenda-module-v1.0.md new file mode 100644 index 0000000..354c0eb --- /dev/null +++ b/docs/specs/agenda/bouwplan-agenda-module-v1.0.md @@ -0,0 +1,506 @@ +# Bouwplan β€” Agenda Module + +**Projectnaam:** Mini-EPD Agenda Module +**Versie:** v1.0 +**Datum:** 02-12-2024 +**Auteur:** Colin + +--- + +## 1. Doel en context + +**Doel:** Een volledige agendafunctionaliteit bouwen voor het Mini-EPD systeem waarmee behandelaars afspraken kunnen plannen, beheren en koppelen aan EPD-documenten. + +**Toelichting:** De agenda is een kernfunctionaliteit binnen elk EPD-systeem. Momenteel bestaat er alleen een placeholder pagina op `/epd/agenda`. Deze module bouwt voort op de bestaande `encounters` tabel (FHIR-compliant) en breidt deze uit met volledige kalenderfunctionaliteit en bidirectionele koppelingen met rapportages. + +**Kernfunctionaliteiten:** +- Kalenderweergaven (dag/week/werkdagen) +- Afspraakbeheer (maken, verzetten, annuleren) +- PatiΓ«nt-selectie bij afspraken +- Koppeling afspraak ↔ verslag (bidirectioneel) + +--- + +## 2. Uitgangspunten + +### 2.1 Technische Stack + +| Component | Technologie | +|-----------|-------------| +| **Frontend** | Next.js 14 (App Router) + React 18 + TypeScript | +| **UI Components** | shadcn/ui (Radix primitives) + Tailwind CSS | +| **Calendar Library** | FullCalendar (@fullcalendar/react) | +| **Datepicker** | shadcn/ui Calendar (bestaand) | +| **Database** | Supabase (PostgreSQL) | +| **Date Handling** | date-fns (al in project) | +| **State Management** | React Server Components + Server Actions | + +### 2.2 Projectkaders + +| Aspect | Waarde | +|--------|--------| +| **Scope** | MVP met uitbreidingsmogelijkheden | +| **Data** | Bestaande `encounters` tabel + uitbreiding `reports` | +| **Integratie** | Naadloos met bestaande patient/intake flows | +| **Gebruikers** | Behandelaars (practitioners) | + +### 2.3 Programmeer Uitgangspunten + +**Bestaande patronen volgen:** +- Server Actions voor mutaties (`actions.ts` per route) +- API routes voor complexe queries +- FHIR-compliant datastructuren +- shadcn/ui component styling + +**Code Quality:** +- TypeScript strict mode +- Zod validatie voor alle inputs +- Error boundaries voor UI failures +- Optimistic updates waar mogelijk + +--- + +## 3. Epics & Stories Overzicht + +| Epic ID | Titel | Doel | Status | Stories | +|---------|-------|------|--------|---------| +| E0 | Database & Types | Schema uitbreiden, types genereren | ⏳ To Do | 3 | +| E1 | Calendar Views | Dag/week/werkdagen weergaven | ⏳ To Do | 4 | +| E2 | Afspraak CRUD | Maken, bewerken, annuleren | ⏳ To Do | 5 | +| E3 | PatiΓ«nt Integratie | Selectie, zoeken, quick-create | ⏳ To Do | 3 | +| E4 | EPD Koppeling | Verslag ↔ Afspraak bidirectioneel | ⏳ To Do | 4 | +| E5 | Polish & Testing | UX verfijning, edge cases | ⏳ To Do | 3 | + +--- + +## 4. Epics & Stories (Uitwerking) + +### Epic 0 β€” Database & Types + +**Epic Doel:** Database schema uitbreiden voor report-encounter koppeling en TypeScript types updaten. + +| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | +|----------|--------------|---------------------|--------|------------------|----| +| E0.S1 | Migration: encounter_id toevoegen aan reports | `encounter_id` en `intake_id` kolommen bestaan, foreign keys werken | ⏳ | β€” | 2 | +| E0.S2 | Index toevoegen voor performance | `idx_reports_encounter`, `idx_encounters_period` indices bestaan | ⏳ | E0.S1 | 1 | +| E0.S3 | TypeScript types regenereren | `database.types.ts` bevat nieuwe kolommen | ⏳ | E0.S2 | 1 | + +**Technical Notes:** +```sql +-- Migration E0.S1 +ALTER TABLE reports +ADD COLUMN encounter_id UUID REFERENCES encounters(id) ON DELETE SET NULL, +ADD COLUMN intake_id UUID REFERENCES intakes(id) ON DELETE SET NULL; + +-- Migration E0.S2 +CREATE INDEX idx_reports_encounter ON reports(encounter_id); +CREATE INDEX idx_reports_intake ON reports(intake_id); +CREATE INDEX idx_encounters_period ON encounters(period_start, period_end); +CREATE INDEX idx_encounters_practitioner ON encounters(practitioner_id); +``` + +--- + +### Epic 1 β€” Calendar Views + +**Epic Doel:** Interactieve kalenderweergaven met dag, week en werkdagen views. + +| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | +|----------|--------------|---------------------|--------|------------------|----| +| E1.S1 | FullCalendar installatie & setup | Library geΓ―nstalleerd, basis component rendert | ⏳ | E0.S3 | 2 | +| E1.S2 | Dag view implementeren | Uurblokken 08:00-18:00, afspraken zichtbaar | ⏳ | E1.S1 | 3 | +| E1.S3 | Week view implementeren | 7-dagen grid, drag-resize werkt | ⏳ | E1.S2 | 3 | +| E1.S4 | Werkdagen view (ma-vr) | Filter voor weekend, business hours highlight | ⏳ | E1.S3 | 2 | + +**Technical Notes:** +```bash +pnpm add @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction +``` + +**Component Structuur:** +``` +app/epd/agenda/ +β”œβ”€β”€ page.tsx # Server component, data fetching +β”œβ”€β”€ components/ +β”‚ β”œβ”€β”€ agenda-calendar.tsx # FullCalendar wrapper (client) +β”‚ β”œβ”€β”€ agenda-toolbar.tsx # View switcher, date nav +β”‚ β”œβ”€β”€ agenda-sidebar.tsx # Mini calendar + filters +β”‚ └── appointment-card.tsx # Event rendering +β”œβ”€β”€ actions.ts # Server actions +└── types.ts # Agenda-specific types +``` + +**Styling:** +- FullCalendar CSS overschrijven met Tailwind +- Consistent met shadcn/ui design tokens +- Dark mode support (later) + +--- + +### Epic 2 β€” Afspraak CRUD + +**Epic Doel:** Volledige afspraakbeheer functionaliteit. + +| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | +|----------|--------------|---------------------|--------|------------------|----| +| E2.S1 | Afspraak aanmaken modal | Form met alle velden, validatie, opslaan werkt | ⏳ | E1.S2 | 5 | +| E2.S2 | Afspraak bewerken | Click op event β†’ edit modal, wijzigingen opslaan | ⏳ | E2.S1 | 3 | +| E2.S3 | Afspraak verzetten (drag-drop) | Drag event naar nieuwe tijd, confirm dialog | ⏳ | E2.S2 | 3 | +| E2.S4 | Afspraak annuleren | Soft delete (status=cancelled), confirm dialog | ⏳ | E2.S2 | 2 | +| E2.S5 | Afspraak details view | Click voor volledige info, quick actions | ⏳ | E2.S2 | 2 | + +**Appointment Modal Fields:** + +| Veld | Type | Verplicht | Bron | +|------|------|-----------|------| +| PatiΓ«nt | Patient selector | Ja | patients tabel | +| Datum | Date picker | Ja | β€” | +| Starttijd | Time picker | Ja | β€” | +| Eindtijd | Time picker | Nee | β€” | +| Type | Select | Ja | encounter types | +| Locatie | Select | Nee | class_code | +| Behandelaar | Select | Ja | practitioners | +| Notities | Textarea | Nee | β€” | + +**Appointment Types (type_code):** +- `intake` - Intakegesprek +- `behandeling` - Behandelsessie +- `follow-up` - Vervolggesprek +- `telefonisch` - Telefonisch contact +- `huisbezoek` - Huisbezoek +- `online` - Online consult +- `crisis` - Crisiscontact +- `overig` - Overig + +**Status Flow:** +``` +planned β†’ in-progress β†’ completed + ↓ +cancelled +``` + +--- + +### Epic 3 β€” PatiΓ«nt Integratie + +**Epic Doel:** Naadloze patiΓ«ntselectie bij het maken van afspraken. + +| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | +|----------|--------------|---------------------|--------|------------------|----| +| E3.S1 | Patient search component | Zoeken op naam, BSN, clientnummer | ⏳ | E2.S1 | 3 | +| E3.S2 | Recent patients dropdown | Laatste 5 patiΓ«nten snel selecteren | ⏳ | E3.S1 | 2 | +| E3.S3 | Quick patient info | Naam, geboortedatum, actieve intake tonen | ⏳ | E3.S2 | 2 | + +**Patient Selector Component:** +```typescript +interface PatientSelectorProps { + value?: string; // patient_id + onChange: (patientId: string) => void; + showRecentPatients?: boolean; + allowCreate?: boolean; // Future: quick create +} +``` + +--- + +### Epic 4 β€” EPD Koppeling + +**Epic Doel:** Bidirectionele koppeling tussen afspraken en verslagen. + +| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | +|----------|--------------|---------------------|--------|------------------|----| +| E4.S1 | Verslag maken vanuit afspraak | Button op appointment β†’ report composer met encounter_id | ⏳ | E2.S5 | 3 | +| E4.S2 | Afspraak koppelen vanuit verslag | In rapportage: link naar bestaande/nieuwe afspraak | ⏳ | E4.S1 | 3 | +| E4.S3 | Gekoppelde items tonen | In afspraak details: linked reports zichtbaar | ⏳ | E4.S2 | 2 | +| E4.S4 | Navigatie tussen afspraak ↔ verslag | Click-through links beide kanten | ⏳ | E4.S3 | 2 | + +**Integration Points:** + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Agenda β”‚ β”‚ Rapportage β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ [Afspraak] │───────────────▢│ [Verslag] β”‚ +β”‚ └─ + Verslag β”‚ β”‚ └─ encounter β”‚ +β”‚ │◀───────────────│ └─ + Koppel β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ encounters β”‚ +β”‚ id, patient_id, period_start, period_end, ... β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ reports β”‚ +β”‚ id, patient_id, encounter_id, intake_id, ... β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Report Composer Updates:** +- Nieuwe prop: `encounterId?: string` +- Pre-fill patient_id vanuit encounter +- Toon encounter info (datum, type) in composer + +--- + +### Epic 5 β€” Polish & Testing + +**Epic Doel:** UX verfijning en edge case handling. + +| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | +|----------|--------------|---------------------|--------|------------------|----| +| E5.S1 | Loading states & skeletons | Alle async operaties hebben loading feedback | ⏳ | E4.S4 | 2 | +| E5.S2 | Error handling | User-friendly errors, retry mogelijkheden | ⏳ | E5.S1 | 2 | +| E5.S3 | Responsive design | Mobile-friendly, touch gestures | ⏳ | E5.S2 | 3 | + +--- + +## 5. Kwaliteit & Testplan + +### Test Types + +| Test Type | Scope | Tools | +|-----------|-------|-------| +| Unit Tests | Date utilities, validators | Vitest | +| Integration | API endpoints, database | Playwright | +| Smoke Tests | Kritieke flows | Manual | +| Visual | UI consistency | Manual | + +### Manual Test Checklist + +**Afspraak Flow:** +- [ ] Nieuwe afspraak aanmaken met alle velden +- [ ] Afspraak bewerken (tijd, type, notities) +- [ ] Afspraak verzetten via drag-drop +- [ ] Afspraak annuleren met bevestiging +- [ ] PatiΓ«nt zoeken en selecteren + +**Calendar Views:** +- [ ] Dag view toont correcte uren +- [ ] Week view toont 7 dagen +- [ ] Werkdagen view filtert weekend +- [ ] Navigatie (vorige/volgende) werkt +- [ ] Vandaag button springt naar huidige dag + +**EPD Koppeling:** +- [ ] Verslag maken vanuit afspraak +- [ ] Afspraak koppelen vanuit verslag +- [ ] Gekoppelde items zijn zichtbaar +- [ ] Navigatie werkt beide kanten + +--- + +## 6. Data Model + +### Encounters (uitgebreid) + +```typescript +interface Encounter { + id: string; + identifier: string; + status: 'planned' | 'in-progress' | 'on-hold' | 'completed' | 'cancelled'; + class_code: string; // AMB, VR, HH (home health) + class_display: string; + type_code: string; // intake, behandeling, follow-up, etc. + type_display: string; + patient_id: string; + practitioner_id: string; + organization_id?: string; + period_start: string; // ISO datetime + period_end?: string; + notes?: string; + intake_id?: string; + created_at: string; + updated_at: string; +} +``` + +### Reports (uitgebreid) + +```typescript +interface Report { + id: string; + patient_id: string; + created_by?: string; + type: 'behandeladvies' | 'vrije_notitie' | 'intake' | 'voortgang' | 'crisis' | 'contact'; + content: string; + encounter_id?: string; // NEW: koppeling met afspraak + intake_id?: string; // NEW: koppeling met intake + // ... existing fields +} +``` + +### Calendar Event (voor FullCalendar) + +```typescript +interface CalendarEvent { + id: string; + title: string; // Patient naam + start: Date; + end?: Date; + extendedProps: { + encounter: Encounter; + patient: Patient; + linkedReports: Report[]; + }; + backgroundColor?: string; // Based on type + borderColor?: string; +} +``` + +--- + +## 7. API Endpoints + +### Nieuwe Endpoints + +| Methode | Endpoint | Beschrijving | +|---------|----------|--------------| +| GET | `/api/agenda/encounters` | Haal afspraken op (met filters) | +| POST | `/api/agenda/encounters` | Nieuwe afspraak aanmaken | +| PUT | `/api/agenda/encounters/[id]` | Afspraak bewerken | +| PATCH | `/api/agenda/encounters/[id]/cancel` | Afspraak annuleren | +| GET | `/api/agenda/encounters/[id]/reports` | Gekoppelde verslagen | + +### Query Parameters (GET encounters) + +```typescript +interface EncounterFilters { + start_date: string; // ISO date + end_date: string; + practitioner_id?: string; + patient_id?: string; + status?: string[]; + type_code?: string[]; +} +``` + +--- + +## 8. Risico's & Mitigatie + +| Risico | Kans | Impact | Mitigatie | +|--------|------|--------|-----------| +| FullCalendar styling conflicten | Middel | Middel | CSS isolation, custom theme | +| Performance bij veel afspraken | Laag | Hoog | Pagination, date range filtering | +| Timezone issues | Middel | Hoog | Altijd UTC opslaan, lokaal tonen | +| Drag-drop UX op mobile | Middel | Middel | Touch-friendly alternatives | +| Concurrent edits | Laag | Middel | Optimistic locking, conflict resolution | + +--- + +## 9. UI/UX Specificaties + +### Agenda Layout + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ EPD Header [User] β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ Agenda [+ Nieuwe Afspraak] β”‚ +β”‚ Nav │───────────────────────────────────────────────────────│ +β”‚ β”‚ [< Vorige] [Vandaag] [Volgende >] [Dag|Week|Werk] β”‚ +β”‚ │───────────────────────────────────────────────────────│ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ December β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ β”‚ 2024 β”‚ β”‚ Ma β”‚ Di β”‚ Wo β”‚ Do β”‚ Vr β”‚β”‚ +β”‚ β”‚ β”‚ [Cal] β”‚ │──────┼──────┼──────┼──────┼────────││ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ 08:00β”‚ β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ │──────│ [Pnt]β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ Filters: β”‚ 09:00β”‚ β”‚ [Int]β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β–‘ Intakes │──────│ β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β–‘ Behandeling β”‚ 10:00β”‚ β”‚ β”‚ [Beh]β”‚ β”‚β”‚ +β”‚ β”‚ β–‘ Telefonisch │──────│ β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β”‚ ... β”‚ β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Afspraak Card (in kalender) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 09:00 - 10:00 β”‚ +β”‚ ● Jan de Vries β”‚ +β”‚ Intakegesprek β”‚ +β”‚ [πŸ“] [✏️] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Afspraak Modal + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Nieuwe Afspraak [X] β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ PatiΓ«nt * β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ” Zoek patiΓ«nt... β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Datum * Tijd * β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” - β”Œβ”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ 02-12-2024 β”‚ β”‚09:00 β”‚ β”‚10:00 β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜β”‚ +β”‚ β”‚ +β”‚ Type afspraak * β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Intakegesprek β–Ό β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Locatie β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Praktijk β–Ό β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Notities β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ [Annuleren] [Opslaan] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 10. Referenties + +**Interne Documenten:** +- FO Mini-EPD v1.2 +- TO Mini-EPD v1.2 +- Screening-Intake Bouwplan + +**Libraries:** +- [FullCalendar React](https://fullcalendar.io/docs/react) +- [shadcn/ui Calendar](https://ui.shadcn.com/docs/components/calendar) +- [date-fns](https://date-fns.org/) + +**Database:** +- `encounters` tabel (FHIR Encounter) +- `reports` tabel +- `patients` tabel +- `practitioners` tabel + +--- + +## 11. Glossary + +| Term | Betekenis | +|------|-----------| +| Encounter | FHIR term voor een contact/afspraak moment | +| period_start | Starttijd van een afspraak (ISO datetime) | +| class_code | Type locatie (AMB=ambulant, VR=virtueel, HH=thuis) | +| type_code | Type afspraak (intake, behandeling, etc.) | +| Bidirectioneel | Koppeling werkt beide kanten op | + +--- + +**Versiehistorie:** + +| Versie | Datum | Auteur | Wijziging | +|--------|-------|--------|-----------| +| v1.0 | 02-12-2024 | Colin | InitiΓ«le versie | diff --git a/docs/specs/agenda/fo-agenda-module-v1_0.md b/docs/specs/agenda/fo-agenda-module-v1_0.md new file mode 100644 index 0000000..b530c9a --- /dev/null +++ b/docs/specs/agenda/fo-agenda-module-v1_0.md @@ -0,0 +1,1233 @@ +# 🧩 Functioneel Ontwerp (FO) – Agenda Module + +**Projectnaam:** Mini-EPD Agenda Module +**Versie:** v1.1 +**Datum:** 02-12-2024 +**Auteur:** Colin + +--- + +## 1. Doel en relatie met het PRD + +🎯 **Doel van dit document:** +Dit Functioneel Ontwerp beschrijft **hoe** de Agenda Module uit het PRD functioneel zal werken β€” wat de behandelaar ziet, doet en ervaart. Waar het PRD uitlegt *wat en waarom*, laat dit FO zien *hoe dit in de praktijk werkt*. + +πŸ“˜ **Relatie met PRD:** +- PRD-referentie: `prd-agenda-module-v1_0.md` +- Dit FO is de functionele uitwerking van PRD secties 3 (Kernfunctionaliteiten) en 4 (Gebruikersflows) +- Scope is afgebakend op MVP-features; nice-to-have items zijn apart gemarkeerd + +**Kernprincipe:** +> De agenda is het kloppend hart van het EPD. Zonder agenda geen dagelijkse workflow, zonder workflow geen AI-integratie in de praktijk. + +--- + +## 2. Overzicht van de belangrijkste onderdelen + +🎯 **Doel:** Kort overzicht van schermen en componenten binnen de Agenda Module. + +### MVP Onderdelen (Must-Have) + +| # | Onderdeel | Beschrijving | Prioriteit | +|---|-----------|--------------|------------| +| 1 | **Kalender View** | Hoofd agenda-weergave met dag/week/werkdagen toggle | πŸ”΄ Kritiek | +| 2 | **Afspraak Modal** | Formulier voor maken/bewerken afspraken | πŸ”΄ Kritiek | +| 3 | **Patient Zoeken** | Autocomplete voor patient selectie | πŸ”΄ Kritiek | +| 4 | **Conflict Dialog** | Waarschuwing bij dubbele boekingen | πŸ”΄ Kritiek | +| 5 | **Afspraak Detail Popup** | Quick-view met acties (verslag maken, bewerken) | πŸ”΄ Kritiek | +| 6 | **Toolbar** | Datum navigatie, view switcher | 🟑 Hoog | + +### Stretch/Nice-to-Have Onderdelen + +| # | Onderdeel | Beschrijving | Prioriteit | +|---|-----------|--------------|------------| +| 7 | Mini Kalender Sidebar | Maandoverzicht met navigatie | 🟒 Stretch | +| 8 | Recente PatiΓ«nten | Quick-select laatste 5 patiΓ«nten | 🟒 Stretch | +| 9 | Kleurcodering | Visuele type-indicator (intake/behandeling/follow-up) | 🟒 Stretch | + +### Niet in Scope (Post-MVP) + +| Onderdeel | Reden exclusie | +|-----------|----------------| +| Multi-practitioner view | Te complex voor MVP, single-user focus | +| Recurring appointments | Beperkte demo-waarde, hoge complexiteit | +| Email/SMS notificaties | Aparte infrastructuur nodig | +| Wachtlijstbeheer | Buiten scope AI Speedrun | +| Video call integratie | Geen core EPD functionaliteit | + +--- + +## 3. User Stories + +🎯 **Doel:** Beschrijven wat gebruikers moeten kunnen doen, vanuit hun perspectief. + +### MVP User Stories + +| ID | Als... | Wil ik... | Zodat... | Prio | +|----|--------|-----------|----------|------| +| **AG-US01** | Behandelaar | Mijn agenda bekijken in dag/week view | Ik weet welke patiΓ«nten vandaag/deze week komen | πŸ”΄ | +| **AG-US02** | Behandelaar | Snel een nieuwe afspraak maken door op een tijdslot te klikken | Ik direct kan inplannen tijdens telefoongesprek | πŸ”΄ | +| **AG-US03** | Behandelaar | Een patient zoeken tijdens afspraak maken | De afspraak gekoppeld is aan het juiste dossier | πŸ”΄ | +| **AG-US04** | Behandelaar | Een waarschuwing zien bij dubbele boeking | Ik niet per ongeluk overlap creΓ«er | πŸ”΄ | +| **AG-US05** | Behandelaar | De waarschuwing kunnen overrulen | Ik flexibel kan plannen wanneer nodig | πŸ”΄ | +| **AG-US06** | Behandelaar | Een afspraak verzetten via drag-drop | Wijzigingen snel zijn doorgevoerd | 🟑 | +| **AG-US07** | Behandelaar | Vanuit een afspraak direct een verslag starten | AI-verslaglegging naadloos in mijn workflow past | πŸ”΄ | +| **AG-US08** | Behandelaar | Op een afspraak klikken om details te zien | Ik snel patient info kan checken | 🟑 | +| **AG-US09** | Behandelaar | Een afspraak annuleren | Geannuleerde afspraken geregistreerd blijven | 🟑 | + +### Stretch User Stories + +| ID | Als... | Wil ik... | Zodat... | Prio | +|----|--------|-----------|----------|------| +| **AG-US10** | Behandelaar | Recente patiΓ«nten snel selecteren | Repeat-afspraken extra snel gaan | 🟒 | +| **AG-US11** | Behandelaar | Afspraken zien met kleurcodering per type | Ik visueel onderscheid kan maken | 🟒 | +| **AG-US12** | Behandelaar | Een mini-kalender in de sidebar zien | Ik snel naar andere weken kan navigeren | 🟒 | + +--- + +## 4. Functionele werking per onderdeel + +🎯 **Doel:** Per component beschrijven wat de gebruiker kan doen en wat het systeem doet. + +--- + +### 4.1 Kalender View (MVP) + +**Locatie:** `/epd/agenda` +**Context:** Level 1 navigatie (behandelaar-breed, niet patient-specifiek) + +#### Functioneel gedrag + +**Views beschikbaar:** +| View | Beschrijving | Standaard | +|------|--------------|-----------| +| **Dag** | Enkele dag, uren 08:00-18:00 verticaal | Nee | +| **Week** | Ma-Zo, 7 kolommen, uren verticaal | Ja βœ“ | +| **Werkdagen** | Ma-Vr, 5 kolommen | Nee | + +**Weergave-elementen:** +- **Tijdslots:** 30 minuten interval, visueel onderscheiden met subtle gridlijnen +- **Afspraak blokken:** Patient naam (bold), tijd, type indicator (kleur dot) +- **Huidige tijd indicator:** Rode lijn op "nu" moment + auto-scroll naar nu bij laden +- **Grijze zones:** Tijden buiten werktijden (vΓ³Γ³r 08:00, na 18:00) licht gearceerd +- **Vandaag markering:** Huidige dag kolom heeft licht gekleurde achtergrond + +**Visuele details kalender grid:** +``` + Ma 2 Di 3 Wo 4 (vandaag) Do 5 + ───────────────────────────────────────────────────── + 08:00 β”‚ β”‚ β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚ + β”‚ β”‚ β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚ + 09:00 β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚ + β”‚ β”‚Jan de Vβ”‚ β”‚ β”‚ ░─────────────│────────────│ ← Rode "nu" lijn + 10:00 β”‚ β”‚Intake β”‚ β”‚ β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚Maria J β”‚ β”‚ + 11:00 β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚Behandelβ”‚ β”‚ + β”‚ β”‚ β”‚Piet K. β”‚ β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +``` +_Vandaag (wo 4) heeft lichte achtergrondkleur (β–‘)_ + +**Interacties:** +| Actie | Resultaat | Feedback | +|-------|-----------|----------| +| Klik op leeg tijdslot | Opent Afspraak Modal met datum/tijd pre-filled | Slot krijgt kort highlight | +| Klik op afspraak blok | Opent Afspraak Detail Popup | Blok krijgt ring focus | +| Drag afspraak naar ander slot | Opent Confirm Dialog voor verzetten | Ghost preview op nieuwe locatie | +| Hover op afspraak | Cursor pointer + lichte lift | 100ms delay | +| Hover op leeg slot | Subtle "+" icon verschijnt in slot | Fade-in | +| Double-click leeg slot | Direct modal openen (sneller dan single click) | β€” | + +**States:** +| State | Weergave | +|-------|----------| +| Laden | Skeleton loading met shimmer effect | +| Leeg (week/dag) | Illustratie + "Geen afspraken" + prominent CTA | +| Leeg (eerste gebruik) | Onboarding card met tips | +| Gevuld | Afspraak blokken in grid | +| Fout | Inline error met retry button | +| Offline | Banner: "Je bent offline. Wijzigingen worden gesynchroniseerd." | + +**Scroll & Navigatie:** +- Bij pagina laden: auto-scroll naar huidige tijd (met 1 uur marge boven) +- Sticky uur-labels aan linkerkant bij horizontaal scrollen (mobile) +- Sticky dag-headers bij verticaal scrollen + +--- + +### 4.2 Afspraak Modal (MVP) + +**Trigger:** Klik op leeg tijdslot OF "Nieuwe afspraak" button OF bewerk-actie + +#### Formulier velden + +| Veld | Type | Verplicht | Pre-fill | Validatie | +|------|------|-----------|----------|-----------| +| **Patient** | Autocomplete search | βœ“ Ja | Nee (of recent als repeat) | Moet bestaande patient zijn | +| **Datum** | Date picker | βœ“ Ja | Geklikte datum of vandaag | Warn bij verleden | +| **Starttijd** | Time select (08:00-18:00) | βœ“ Ja | Geklikte tijd of volgende vrije | 30 min intervals | +| **Duur** | Select dropdown | βœ“ Ja | Automatisch obv type | 15/30/45/60/90/120 min | +| **Type** | Radio buttons | βœ“ Ja | Slim obv patient historie | Intake/Behandeling/Follow-up | +| **Notities** | Textarea (collapsed) | Nee | Leeg | Max 500 karakters | + +#### Smart Defaults + +**Type β†’ Duur koppeling (automatisch bij wijziging type):** +| Type geselecteerd | Duur wordt | +|-------------------|------------| +| Intake | 60 min | +| Behandeling | 45 min | +| Follow-up | 30 min | + +_User kan duur nog steeds handmatig wijzigen na auto-fill_ + +**Type suggestie obv patient historie:** +| Patient situatie | Voorgeselecteerd type | +|------------------|----------------------| +| Nieuwe patient (geen afspraken) | Intake | +| Had intake, geen behandeling | Behandeling | +| Had 3+ behandelingen | Follow-up | +| Laatste afspraak > 6 maanden | Intake (herintake) | + +#### Formulier Layout (Progressive Disclosure) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Nieuwe afspraak βœ• β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Patient * β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ” Zoek patient... β”‚ β”‚ ← Auto-focus hier +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ“… Do 5 dec β”‚ β”‚ πŸ• 14:00 β–Ό β”‚ β”‚ ← Compact: datum + tijd +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Duur: β—‹ 30 ● 45 β—‹ 60 β—‹ 90 min β”‚ ← Inline radio (niet dropdown) +β”‚ β”‚ +β”‚ Type: β—‹ Intake ● Behandeling β—‹ Follow β”‚ ← Inline radio +β”‚ β”‚ +β”‚ [+ Notitie toevoegen] β”‚ ← Collapsed by default +β”‚ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [Annuleren] [βœ“ Opslaan] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +_Bij klik "+ Notitie toevoegen" β†’ expand naar textarea_ + +#### Inline Validatie + +| Veld | Wanneer valideren | Feedback | +|------|-------------------|----------| +| Patient | On blur + na 500ms idle | Rode border + "Selecteer een patient" | +| Datum | On change | Gele border + "Let op: verleden datum" | +| Tijd | On change | Check conflict realtime, toon inline warning | +| Notities | On input | Character counter: "234/500" | + +**Conflict preview (inline, vΓ³Γ³r submit):** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ⚠️ Overlap met: Jan de Vries 14:00 β”‚ ← Inline warning +β”‚ [Andere tijd kiezen] of [Negeren] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### Functionaliteit + +**Validatie flow (optimized):** +``` +1. Bij elk veld change: inline validatie +2. Conflict check: real-time bij datum/tijd wijziging +3. Bij "Opslaan": + a. Final validation sweep + b. Als conflict: toon inline warning (niet aparte dialog voor simpele cases) + c. Als meerdere conflicts: toon Conflict Dialog + d. Opslaan β†’ optimistic update β†’ toast +``` + +**Acties:** +| Knop | Gedrag | State | +|------|--------|-------| +| **Opslaan** | Valideer β†’ Sla op β†’ Sluit β†’ Toast | Disabled tijdens save, spinner | +| **Annuleren** | Check unsaved β†’ Sluit modal | β€” | +| **Verwijderen** (edit) | Inline confirm β†’ Soft delete | Rode tekst, niet prominent | + +**Keyboard shortcuts:** +- `Cmd/Ctrl + Enter` = Opslaan (ook vanuit textarea) +- `Escape` = Annuleren (met unsaved check) +- `Tab` = Volgende veld +- `↓` / `↑` in dropdowns = Navigate options + +--- + +### 4.3 Patient Zoeken (MVP) + +**Locatie:** Autocomplete input in Afspraak Modal + +#### Functioneel gedrag + +**Zoeklogica:** +- Minimaal 2 karakters voor zoekstart +- Zoekt op: achternaam, voornaam, BSN (laatste 4 cijfers), geboortedatum +- Resultaten: Max 10, gesorteerd op relevantie (exacte match eerst) +- Debounce: 300ms na laatste toetsaanslag + +**Resultaat weergave:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ” "jan de" ⟳ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ ⭐ Recente patiΓ«nten β”‚ ← Sectie header (subtle) +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Jan de Vries ↡ β”‚ ← Recent + keyboard hint +β”‚ β™‚ 12-03-1985 (39 jaar) Β· Laatste: 28 nov β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ πŸ” Zoekresultaten β”‚ ← Sectie header +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Jantine de Boer β”‚ +β”‚ ♀ 05-08-1972 (52 jaar) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Jan de Groot β”‚ +β”‚ β™‚ 23-07-1990 (34 jaar) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ + Nieuwe patient aanmaken β”‚ ← Altijd onderaan +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**States:** +| State | Weergave | +|-------|----------| +| Leeg (focus) | Toon recente patiΓ«nten (max 5) als suggestie | +| Typen (< 2 chars) | "Typ minimaal 2 karakters..." | +| Zoeken | Spinner rechts in input | +| Resultaten | Recente patiΓ«nten bovenaan, dan zoekresultaten | +| Geen resultaat | "Geen patiΓ«nt gevonden" + link naar patient aanmaken | +| Geselecteerd | Chip met patient naam + X om te wissen | +| Keyboard navigatie | Highlighted row met subtle achtergrond | + +**UX verbeteringen:** +- **Recente patiΓ«nten:** Bij focus zonder input, toon laatste 5 bezochte patiΓ«nten +- **Match highlighting:** Zoekterm bold in resultaten ("**Jan de** Vries") +- **Keyboard hints:** Toon "↡" icon bij geselecteerde row +- **Laatste afspraak:** Toon "Laatste: 28 nov" voor context +- **Focus management:** Auto-focus op input bij modal open + +--- + +### 4.4 Conflict Dialog (MVP) + +**Trigger:** Automatisch bij opslaan afspraak met overlap + +#### Functioneel gedrag + +**Detectie logica:** +- Check: Nieuwe afspraak `[start, eind]` overlapt met bestaande afspraak(en)? +- Overlap = enige overlap in tijd (inclusief aangrenzend) +- Zelfde behandelaar (huidige user) is criterium + +**Dialog inhoud:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ⚠️ Dubbele boeking gedetecteerd β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Je hebt al een afspraak op dit tijdstip: β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ“… Donderdag 5 dec, 14:00-15:00 β”‚ β”‚ +β”‚ β”‚ πŸ‘€ Jan de Vries β”‚ β”‚ +β”‚ β”‚ πŸ“‹ Intake β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Wil je de tijd aanpassen of toch inplannen? β”‚ +β”‚ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [Wijzigen] [Toch inplannen] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Acties:** +| Knop | Gedrag | +|------|--------| +| **Wijzigen** | Sluit dialog, terug naar modal met tijd veld gefocust | +| **Toch inplannen** | Bypass conflict, sla op, sluit alles, toast met waarschuwing | + +**Business rule:** +> Systeem waarschuwt maar blokkeert NIET. Behandelaars hebben valide redenen voor overlap (bijv. administratie-tijd, korte telefoon tussendoor). + +--- + +### 4.5 Afspraak Detail Popup (MVP) + +**Trigger:** Klik op afspraak blok in kalender + +#### Functioneel gedrag + +**Popup inhoud:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Jan de Vries βœ• β”‚ +β”‚ β™‚ 39 jaar Β· Intake #3 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ πŸ“… Do 5 dec 2024 β”‚ +β”‚ πŸ• 14:00 - 15:00 (60 min) β”‚ +β”‚ ● Intakegesprek β”‚ ← Kleur dot voor type +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Notities: β”‚ +β”‚ Eerste gesprek na verwijzing huisarts. β”‚ +β”‚ Verwacht: angstklachten, slaapproblemen. β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ [πŸ“ Maak verslag] [✏️ Bewerken]β”‚ +β”‚ β”‚ +β”‚ ─────────────────────────────────────── β”‚ +β”‚ [πŸ‘€ Naar dossier] [πŸ—‘οΈ Annuleren] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Acties:** +| Knop | Gedrag | Visuele prioriteit | +|------|--------|-------------------| +| **πŸ“ Maak verslag** | Navigeer naar rapportage met pre-fills | **Primary** (solid button) | +| **✏️ Bewerken** | Open Afspraak Modal in edit mode | Secondary (outline) | +| **πŸ‘€ Naar dossier** | Navigeer naar patient dossier | Tertiary (text link) | +| **πŸ—‘οΈ Annuleren** | Confirm dialog β†’ soft delete | Tertiary (text link, rode tekst) | + +**Contextuele CTA (slimme primary actie):** +| Afspraak status | Primary actie | +|-----------------|---------------| +| Gepland (toekomst) | "Bewerken" prominent | +| Vandaag/nu | "**Maak verslag**" prominent (pulsing) | +| Verleden zonder verslag | "**Maak verslag**" prominent + badge "!" | +| Verleden met verslag | "Bekijk verslag" + "Bewerken" | + +**Status indicatoren:** +| Status | Badge | Extra visueel | +|--------|-------|---------------| +| `planned` | πŸ”΅ Blauw | β€” | +| Vandaag | 🟒 Groen | Subtle glow effect | +| `in-progress` | 🟒 Groen pulsing | "Nu bezig" label | +| `on-hold` | 🟠 Oranje | Pauze icon | +| `completed` | βšͺ Grijs | Check icon | +| `cancelled` | πŸ”΄ Rood | Doorgestreepte tekst | + +**UX verbeteringen:** +- **Quick-action keyboard:** `V` = Verslag maken, `E` = Edit, `D` = Delete +- **Positie:** Popup verschijnt naast afspraak blok, niet centered (context behouden) +- **Click-outside:** Sluit popup +- **Animatie:** Fade-in vanuit afspraak blok positie +- **Verslag indicator:** Als verslag bestaat, toon "πŸ“ Verslag (28 nov)" link in popup + +--- + +### 4.6 Toolbar (MVP) + +**Locatie:** Boven kalender grid, sticky bij scrollen + +#### Functioneel gedrag + +**Desktop layout:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [β—€] [Vandaag] [β–Ά] πŸ“… Week 49 Β· 2-8 december 2024 [Dag|Week|Werk] β”‚ +β”‚ [+ Nieuwe afspraak] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Tablet layout (768-1024px):** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [β—€] [Vandaag] [β–Ά] Week 49 [D|W|5] [+ Nieuw] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Mobile layout (<768px):** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [β—€] Do 5 december [β–Ά] [+] β”‚ ← Compact, alleen dag +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +| Element | Gedrag | Keyboard | +|---------|--------|----------| +| **β—€ / β–Ά** | Navigeer 1 periode terug/vooruit | `←` / `β†’` | +| **Vandaag** | Spring naar huidige datum + scroll naar nu | `T` | +| **Datum label** | Klik β†’ open date picker voor directe navigatie | β€” | +| **View toggle** | Switch tussen views | `1`/`2`/`3` | +| **+ Nieuwe afspraak** | Open modal zonder pre-fill datum | `N` | + +**View toggle states:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [Dag] Week Werkdagen β”‚ ← Dag geselecteerd +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + ↓ na klik op Week +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Dag [Week] Werkdagen β”‚ ← Week geselecteerd, animated underline +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Extra UX details:** +- **Vandaag button:** Highlight als niet op vandaag (subtle pulsing dot) +- **Datum klik:** Opent kleine kalender picker voor snelle week-jump +- **Navigatie animatie:** Smooth slide transition bij periode wisseling +- **Loading tijdens navigatie:** Spinner in datum label, grid blijft staan + +--- + +### 4.7 Mini Kalender Sidebar (Stretch) + +**Locatie:** Linker sidebar op desktop + +#### Functioneel gedrag (indien gebouwd) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ December 2024 β”‚ +β”‚ Ma Di Wo Do Vr Za Zoβ”‚ +β”‚ 1 β”‚ +β”‚ 2 3 4 5β€’ 6 7 8β”‚ +β”‚ 9 10 11 12 13 14 15β”‚ +β”‚ 16 17 18 19 20 21 22β”‚ +β”‚ 23 24 25 26 27 28 29β”‚ +β”‚ 30 31 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +- **Bullet (β€’)**: Dagen met afspraken +- **Highlight**: Geselecteerde dag/week +- **Klik op dag**: Navigeer naar die dag in main view + +--- + +## 5. UI-overzicht (visuele structuur) + +🎯 **Doel:** Globale schermopbouw communiceren. + +### 5.1 Desktop Layout (β‰₯1024px) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ EPD Sidebar β”‚ Agenda Module β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚Dashboardβ”‚ β”‚ β”‚ Toolbar: [β—€][Vandaag][β–Ά] Week 49 [Dag|Week|Werk] β”‚β”‚ +β”‚ │─────────│ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚ +β”‚ β”‚CliΓ«nten β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ │─────────│ β”‚ β”‚ Ma Di Wo Do Vr Za Zo β”‚β”‚ +β”‚ β”‚β–ΊAgenda β”‚ β”‚ β”‚ ───────────────────────────────────────── β”‚β”‚ +β”‚ │─────────│ β”‚ β”‚ 08:00 β”‚β”‚ +β”‚ β”‚Rapportageβ”‚ β”‚ β”‚ 09:00 β”Œβ”€β”€β”€β”€β” β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 10:00 β”‚Jan β”‚ β”Œβ”€β”€β”€β”€β” β”‚β”‚ +β”‚ β”‚ β”‚ 11:00 β”‚de Vβ”‚ β”‚Mariβ”‚ β”‚β”‚ +β”‚ β”‚ β”‚ 12:00 β””β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β”‚ 13:00 β””β”€β”€β”€β”€β”˜ β”‚β”‚ +β”‚ β”‚ β”‚ 14:00 β”Œβ”€β”€β”€β”€β” β”‚β”‚ +β”‚ β”‚ β”‚ 15:00 β”‚Pietβ”‚ β”‚β”‚ +β”‚ β”‚ β”‚ 16:00 β””β”€β”€β”€β”€β”˜ β”‚β”‚ +β”‚ β”‚ β”‚ 17:00 β”‚β”‚ +β”‚ β”‚ β”‚ 18:00 β”‚β”‚ +β”‚ β”‚ └──────────────────────────────────────────────────────│ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 5.2 Mobile Layout (<768px) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ≑ Agenda + Nieuw β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β—€ Vandaag β–Ά β”‚ +β”‚ Do 5 december 2024 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 08:00 ──────────────────│ +β”‚ 09:00 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Jan de Vries β”‚ β”‚ +β”‚ 10:00 β”‚ Intake β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ 11:00 ──────────────────│ +β”‚ 12:00 ──────────────────│ +β”‚ 13:00 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Maria Jansen β”‚ β”‚ +β”‚ 14:00 β”‚ Behandeling β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ 15:00 ──────────────────│ +β”‚ ... β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Mobile aanpassingen:** +- Alleen dag-view standaard +- Geen drag-drop (gebruik edit button) +- Swipe links/rechts voor dag navigatie +- Hamburger menu voor EPD sidebar + +### 5.3 Afspraak Modal (Responsive) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Nieuwe afspraak βœ• β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Patient * β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ” Zoek patient... β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Datum * Starttijd * β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ“… 05-12-2024 β”‚ β”‚ 14:00 β–Ό β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Duur * β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ 60 minuten β–Ό β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Type afspraak * β”‚ +β”‚ β—‹ Intake ● Behandeling β—‹ Follow-up β”‚ +β”‚ β”‚ +β”‚ Notities β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [Annuleren] [Opslaan] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 5.5 UX Richtlijnen & Micro-interacties + +🎯 **Doel:** DefiniΓ«ren van de kleine details die de ervaring vloeiend maken. + +### 5.5.1 Design Principes + +| Principe | Toepassing in Agenda | +|----------|---------------------| +| **Snelheid boven alles** | Afspraak maken < 10 sec, geen onnodige stappen | +| **Visuele feedback** | Elke actie toont direct resultaat | +| **Foutpreventie > foutcorrectie** | Slim valideren vΓ³Γ³r submit | +| **Flexibiliteit** | Waarschuwen, niet blokkeren | +| **Context behouden** | Na actie terug naar waar je was | + +### 5.5.2 Micro-interacties & Animaties + +**Afspraak blokken:** +| Interactie | Visuele feedback | Timing | +|------------|------------------|--------| +| Hover | Lichte schaduw + cursor pointer | Instant | +| Klik | Scale 0.98 + donkerder | 100ms ease | +| Drag start | Opacity 0.7 + verhoogde schaduw | 150ms | +| Drag over slot | Doelslot krijgt highlight border | Instant | +| Drop | Bounce-in animatie naar nieuwe positie | 200ms spring | + +**Modals & Dialogs:** +| Actie | Animatie | Timing | +|-------|----------|--------| +| Open | Fade-in + slide-up | 200ms ease-out | +| Sluiten | Fade-out + slide-down | 150ms ease-in | +| Achtergrond | Blur + dim (opacity 0.5) | 200ms | + +**Feedback elementen:** +| Element | Gedrag | +|---------|--------| +| Toast success | Slide-in rechtsonder, auto-dismiss 3s | +| Toast warning | Slide-in, persist tot dismiss | +| Loading spinner | Pulse animatie, centered in context | +| Skeleton | Shimmer effect linksβ†’rechts | + +### 5.5.3 Optimistic Updates + +**Principe:** UI update direct, rollback bij fout. + +| Actie | Optimistic gedrag | Bij fout | +|-------|-------------------|----------| +| Afspraak opslaan | Direct in kalender tonen | Remove + toast "Opslaan mislukt, probeer opnieuw" | +| Afspraak verzetten | Direct naar nieuwe positie | Terug naar origineel + toast | +| Afspraak annuleren | Direct doorstrepen (visueel) | Herstel + toast | + +**Implementatie voorbeeld:** +``` +1. User klikt "Opslaan" +2. Modal sluit, afspraak verschijnt in kalender (grijs/loading state) +3. API call in background +4. Success β†’ afspraak krijgt normale styling +5. Error β†’ afspraak verdwijnt, toast met retry optie +``` + +### 5.5.4 Smart Defaults & Auto-fill + +**Tijdslot klikken:** +| Context | Auto-fill | +|---------|-----------| +| Klik op 14:00 | Datum + tijd pre-filled | +| Klik na laatste afspraak | Suggereer aansluitend tijdslot | +| Klik op zelfde dag als recente afspraak | Suggereer zelfde patient | + +**Type selectie:** +| Laatste afspraak patient | Suggestie | +|--------------------------|-----------| +| Geen eerdere afspraken | Intake (standaard) | +| Had intake | Behandeling | +| Had 3+ behandelingen | Follow-up | + +**Duur selectie:** +| Type | Standaard duur | +|------|----------------| +| Intake | 60 min | +| Behandeling | 45 min | +| Follow-up | 30 min | + +### 5.5.5 Keyboard Navigatie + +**Globale shortcuts (wanneer op agenda pagina):** +| Shortcut | Actie | +|----------|-------| +| `N` | Nieuwe afspraak (opent modal) | +| `T` | Ga naar vandaag | +| `←` / `β†’` | Vorige/volgende periode | +| `1` / `2` / `3` | Switch naar Dag/Week/Werkdagen | +| `Esc` | Sluit modal/popup | + +**In Afspraak Modal:** +| Shortcut | Actie | +|----------|-------| +| `Tab` | Volgende veld | +| `Shift+Tab` | Vorige veld | +| `Enter` | Opslaan (als geen textarea focus) | +| `Esc` | Annuleren (met unsaved changes check) | +| `↓` / `↑` | Navigeer zoekresultaten | + +**In Patient zoeken:** +| Shortcut | Actie | +|----------|-------| +| `↓` | Eerste/volgende resultaat selecteren | +| `↑` | Vorige resultaat | +| `Enter` | Bevestig selectie | +| `Esc` | Sluit dropdown, behoud huidige selectie | + +### 5.5.6 Empty States & Onboarding + +**Lege agenda (geen afspraken deze week):** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ πŸ“… β”‚ +β”‚ β”‚ +β”‚ Geen afspraken deze week β”‚ +β”‚ β”‚ +β”‚ Klik op een tijdslot om je eerste β”‚ +β”‚ afspraak in te plannen, of gebruik: β”‚ +β”‚ β”‚ +β”‚ [+ Nieuwe afspraak] β”‚ +β”‚ β”‚ +β”‚ πŸ’‘ Tip: Druk 'N' voor snelle invoer β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Eerste keer agenda openen (onboarding hint):** +- Subtle pulse animatie op "+ Nieuwe afspraak" button +- Tooltip: "Begin hier met je eerste afspraak" +- Verdwijnt na eerste afspraak of dismiss + +### 5.5.7 Error Handling & Recovery + +**Graceful degradation:** +| Fout scenario | User ziet | +|---------------|-----------| +| API timeout | "Even geduld..." β†’ na 5s: "Kon niet laden. [Opnieuw proberen]" | +| Conflict check faalt | Sla op zonder conflict check + warning toast | +| Patient search faalt | "Zoeken niet beschikbaar. Typ volledige naam." | + +**Undo functionaliteit:** +| Actie | Undo mogelijkheid | +|-------|-------------------| +| Afspraak verwijderd | Toast met "Ongedaan maken" link (5 sec window) | +| Afspraak verzet | Toast met "Ongedaan maken" link (5 sec window) | +| Modal gesloten met unsaved | "Je hebt onopgeslagen wijzigingen. Sluiten?" | + +**Unsaved changes detection:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Wijzigingen niet opgeslagen β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Je hebt wijzigingen die nog niet zijn β”‚ +β”‚ opgeslagen. Wil je deze verliezen? β”‚ +β”‚ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [Terug naar formulier] [Niet opslaan] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 5.5.8 Visual Hierarchy in Kalender + +**Tijdslot visuele staten:** +| Staat | Visuele behandeling | +|-------|---------------------| +| Beschikbaar | Wit/licht, hover highlight | +| Bezet | Afspraak blok | +| Verleden | Licht grijs, minder contrast | +| Buiten werktijd | Gestreept/gearceerd patroon | +| Nu (huidige tijd) | Rode lijn indicator | +| Geselecteerd slot | Blue outline | + +**Afspraak blok informatie hiΓ«rarchie:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Jan de Vries πŸ“ β”‚ ← Naam (bold) + verslag icon +β”‚ 14:00 - 15:00 β”‚ ← Tijd (secondary) +β”‚ ● Intake β”‚ ← Type met kleur dot +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Bij weinig ruimte (korte afspraken 15-30 min):** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ J. de Vries 14:00 ● β”‚ ← Compacte weergave +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 5.5.9 Touch & Mobile Specifiek + +**Touch targets:** +- Minimum 44x44px voor alle interactieve elementen +- Afspraak blokken: volledige breedte van kolom + +**Gestures:** +| Gesture | Actie | +|---------|-------| +| Swipe links/rechts | Volgende/vorige dag | +| Long press op afspraak | Open context menu (bewerken/verwijderen) | +| Pull down | Refresh agenda data | +| Tap op leeg slot | Toon "+" button β†’ tap = nieuwe afspraak | + +**Mobile-specifieke UI:** +- Geen drag-drop (te foutgevoelig op touch) +- Bottom sheet ipv centered modal +- Sticky header met datum bij scrollen + +### 5.5.10 Loading States per Component + +| Component | Loading state | +|-----------|---------------| +| Agenda grid | Skeleton met 3-4 placeholder blokken | +| Patient search | Spinner in input rechts | +| Modal opslaan | Button disabled + spinner + "Opslaan..." | +| View switch | Fade-out β†’ skeleton β†’ fade-in | +| Datum navigatie | Kort skeleton (200ms max) | + +**Perceived performance:** +- Prefetch volgende/vorige week data bij idle +- Cache huidige week, invalidate bij mutatie +- Stale-while-revalidate pattern + +--- + +## 6. Interacties met AI (functionele beschrijving) + +🎯 **Doel:** Uitleggen waar AI in de agenda flow voorkomt. + +### 6.1 AI-integratiepunten in Agenda Module + +| Locatie | AI-actie | Trigger | Output | +|---------|----------|---------|--------| +| Afspraak Detail | **β†’ Maak verslag** | Klik op "Maak verslag" button | Navigatie naar Rapportage met pre-fills | +| Rapportage (na navigatie) | Transcriptie | Klik "Opname starten" | Audio β†’ tekst via Deepgram | +| Rapportage (na navigatie) | Verslag genereren | Klik "AI genereer" | Gestructureerd verslag op basis van transcriptie | + +### 6.2 Afspraak β†’ Verslag Flow (Kernintegratie) + +Dit is de **killer feature** van de agenda integratie: + +**Stap-voor-stap:** +``` +1. Behandelaar klikt op afspraak in agenda (bijv. "Jan de Vries - 10:00 Intake") +2. Detail popup opent met "πŸ“ Maak verslag" button +3. Behandelaar klikt "Maak verslag" +4. Systeem navigeert naar: /epd/patients/[jan-id]/rapportage?encounter=[afspraak-id] +5. Rapportage module opent met: + - Patient: Jan de Vries (pre-filled, read-only) + - Type: Intake verslag (pre-selected) + - Afspraak referentie: 05-12-2024 10:00 (gelinkt) +6. Behandelaar kan direct: + - Audio opnemen β†’ AI transcribeert + - AI verslag laten genereren + - Handmatig schrijven +7. Bij opslaan: verslag automatisch gekoppeld aan afspraak +8. In agenda: afspraak toont nu πŸ“ icon (verslag aanwezig) +``` + +**Pre-fill mapping:** + +| Rapportage veld | Bron uit Afspraak | +|-----------------|-------------------| +| `patient_id` | `appointment.patient_id` | +| `encounter_id` | `appointment.id` | +| `report_type` | Gebaseerd op `appointment.type` (intake β†’ "intake_verslag") | +| `title` | `"Verslag {type} - {patient_naam} - {datum}"` | + +### 6.3 Bidirectionele Linking + +**Van Agenda naar Dossier:** +- Afspraak detail popup bevat link naar patient dossier +- Klik "Naar dossier" β†’ `/epd/patients/[id]` + +**Van Dossier naar Agenda:** +- In patient timeline: afspraken weergeven +- Klik op afspraak β†’ terug naar agenda met die afspraak geselecteerd + +**Van Afspraak naar Verslag:** +- Afspraak icon toont πŸ“ als verslag bestaat +- Klik op icon β†’ direct naar verslag + +**Van Verslag naar Afspraak:** +- Verslag header toont "Gekoppeld aan afspraak: Do 5 dec 14:00" +- Klik β†’ terug naar afspraak detail + +--- + +## 7. Gebruikersrollen en rechten + +🎯 **Doel:** Beschrijven welke rollen toegang hebben. + +### MVP: Single-User Model + +In de MVP is er **geen rechtenbeheer**. Alle functionaliteit is beschikbaar voor de ingelogde behandelaar. + +| Rol | Toegang | Beperkingen | +|-----|---------|-------------| +| **Behandelaar** (enige rol) | Volledige agenda | Alleen eigen afspraken zichtbaar | + +### Data Scoping + +- **Afspraken:** Gefilterd op `practitioner_id = current_user.id` +- **PatiΓ«nten:** Alle patiΓ«nten in systeem doorzoekbaar (MVP simplificatie) +- **Verslagen:** Alleen eigen verslagen + +### Post-MVP: Multi-User Model + +| Rol | Toegang | Beperkingen | +|-----|---------|-------------| +| Behandelaar | Eigen agenda | Alleen eigen afspraken bewerken | +| Assistent | Alle agenda's | Inplannen voor anderen, niet verwijderen | +| Manager | Alle agenda's (readonly) | Geen bewerkingen, wel rapportages | + +--- + +## 8. Data Model (Functionele Entiteiten) + +🎯 **Doel:** Functioneel overzicht van data-entiteiten. + +### 8.1 Afspraak (Encounter) + +| Attribuut | Type | Beschrijving | +|-----------|------|--------------| +| `id` | UUID | Unieke identifier | +| `patient_id` | UUID | Gekoppelde patient | +| `practitioner_id` | UUID | Behandelaar (current user) | +| `period_start` | DateTime | Starttijd afspraak | +| `period_end` | DateTime | Eindtijd afspraak | +| `type_code` | Enum | intake / treatment / followup | +| `type_display` | String | "Intakegesprek", "Behandeling", etc. | +| `status` | Enum | planned / in-progress / on-hold / completed / cancelled | +| `notes` | Text | Vrije notities | +| `created_at` | DateTime | Aanmaakdatum | +| `updated_at` | DateTime | Laatste wijziging | + +### 8.2 Afspraak Types + +| Code | Display (NL) | Standaard duur | Kleur (stretch) | +|------|--------------|----------------|-----------------| +| `intake` | Intakegesprek | 60 min | πŸ”΅ Blauw | +| `treatment` | Behandeling | 45 min | 🟒 Groen | +| `followup` | Follow-up | 30 min | 🟑 Geel | + +### 8.3 Status Lifecycle + +``` +planned β†’ in-progress β†’ completed + ↓ ↓ +on-hold cancelled (soft delete) +``` + +| Status | Betekenis | Automatisch | +|--------|-----------|-------------| +| `planned` | Ingepland, nog niet gestart | Bij aanmaken | +| `in-progress` | Momenteel bezig | Bij starttijd? (post-MVP) | +| `on-hold` | Tijdelijk gepauzeerd | Handmatig (post-MVP) | +| `completed` | Afgerond | Handmatig of na verslag opslaan | +| `cancelled` | Geannuleerd | Bij "Verwijderen" actie | + +--- + +## 9. Flows in Detail + +🎯 **Doel:** Gedetailleerde stap-voor-stap beschrijvingen. + +### 9.1 Flow: Nieuwe Afspraak (Happy Path) + +**Scenario:** Behandelaar plant afspraak in tijdens telefoongesprek + +```mermaid +sequenceDiagram + actor B as Behandelaar + participant K as Kalender + participant M as Afspraak Modal + participant S as Systeem + participant DB as Database + + B->>K: Klikt op leeg tijdslot (Do 14:00) + K->>M: Opent modal met pre-fill + Note over M: Datum: 05-12-2024
Tijd: 14:00 + B->>M: Typt "jan de" in patient zoek + M->>S: Search request (debounced) + S->>DB: Query patients WHERE name LIKE 'jan de%' + DB-->>S: [Jan de Vries, Jantine de Boer] + S-->>M: Resultaten + B->>M: Selecteert "Jan de Vries" + B->>M: Selecteert type "Behandeling" + B->>M: Klikt "Opslaan" + M->>S: Valideer + conflict check + S->>DB: Check overlap voor practitioner + DB-->>S: Geen conflict + S->>DB: INSERT encounter + DB-->>S: Success + S-->>M: Opslaan OK + M-->>K: Sluit modal + K->>K: Refresh + toon nieuwe afspraak + K-->>B: Toast: "Afspraak opgeslagen" +``` + +### 9.2 Flow: Dubbele Boeking + +**Scenario:** Behandelaar probeert afspraak te maken op bezette tijd + +``` +1. Behandelaar klikt op 14:00 (waar al afspraak staat met Jan de Vries) +2. Modal opent, behandelaar vult Maria Jansen in, klikt Opslaan +3. Systeem detecteert overlap: + - Nieuwe: Maria 14:00-15:00 + - Bestaand: Jan 14:00-15:00 +4. Conflict Dialog opent: + "⚠️ Je hebt al een afspraak: Jan de Vries - 14:00 (Intake)" +5a. Behandelaar kiest "Wijzigen": + β†’ Terug naar modal, tijd veld gefocust + β†’ Kiest 15:00, opslaan succesvol +5b. Behandelaar kiest "Toch inplannen": + β†’ Beide afspraken op 14:00 + β†’ Toast: "Afspraak opgeslagen (dubbele boeking)" +``` + +### 9.3 Flow: Afspraak β†’ Verslag + +**Scenario:** Direct na intake verslag maken via AI + +``` +1. Behandelaar klikt op afspraak "Jan de Vries - 10:00 Intake" (zojuist afgelopen) +2. Detail popup opent +3. Behandelaar klikt "πŸ“ Maak verslag" +4. Navigatie naar /epd/patients/[jan-id]/rapportage?encounter=[enc-id] +5. Rapportage module laadt met: + - Patient: Jan de Vries (locked) + - Type: Intake verslag + - Reference: "Afspraak 05-12-2024 10:00" +6. Behandelaar klikt "🎀 Start opname" +7. Behandelaar dicteert verslag van gesprek +8. Behandelaar klikt "Stop" +9. Deepgram transcribeert audio β†’ tekst +10. Behandelaar klikt "AI Genereer verslag" +11. Claude genereert gestructureerd verslag +12. Behandelaar reviewt, past aan, klikt "Opslaan" +13. Verslag opgeslagen met encounter_id link +14. Terug naar agenda: afspraak toont πŸ“ icon +``` + +### 9.4 Flow: Drag & Drop Verzetten + +**Scenario:** Patient belt, moet naar andere tijd + +``` +1. Behandelaar ziet afspraak "Maria - Di 14:00" in week view +2. Behandelaar klikt en houdt vast op afspraak card +3. Visuele feedback: card krijgt schaduw, wordt semi-transparant +4. Behandelaar sleept naar "Wo 15:00" +5. Drop β†’ Confirm dialog: + "Afspraak verzetten van di 14:00 naar wo 15:00?" + [Annuleren] [Bevestigen] +6. Behandelaar klikt "Bevestigen" +7. Systeem checkt conflict (geen) +8. Database update: period_start = wo 15:00 +9. Kalender refresh +10. Toast: "Afspraak verzet naar wo 15:00" +``` + +--- + +## 10. Validaties & Business Rules + +🎯 **Doel:** Overzicht van validatieregels. + +### 10.1 Formulier Validaties + +| Veld | Regel | Foutmelding | +|------|-------|-------------| +| Patient | Verplicht, moet bestaan | "Selecteer een patient" | +| Datum | Verplicht, format dd-mm-yyyy | "Vul een datum in" | +| Datum | Niet in verleden | "Datum ligt in het verleden" (warning, niet block) | +| Starttijd | Verplicht, 08:00-18:00 | "Kies een tijd binnen werktijden" | +| Duur | Verplicht, 15-120 min | "Kies een geldige duur" | +| Type | Verplicht | "Kies een type afspraak" | +| Notities | Max 500 karakters | "Notities te lang (max 500)" | + +### 10.2 Business Rules + +| Regel | Type | Gedrag | +|-------|------|--------| +| **Dubbele boeking** | Warning | Toon dialog, sta override toe | +| **Werktijden** | Soft | Waarschuw bij buiten 08:00-18:00, maar allow | +| **Verleden datum** | Soft | Waarschuw, maar allow (correcties achteraf) | +| **Eigen afspraken** | Hard | Kan alleen eigen afspraken zien/bewerken | +| **Soft delete** | Hard | Cancelled afspraken blijven in DB, niet verwijderd | +| **Verslag koppeling** | Auto | Verslag maakt automatisch link naar encounter | + +### 10.3 Conflict Detectie Logica + +``` +isConflict = + (new.start < existing.end) AND (new.end > existing.start) + AND (new.practitioner_id = existing.practitioner_id) + AND (existing.status != 'cancelled') +``` + +--- + +## 11. Prioritering MVP vs Post-MVP + +🎯 **Doel:** Duidelijke cut-off voor eerste oplevering. + +### MVP (Must Ship) + +| Feature | Reden | +|---------|-------| +| Week view kalender | Core navigatie | +| Dag view kalender | Alternatieve weergave | +| Nieuwe afspraak maken | Core functie | +| Patient zoeken (autocomplete) | Koppeling nodig | +| Afspraak bewerken | Wijzigingen nodig | +| Afspraak annuleren (soft delete) | Status management | +| Dubbele boeking warning | Foutpreventie | +| Conflict override | Flexibiliteit | +| "Maak verslag" navigatie | AI-integratie showcase | +| Basic drag-drop (desktop) | Snelle wijzigingen | +| Responsive mobile view | Demo-proof | + +### Stretch (Nice-to-Have voor Demo) + +| Feature | Reden | +|---------|-------| +| Werkdagen view | Extra convenience | +| Mini kalender sidebar | Navigatie gemak | +| Recente patiΓ«nten dropdown | Sneller repeat | +| Kleurcodering per type | Visueel | +| Verslag icon op afspraak | Status indicator | + +### Post-MVP (Later) + +| Feature | Wanneer | +|---------|---------| +| Recurring appointments | Na klantfeedback | +| Multi-practitioner | Bij team gebruik | +| Email/SMS notifications | Bij productie | +| Calendar sync (Google/Outlook) | Enterprise | +| Wachtlijstbeheer | Productie versie | +| Audit logging | Compliance | + +--- + +## 12. Bijlagen & Referenties + +🎯 **Doel:** Links naar gerelateerde documenten. + +### Interne Documenten + +| Document | Beschrijving | Status | +|----------|--------------|--------| +| PRD Agenda Module v1.0 | Productvisie en scope | βœ… Approved | +| TO Agenda Module (volgt) | Technische implementatie | πŸ“‹ Te schrijven | +| UX Stylesheet | Design tokens en componenten | βœ… Beschikbaar | +| Database Types | Supabase schema definities | βœ… Gegenereerd | + +### Externe Referenties + +| Bron | Gebruik | +|------|---------| +| [FullCalendar Docs](https://fullcalendar.io/docs) | Kalender library | +| [shadcn/ui](https://ui.shadcn.com/docs/components) | UI componenten | +| [date-fns](https://date-fns.org/) | Datum manipulatie | +| [FHIR Encounter](https://www.hl7.org/fhir/encounter.html) | Data model referentie | + +### Bestaande Code Patronen + +Referentie naar bestaande implementaties in codebase: + +| Pattern | Locatie | Hergebruik voor | +|---------|---------|-----------------| +| Patient list + search | `app/epd/patients/` | Patient zoeken component | +| Form validation | `app/epd/patients/[id]/intakes/` | Afspraak formulier | +| Modal dialogs | `components/ui/dialog.tsx` | Afspraak modal | +| Toast notifications | `components/ui/toaster.tsx` | Feedback | +| Server actions | `app/epd/patients/[id]/intakes/actions.ts` | CRUD operaties | +| Type definitions | `lib/types/intake.ts` | Appointment types | + +--- + +## Changelog + +| Versie | Datum | Auteur | Wijziging | +|--------|-------|--------|-----------| +| v1.0 | 02-12-2024 | Colin | Initieel FO op basis van PRD v1.0 | +| v1.1 | 02-12-2024 | Colin | UX verbeteringen: micro-interacties, smart defaults, keyboard nav, optimistic updates, inline validatie, responsive toolbar, recente patiΓ«nten, contextuele CTAs | + +--- + +**Document Status:** βœ… Klaar voor review +**Volgende stap:** β†’ Technisch Ontwerp (TO) Agenda Module + +--- + +## Appendix: UX Checklist voor Implementatie + +Snelle checklist voor developers om UX verbeteringen niet te vergeten: + +### Must-Have UX (MVP) +- [ ] Auto-scroll naar "nu" bij pagina laden +- [ ] Vandaag kolom highlight +- [ ] Skeleton loading states +- [ ] Optimistic updates met rollback +- [ ] Inline validatie (niet alleen op submit) +- [ ] Smart defaults: type β†’ duur koppeling +- [ ] Keyboard shortcuts: N, T, ←, β†’, Esc +- [ ] Focus management: auto-focus patient search in modal +- [ ] Toast feedback bij alle acties +- [ ] Unsaved changes warning +- [ ] Recente patiΓ«nten bij lege search +- [ ] Contextuele primary button (verslag maken prominent na afspraak) + +### Nice-to-Have UX (Stretch) +- [ ] Double-click op slot voor snelle afspraak +- [ ] Hover "+" icon op lege slots +- [ ] Match highlighting in zoekresultaten +- [ ] Slide animaties bij view/datum wisseling +- [ ] Undo toast na delete/verzetten (5 sec window) +- [ ] Prefetch volgende/vorige week +- [ ] Mobile swipe gestures +- [ ] Onboarding hints bij eerste gebruik diff --git a/docs/specs/agenda/prd-agenda-module-v1_0.md b/docs/specs/agenda/prd-agenda-module-v1_0.md new file mode 100644 index 0000000..ecd1504 --- /dev/null +++ b/docs/specs/agenda/prd-agenda-module-v1_0.md @@ -0,0 +1,695 @@ +# πŸ“„ Product Requirements Document (PRD) β€” Agenda Module + +**Projectnaam:** Mini-EPD Agenda Module +**Versie:** v1.0 +**Datum:** 02-12-2024 +**Auteur:** Colin + +--- + +## 1. Doelstelling + +🎯 **Doel van deze sectie:** Beschrijf waarom dit product of prototype wordt gebouwd en wat het beoogde resultaat is. + +### Waarom deze module? + +De agenda is het **kloppend hart** van elk EPD-systeem. Zonder agenda geen structuur, geen overzicht, geen efficiΓ«nte praktijkvoering. Voor het AI Speedrun project is de agenda module cruciaal omdat het: + +1. **De workflow compleet maakt**: Intake β†’ Behandelplan β†’ Afspraken β†’ Verslaglegging β†’ Herhaal +2. **Dagelijks gebruik afdwingt**: Een EPD zonder agenda wordt niet gebruikt, want behandelaars hebben dit elke dag nodig +3. **AI-integratie natuurlijk maakt**: Afspraak β†’ AI-verslag in 1 klik, naadloos in de dagelijkse workflow +4. **Geloofwaardigheid toevoegt**: "Zonder agenda is het geen echt EPD" β€” dit maakt het prototype serieus + +### Wat willen we bereiken? + +Een **werkende agenda module** die behandelaars: +- Laat plannen, verzetten en annuleren van afspraken +- Koppelt aan patiΓ«ntendossiers Γ©n verslagen (bidirectioneel) +- Waarschuwt bij dubbele boekingen (maar niet blokkeert β€” flexibiliteit!) +- Integreert met bestaande intake/rapportage flows + +**Type oplevering:** MVP voor AI Speedrun demo + basis voor productieversie + +**Kernboodschap voor demo:** +> "Dit is geen speeltje β€” dit is een werkend EPD waar je morgen mee aan de slag kunt. Hier is mijn agenda voor deze week, klik, nieuwe afspraak, patient selecteren, opslaan. En nu direct een verslag maken. Dat is AI in de praktijk." + +--- + +## 2. Doelgroep + +🎯 **Doel:** Schets wie de eindgebruikers, stakeholders en testers zijn. + +### Primaire gebruikers (direct) + +**1. Behandelaars in de GGZ** +- **Behoefte:** Overzicht van hun dag/week, snel afspraken plannen, direct verslagen kunnen maken +- **Pijnpunten met huidige systemen:** + - Te veel klikken om een afspraak te maken + - Geen koppeling tussen afspraak en verslag + - Geen waarschuwing bij dubbele boekingen + - Slow and clunky interfaces +- **Wat ze willen zien:** "Kan ik hier echt mijn praktijk mee runnen?" + +**2. Praktijkassistenten / Secretaresses** +- **Behoefte:** Afspraken inplannen voor behandelaars, overzicht van beschikbaarheid +- **Pijnpunten:** Veel telefoontjes, handmatige planning, geen real-time updates +- **Opmerking:** Dit is stretch voor MVP β€” focus ligt op single-user (behandelaar) + +### Secundaire stakeholders (indirect) + +**3. Zorgmanagers / Praktijkhouders** +- **Interesse:** Kan dit hun praktijk efficiΓ«nter maken? +- **Vraag:** "Hoeveel kost dit vs. PinkRoccade/Nedap?" +- **Demo-moment:** Laten zien hoe snel afspraken worden ingepland vs. legacy systemen + +**4. Developers / Tech Managers** +- **Interesse:** Hoe is dit gebouwd? Wat kan AI echt? +- **Vraag:** "Hoeveel werk is dit geweest? Kan ons team dit ook?" +- **Demo-moment:** Transparantie over buildtime, tech stack, AI-gebruik + +**5. PotentiΓ«le klanten (via LinkedIn)** +- **Interesse:** Proof of concept dat "Software on Demand" werkt +- **Vraag:** "Is dit echt in 4 weken gebouwd?" +- **Demo-moment:** Build metrics, transparante timeline + +### Persona's + +**Persona 1: Marieke (Psycholoog, 38 jaar)** +- Zelfstandig gevestigd, 3 dagen per week cliΓ«nten +- Gebruikt nu Google Agenda + Word voor verslagen +- Frustratie: "Ik moet alles dubbel invoeren en kan niks terugvinden" +- Wens: "Gewoon een simpel systeem waar alles bij elkaar staat" + +**Persona 2: Bas (Psychiater, 52 jaar)** +- Werkt in instelling met Nedap ECD +- Frustratie: "Het kost me 5 minuten om een afspraak te maken, en dan crasht het nog" +- Wens: "Iets wat gewoon werkt, zoals Outlook maar dan met verslagen" + +--- + +## 3. Kernfunctionaliteiten (MVP-scope) + +🎯 **Doel:** Afbakenen van de minimale werkende functies. + +### Must-Have (MVP) + +1. **Kalenderweergave** + - **Input:** Datum selectie + - **Output:** Dag/Week/Werkdagen view met afspraken + - **Interactie:** Klikken op tijdslot = nieuwe afspraak, klikken op afspraak = details + - **Demo-waarde:** "Dit is mijn agenda, net als Outlook" + +2. **Afspraak maken** + - **Input:** Patient, datum/tijd, type afspraak (intake/behandeling/follow-up) + - **Output:** Nieuwe afspraak in kalender + - **Interactie:** Modal form, patient zoeken, tijd kiezen + - **Demo-waarde:** "Kijk hoe snel dit gaat β€” patient zoeken, tijd kiezen, klaar" + +3. **Dubbele boeking waarschuwing** + - **Input:** Overlappende tijden voor zelfde behandelaar + - **Output:** Warning met overzicht bestaande afspraken + optie om toch in te plannen + - **Interactie:** Dialog met "Wijzigen" of "Toch inplannen" knoppen + - **Demo-waarde:** "Het systeem waarschuwt me, maar ik kan het overrulen β€” flexibiliteit!" + +4. **Afspraak verzetten & annuleren** + - **Input:** Drag-drop (desktop) of Edit button (mobile) + - **Output:** Gewijzigde tijd of status 'cancelled' + - **Interactie:** Confirm dialog bij grote wijzigingen + - **Demo-waarde:** "Even snel een afspraak verzetten, done" + +5. **Patient koppeling** + - **Input:** Patient search (naam/BSN) + - **Output:** Patient geselecteerd, afspraak gekoppeld + - **Interactie:** Autocomplete search in modal + - **Demo-waarde:** "Alle patiΓ«ntgegevens direct beschikbaar" + +6. **Afspraak ↔ Verslag koppeling (bidirectioneel)** + - **Input:** "Maak verslag" button in afspraak detail + - **Output:** Verslag editor met afspraak pre-filled + - **Interactie:** Click-through tussen afspraak en verslag + - **Demo-waarde:** **"DIT is waar AI echt helpt β€” afspraak gehad, 1 klik, verslag maken"** + +### Nice-to-Have (Stretch voor MVP) + +7. **(Stretch) Recent patients dropdown** + - Laatste 5 patiΓ«nten snel selecteren + - Demo-waarde: Extra snelheid voor repeat gebruikers + +8. **(Stretch) Color coding per type** + - Intake = blauw, Behandeling = groen, etc. + - Demo-waarde: Visueel overzicht + +9. **(Stretch) Mini calendar sidebar** + - Maandoverzicht met navigatie + - Demo-waarde: Extra polish, maar niet essentieel + +### Expliciet NIET in MVP + +- ❌ Recurring appointments (herhaling) +- ❌ Multi-practitioner view (agenda's van meerdere behandelaars) +- ❌ Email/SMS notificaties naar patiΓ«nten +- ❌ Patient portal (patiΓ«nten zelf afspraken maken) +- ❌ Wachtlijstbeheer +- ❌ Video call integratie + +--- + +## 4. Gebruikersflows (Demo- of MVP-flows) + +🎯 **Doel:** Laten zien hoe de gebruiker stap-voor-stap door het systeem gaat. + +### Flow 1: Nieuwe afspraak maken (Snelste pad) + +**Scenario:** Behandelaar krijgt patient aan de telefoon, wil snel afspraak inplannen + +1. **Start:** Behandelaar kijkt naar agenda (week view) +2. **Actie:** Klikt op lege tijdslot (bijv. donderdag 14:00) +3. **Systeem:** Modal opent met pre-filled datum/tijd +4. **Actie:** Typt patient naam in zoekbalk: "Jan de" +5. **Systeem:** Autocomplete toont "Jan de Vries (12-03-1985)" +6. **Actie:** Selecteert patient, kiest type "Behandeling", klikt "Opslaan" +7. **Systeem:** Afspraak verschijnt in kalender +8. **Resultaat:** Afspraak ingepland in ~10 seconden + +**Demo-tijd:** 15 seconden (met uitleg: 30 sec) + +### Flow 2: Afspraak maken met dubbele boeking + +**Scenario:** Behandelaar vergist zich, wil afspraak maken op al bezet tijdslot + +1. **Start:** Behandelaar klikt op tijdslot 14:00 (al bezet met andere patient) +2. **Systeem:** Modal opent, behandelaar vult patient "Maria Jansen" in +3. **Actie:** Klikt "Opslaan" +4. **Systeem:** ⚠️ Warning toont: "Let op: dubbele boeking β€” Je hebt al een afspraak: Jan de Vries - 14:00 (Intake)" +5. **Beslismoment:** Behandelaar ziet twee opties: + - "Wijzigen" β†’ terug naar form, andere tijd kiezen + - "Toch inplannen" β†’ override, beide afspraken staan er +6. **Actie:** Klikt "Wijzigen", kiest 15:00 +7. **Systeem:** Geen warning, afspraak wordt opgeslagen +8. **Resultaat:** Conflict voorkomen, afspraak correct ingepland + +**Demo-tijd:** 30 seconden (laat zien: systeem helpt je, maar dwingt niet af) + +### Flow 3: Afspraak β†’ Verslag (De AI-kracht) + +**Scenario:** Behandelaar heeft zojuist intake gehad, wil direct verslag maken + +1. **Start:** Behandelaar klikt op afspraak in kalender (net geweest: 10:00-11:00) +2. **Systeem:** Afspraak detail popup toont: + - Patient: Jan de Vries + - Type: Intakegesprek + - Notities: [leeg of korte aantekeningen] + - **Button: "πŸ“ Maak verslag"** +3. **Actie:** Klikt "Maak verslag" +4. **Systeem:** Navigeert naar Rapportage module met: + - Patient pre-filled: Jan de Vries + - Type pre-selected: Intake verslag + - Afspraak linked: 02-12-2024 10:00 +5. **Actie:** Behandelaar gebruikt AI-transcriptie of typt verslag +6. **Systeem:** AI genereert structuur, samenvatting, suggesties +7. **Actie:** Behandelaar bewerkt, klikt "Opslaan" +8. **Systeem:** Verslag opgeslagen + automatisch gekoppeld aan afspraak +9. **Resultaat:** In agenda: afspraak toont nu πŸ“ icon (verslag aanwezig) + +**Demo-tijd:** 45 seconden (zonder verslag schrijven, alleen workflow tonen) + +**Demo-impact:** πŸ”₯ **DIT is de killer feature** β€” naadloze integratie afspraak ↔ verslag ↔ AI + +### Flow 4: Afspraak verzetten (Desktop drag-drop) + +**Scenario:** Patient belt, kan niet op dinsdag, moet naar woensdag + +1. **Start:** Behandelaar ziet afspraak op dinsdag 14:00 +2. **Actie:** Klikt en houdt vast op afspraak card +3. **Systeem:** Afspraak wordt "draggable", cursor changes +4. **Actie:** Sleept naar woensdag 15:00 tijdslot +5. **Systeem:** Confirm dialog: "Afspraak verzetten naar wo 03-12 15:00?" +6. **Actie:** Klikt "Ja, verplaatsen" +7. **Systeem:** Afspraak verplaatst, conflict check uitgevoerd (geen warning) +8. **Resultaat:** Afspraak staat nu op woensdag 15:00 + +**Demo-tijd:** 10 seconden (visual impressive) + +--- + +## 5. Niet in Scope + +🎯 **Doel:** Duidelijk maken wat (nog) niet wordt gebouwd. + +### Features die NIET in MVP zitten + +**1. Multi-user & Rechtenbeheer** +- Geen rollen (admin/behandelaar/assistent) +- Geen gebruikersbeheer (toevoegen/verwijderen users) +- Geen rechten per patient/afspraak +- **Reden:** MVP is single-user demo, complexiteit te hoog voor Speedrun + +**2. Patient Portal & Self-Service** +- PatiΓ«nten kunnen niet zelf afspraken maken +- Geen patient login +- Geen afspraak bevestigingen/herinneringen via email/SMS +- **Reden:** Focus ligt op behandelaar workflow, patient side is apart product + +**3. Advanced Scheduling** +- Geen recurring appointments (wekelijks, maandelijks) +- Geen wachtlijstbeheer +- Geen automatische slot-filling +- Geen "find first available" functie +- **Reden:** Te complex voor MVP, weinig demo-waarde + +**4. External Integrations** +- Geen Google Calendar sync +- Geen Outlook sync +- Geen Teams integratie +- Geen iCal export +- **Reden:** Integraties vragen maanden werk, geen core functionaliteit + +**5. Advanced Analytics** +- Geen no-show tracking +- Geen bezettingsgraad dashboard +- Geen revenue/billing integratie +- **Reden:** Dit is post-MVP "nice to have" + +**6. Video Call Features** +- Geen ingebouwde video calling +- Geen Zoom/Teams meeting links automatisch genereren +- **Reden:** Complex, weinig meerwaarde voor demo + +**7. Production-Grade Features** +- Geen audit logging (wie heeft wat gewijzigd) +- Geen versioning van afspraken +- Geen backup/restore functionaliteit +- Geen advanced error recovery +- **Reden:** MVP is demo, niet productie-ready + +### Waarom deze keuzes? + +**Principe:** Focus op **snelheid** en **demo-impact**, niet op completeness. + +De agenda module moet: +βœ… Werken voor 1 behandelaar +βœ… Integreren met bestaande intake/rapportage flows +βœ… Laten zien dat AI naadloos past in dagelijkse workflow +βœ… Indruk maken: "Dit is geen speeltje, dit werkt Γ©cht" + +**Niet:** +❌ Alle edge cases afvangen +❌ Multi-tenant ready zijn +❌ Productie-grade security/performance +❌ Feature parity met legacy systemen + +--- + +## 6. Succescriteria + +🎯 **Doel:** Objectieve meetlat voor een geslaagde oplevering. + +### Demo Succescriteria + +**1. Demo Flow Compleetheid** +- [ ] Volledige flow 1-4 werkend zonder bugs +- [ ] Demo kan in **< 5 minuten** worden gedaan (inclusief uitleg) +- [ ] Geen "let's pretend" moments β€” alles werkt Γ©cht +- [ ] Mobile view werkt (backup als desktop faalt) + +**2. AI Integratie Showcase** +- [ ] Afspraak β†’ Verslag flow werkt in **< 3 clicks** +- [ ] AI-generated content zichtbaar in verslag +- [ ] Bidirectionele link werkt (afspraak ↔ verslag beide kanten) + +**3. Gebruikerservaring** +- [ ] Conflict warning toont correcte info (patient naam, tijd) +- [ ] Patient search werkt met **< 500ms response time** +- [ ] Drag-drop feels responsive (< 100ms visual feedback) +- [ ] Geen UI glitches op demo tijdstip + +**4. Technische Stabiliteit** +- [ ] Geen crashes tijdens demo flow +- [ ] Database queries < 2 seconden +- [ ] Timezone handling correct (NL-tijd in UI, UTC in DB) + +### LinkedIn Content Succescriteria + +**5. Viral Potential** +- [ ] Screenshot-worthy moments (bijv. conflict warning, AI-verslag) +- [ ] "Build in public" metrics: X story points, Y hours, €Z kosten +- [ ] Quote-worthy moment: "4 weken, €50/maand, volledig werkend EPD" + +**6. Geloofwaardigheid** +- [ ] Live demo URL werkt (vercel deployment) +- [ ] Code op GitHub (public repo) +- [ ] Geen fake data in screenshots (echte-looking test data) + +### Post-Demo Validatie + +**7. User Feedback** +- [ ] Minimaal 1 zorgprofessional test het live +- [ ] Feedback verzameld: "Zou je dit gebruiken?" +- [ ] Top 3 missing features geΓ―dentificeerd + +**8. Technical Learnings** +- [ ] AI prompt quality documented (wat werkte, wat niet) +- [ ] Performance bottlenecks geΓ―dentificeerd +- [ ] Timezone edge cases getest (zomertijd transitie) + +### Kwantitatieve Metrics + +| Metric | Target | Stretch | +|--------|--------|---------| +| **Demo tijd** | < 5 min | < 3 min | +| **Page load tijd** | < 2s | < 1s | +| **Afspraak maken** | < 10 sec | < 5 sec | +| **Patient search response** | < 500ms | < 200ms | +| **LinkedIn impressions** | 5,000 | 10,000 | +| **Engagement rate** | 2% | 5% | +| **Inbound leads** | 2 | 5 | + +--- + +## 7. Risico's & Mitigatie + +🎯 **Doel:** Risico's vroeg signaleren en plannen hoe ermee om te gaan. + +### Technische Risico's + +| Risico | Kans | Impact | Mitigatie | +|--------|------|--------|-----------| +| **FullCalendar styling conflicts** | Middel | Middel | Prototype early (week 1), CSS isolation strategy, fallback naar simpeler calendar | +| **Timezone bugs** | Middel | Hoog | Test zomertijd transitie (29 maart), gebruik date-fns-tz, TIMESTAMPTZ in DB | +| **Performance bij veel afspraken** | Laag | Middel | Query optimization met indices, date range filtering, pagination if needed | +| **Drag-drop mobile UX** | Hoog | Laag | Accept limitation: desktop-only drag, mobile uses Edit button | +| **Conflict detection edge cases** | Middel | Middel | Document assumptions, test concurrent edits, graceful degradation | + +### Scope & Planning Risico's + +| Risico | Kans | Impact | Mitigatie | +|--------|------|--------|-----------| +| **Scope creep** | Hoog | Hoog | **Hard blocker:** PRD is frozen. Extra features β†’ post-MVP backlog | +| **Underestimated complexity** | Middel | Hoog | Buffer 20% op story points, drop stretch features first | +| **Demo prep time onderschat** | Middel | Middel | Test demo flow 2 dagen voor launch, record backup video | +| **Integration breaking changes** | Laag | Hoog | Freeze intake/rapportage modules week voor agenda start | + +### Demo & Marketing Risico's + +| Risico | Kans | Impact | Mitigatie | +|--------|------|--------|-----------| +| **Live demo fails** | Middel | Hoog | Pre-record video backup, test deployment morning-of, have fallback slides | +| **LinkedIn post flops** | Middel | Middel | A/B test headlines, post at optimal time (dinsdag 9:00), engage in comments | +| **Negative feedback** | Laag | Middel | Respond professionally, use as learning, "building in public = warts and all" | +| **Competitor copies idea** | Laag | Laag | Accept risk, speed-to-market is advantage, open source = marketing | + +### Data & Privacy Risico's + +| Risico | Kans | Impact | Mitigatie | +|--------|------|--------|-----------| +| **Demo data looks fake** | Middel | Middel | Use realistic names/dates, Nederlandse context, avoid "Test User 1" | +| **Accidentally expose PII** | Laag | Hoog | **Only use fake data**, sanitize screenshots, no real BSN/medical info | +| **GDPR concerns** | Laag | Middel | Demo data only, privacy policy placeholder, mention "demo purposes" | + +### Mitigation Strategies + +**1. Technical Blockers (FullCalendar, Timezone)** +- **Week 1 Action:** Spike FullCalendar integration, confirm it works +- **Fallback:** If FullCalendar too complex β†’ simple custom grid view (bare minimum) +- **Test Plan:** Timezone edge case tests (E5.S3) before launch + +**2. Scope Management** +- **Weekly Check:** "Are we building MVP or nice-to-have?" +- **Decision Rule:** "Does this feature appear in demo flow? No β†’ backlog" +- **Communication:** Update Mission Control daily with scope decisions + +**3. Demo Insurance** +- **T-2 days:** Full dry-run with fresh browser +- **T-1 day:** Record backup video (Plan B) +- **Demo day:** Local fallback if Vercel down, screenshots ready + +**4. Marketing Hedge** +- **Content Calendar:** 3 LinkedIn posts drafted in advance +- **Engagement Plan:** Reply to every comment within 1 hour +- **Narrative Pivot:** If build too fast β†’ "How I did it", if too slow β†’ "Lessons learned" + +--- + +## 8. Roadmap / Vervolg (Post-MVP) + +🎯 **Doel:** Richting geven aan toekomstige uitbreidingen. + +### Fase 1: MVP Launch (Week 1-2 van Agenda Build) +- βœ… Basic calendar views (dag/week/werkdagen) +- βœ… Afspraak CRUD (make, edit, cancel) +- βœ… Conflict detection met override +- βœ… Afspraak ↔ Verslag koppeling +- βœ… Patient search + +**Output:** Demo-ready agenda voor AI Speedrun showcase + +--- + +### Fase 2: Production Readiness (Post-Speedrun, Week +4-8) + +**Prioriteit: Hoog** (als klant interesse toont) + +1. **Multi-User Support** + - Rollenbeheer (admin, behandelaar, assistent) + - User management (toevoegen/verwijderen) + - Per-user agenda view + - **Business Value:** Nodig voor praktijken met >1 behandelaar + +2. **Security Hardening** + - Row Level Security (RLS) policies + - Audit logging (wie deed wat wanneer) + - Session management + - **Business Value:** Compliance voor echte zorgdata + +3. **Performance Optimization** + - Query optimization + - Caching strategie + - Pagination voor grote datasets + - **Business Value:** Schaalbaar naar 100+ afspraken/dag + +4. **Error Handling & Monitoring** + - Sentry integratie + - Graceful degradation + - Retry mechanisms + - **Business Value:** Productie-grade stabiliteit + +**Effort:** ~3-4 weken (1 FTE) +**Business Case:** Nodig voor betalende klanten + +--- + +### Fase 3: Feature Parity (Month 3-4) + +**Prioriteit: Middel** (nice-to-have, niet blocker) + +1. **Advanced Scheduling** + - Recurring appointments (wekelijks/maandelijks) + - Wachtlijstbeheer + - "Find first available" slot + - **Business Value:** Efficiency boost voor drukke praktijken + +2. **Patient Communications** + - Email herinneringen (24u voor afspraak) + - SMS notificaties (optioneel) + - Cancellation confirmations + - **Business Value:** Reduce no-shows (vaak 10-20% in GGZ) + +3. **Multi-Practitioner View** + - Agenda van meerdere behandelaars naast elkaar + - Resource allocation (kamers, apparatuur) + - Team scheduling + - **Business Value:** Voor grotere praktijken/instellingen + +4. **Reporting & Analytics** + - Bezettingsgraad dashboard + - No-show tracking + - Revenue per practitioner (indien billing) + - **Business Value:** Management insights + +**Effort:** ~4-6 weken (1 FTE) +**Business Case:** Upsell opportunity, premium features + +--- + +### Fase 4: Ecosystem Integration (Month 5-6) + +**Prioriteit: Laag** (tenzij strategische partnerships) + +1. **Calendar Sync** + - Google Calendar sync (two-way) + - Outlook sync + - iCal export + - **Business Value:** Reduce double-entry, increase adoption + +2. **Video Call Integration** + - Zoom meeting auto-generation + - Teams meeting links + - In-browser video (WebRTC) + - **Business Value:** COVID-era feature, online consulten + +3. **External System Integration** + - PinkRoccade koppeling (indien klant vraagt) + - Nedap ECD import/export + - Zorgdomein connectie + - **Business Value:** Migration path van legacy systemen + +**Effort:** ~6-12 weken (1 FTE) +**Business Case:** Vereist voor enterprise deals + +--- + +### Fase 5: Patient Self-Service (Month 7-9) + +**Prioriteit: Hoog** (maar separate product) + +1. **Patient Portal** + - Inloggen met DigiD + - Eigen afspraken bekijken + - Afspraken maken (binnen beschikbare slots) + - Afspraken annuleren (met policy) + - **Business Value:** Reduce admin workload, 24/7 booking + +2. **Patient Experience** + - Intake formulieren online invullen + - Documenten uploaden (verwijsbrieven, etc.) + - Vragenlijsten invullen (ROM, etc.) + - **Business Value:** Efficiency, patient empowerment + +**Effort:** ~8-12 weken (1 FTE) +**Business Case:** Dit kan separate SaaS product zijn + +--- + +### Strategic Considerations + +**Build vs. Integrate Decision Tree:** + +``` +Feature request + β”‚ + β”œβ”€ Is it core to EPD? + β”‚ β”œβ”€ Yes β†’ Build it + β”‚ └─ No β†’ Can we integrate? + β”‚ β”œβ”€ Yes β†’ Use API/webhook + β”‚ └─ No β†’ Deprioritize + β”‚ + └─ Does it differentiate us? + β”œβ”€ Yes β†’ Build it (competitive advantage) + └─ No β†’ Buy/integrate (focus on AI) +``` + +**Example:** +- Video calling β†’ **Integrate** (Zoom API) +- AI-powered scheduling β†’ **Build** (our secret sauce) +- Billing/invoicing β†’ **Integrate** (Mollie, etc.) +- Treatment plan generation β†’ **Build** (core AI value) + +--- + +### Parking Lot (Ideas, geen commitment) + +- AI-powered "optimal scheduling" (ML-based slot recommendations) +- Predictive no-show scoring (AI flags high-risk appointments) +- Voice-controlled agenda ("Schedule Jan de Vries for Thursday 2pm") +- WhatsApp integration for reminders +- Apple Health / Google Fit data import (for lifestyle coaching) +- Group session management (multiple patients per appointment) + +**Criteria for moving from Parking Lot β†’ Roadmap:** +1. Customer explicitly asks for it (at least 3 requests) +2. Clear business case (revenue, retention, or viral potential) +3. Feasible effort (< 4 weeks build time) + +--- + +## 9. Bijlagen & Referenties + +🎯 **Doel:** Bronnen koppelen voor context en consistentie. + +### Interne Documenten + +**Strategisch:** +- [AI Speedrun Manifesto](link) β€” Overall projectvisie +- [Mission Control v1](link) β€” Project status tracking +- [Build Metrics Tracking](link) β€” Transparante build log + +**Functioneel:** +- FO Mini-EPD v2.0 β€” Volledige functionele specificatie +- TO Mini-EPD v1.2 β€” Technische architectuur +- PRD Intake Module v1 β€” Hoe intake werkt (dependency) +- PRD Rapportage Module v1 β€” Hoe verslagen werken (integration point) + +**Technisch:** +- Bouwplan Agenda Module v1.1 β€” Gedetailleerde technische implementatie (dit volgt nΓ‘ PRD approval) +- API Access Mini-ECD β€” AI integratie documentatie +- Database Schema FHIR GGZ β€” Data model + +**UX/UI:** +- UX Stylesheet β€” Design system & component library +- Interface Design Plan β€” Wireframes en user flows +- Mocks UI Flow β€” Visual designs + +### Externe Referenties + +**Standards & Compliance:** +- [FHIR R4 Encounter Resource](https://www.hl7.org/fhir/encounter.html) β€” Healthcare data standard +- [ISO 8601 DateTime](https://en.wikipedia.org/wiki/ISO_8601) β€” Date/time formatting +- [IANA Time Zones](https://www.iana.org/time-zones) β€” Timezone database + +**Technology:** +- [FullCalendar Documentation](https://fullcalendar.io/docs) β€” Calendar library we'll use +- [shadcn/ui Components](https://ui.shadcn.com/docs/components) β€” UI component library +- [Supabase Auth](https://supabase.com/docs/guides/auth) β€” Authentication +- [date-fns](https://date-fns.org/) β€” Date manipulation library + +**Inspiration & Benchmarks:** +- [Cal.com](https://cal.com) β€” Open-source scheduling (UI inspiration) +- [Calendly](https://calendly.com) β€” Modern scheduling UX (simplicity benchmark) +- [Acuity Scheduling](https://acuityscheduling.com) β€” Healthcare scheduling features +- Google Calendar β€” De facto standard, users expect this UX + +### Competitive Analysis (Legacy GGZ Systems) + +**Why we're better:** + +| Feature | PinkRoccade | Nedap ECD | Zorgdomein | **Mini-EPD** | +|---------|-------------|-----------|------------|--------------| +| Afspraak maken | 5+ clicks, slow | 4-5 clicks | 6+ clicks | **2-3 clicks** | +| Dubbele boeking | Hard block | Warning | Hard block | **Warning + override** | +| Afspraak ↔ Verslag | Manual link | No link | Manual link | **1-click bidirectioneel** | +| AI-assisted verslag | ❌ | ❌ | ❌ | **βœ… Core feature** | +| Mobile UX | Terrible | Desktop-only | Barely works | **Responsive + optimized** | +| Load time | 5-10s | 3-5s | 4-8s | **< 2s** | +| Training tijd | 2-4 dagen | 1-2 dagen | 2-3 dagen | **< 10 minuten** | +| Kosten | €200-400/maand | €150-300/maand | €180-350/maand | **€50/maand** | + +**Key Differentiators:** +1. πŸš€ **Speed:** Zowel UX als development speed +2. πŸ€– **AI Integration:** Naadloos, niet als afterthought +3. πŸ’° **Price:** 1/4e van legacy systemen +4. 🎯 **Focus:** Alleen wat je nodig hebt, geen bloat + +--- + +### Contact & Feedback + +**Product Owner:** Colin +**Email:** [email] +**LinkedIn:** [profile] +**GitHub Repo:** [link to public repo] + +**Feedback Welcome:** +- πŸ’¬ DM op LinkedIn +- πŸ› GitHub Issues voor bugs +- πŸ’‘ Feature requests via Google Form [link] + +--- + +**Document Status:** βœ… Approved +**Next Step:** β†’ Bouwplan Agenda Module v1.1 (technische uitwerking) + +**Changelog:** + +| Versie | Datum | Auteur | Wijziging | +|--------|-------|--------|-----------| +| v1.0 | 02-12-2024 | Colin | InitiΓ«le PRD β€” scope, flows, success criteria gedefinieerd | diff --git a/docs/specs/agenda/to-agenda-module-v1_0.md b/docs/specs/agenda/to-agenda-module-v1_0.md new file mode 100644 index 0000000..d0937fa --- /dev/null +++ b/docs/specs/agenda/to-agenda-module-v1_0.md @@ -0,0 +1,1778 @@ +# βš™οΈ Technisch Ontwerp (TO) – Agenda Module + +**Projectnaam:** Mini-EPD Agenda Module +**Versie:** v1.0 +**Datum:** 02-12-2024 +**Auteur:** Colin + +--- + +## 1. Doel en relatie met PRD en FO + +🎯 **Doel van dit document:** +Het Technisch Ontwerp beschrijft **hoe** de Agenda Module technisch wordt geΓ―mplementeerd. Dit document vertaalt de functionele specificaties uit het FO naar concrete technische oplossingen, architectuur en implementatiedetails. + +πŸ“˜ **Document hiΓ«rarchie:** +``` +PRD (Wat & Waarom) β†’ FO (Hoe functioneel) β†’ TO (Hoe technisch) +``` + +**Referenties:** +- PRD: `prd-agenda-module-v1_0.md` β€” Scope, user stories, success criteria +- FO: `fo-agenda-module-v1_0.md` β€” Functionele werking, UX, flows +- Bestaande codebase: `/home/colin/development/15-mini-epd-prototype` + +**Scope:** +- MVP implementatie van agenda module +- Integratie met bestaande EPD modules (patients, rapportage) +- FullCalendar integratie +- FHIR-compliant data model (Encounter resource) + +--- + +## 2. Technische Architectuur Overzicht + +🎯 **Doel:** Globaal beeld van de systeemarchitectuur. + +### 2.1 High-Level Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ FRONTEND β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Next.js 14 (App Router) β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ EPD Layout β”‚ β”‚ Agenda β”‚ β”‚ Shared Components β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ + Sidebar β”‚ β”‚ Module β”‚ β”‚ (shadcn/ui, forms) β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ FullCalendarβ”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ React β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ API Layer β”‚ + β”‚ (Next.js API Routes β”‚ + β”‚ + Server Actions) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Supabase β”‚ β”‚ Supabase β”‚ β”‚ Deepgram β”‚ +β”‚ PostgreSQL β”‚ β”‚ Auth β”‚ β”‚ (Audioβ†’Text) β”‚ +β”‚ (Data) β”‚ β”‚ (Identity) β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 2.2 Agenda Module Architecture + +``` +app/epd/agenda/ +β”œβ”€β”€ page.tsx # Server Component - data fetching +β”œβ”€β”€ layout.tsx # Optional: agenda-specific layout +β”œβ”€β”€ loading.tsx # Suspense fallback +β”œβ”€β”€ error.tsx # Error boundary +β”‚ +β”œβ”€β”€ components/ +β”‚ β”œβ”€β”€ calendar-view.tsx # FullCalendar wrapper (Client) +β”‚ β”œβ”€β”€ calendar-toolbar.tsx # Navigation, view switcher +β”‚ β”œβ”€β”€ appointment-modal.tsx # Create/Edit form (Client) +β”‚ β”œβ”€β”€ appointment-detail.tsx # Popup/popover detail view +β”‚ β”œβ”€β”€ conflict-dialog.tsx # Double booking warning +β”‚ β”œβ”€β”€ patient-search.tsx # Autocomplete combobox +β”‚ └── appointment-card.tsx # Event rendering in calendar +β”‚ +β”œβ”€β”€ hooks/ +β”‚ β”œβ”€β”€ use-appointments.ts # Data fetching hook (SWR/React Query) +β”‚ β”œβ”€β”€ use-appointment-mutations.ts # Create/Update/Delete +β”‚ └── use-conflict-check.ts # Real-time conflict detection +β”‚ +β”œβ”€β”€ lib/ +β”‚ β”œβ”€β”€ calendar-config.ts # FullCalendar configuration +β”‚ β”œβ”€β”€ appointment-utils.ts # Date/time helpers +β”‚ └── conflict-detection.ts # Overlap logic +β”‚ +β”œβ”€β”€ actions.ts # Server Actions (mutations) +└── types.ts # Module-specific types +``` + +### 2.3 Data Flow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CREATE APPOINTMENT FLOW β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ 1. User clicks time slot β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ 2. Modal opens (Client Component) β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ 3. User fills form + patient search β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€β–Ί Patient Search API (/api/patients/search) β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ +β”‚ β”‚ Supabase Query (patients table) β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ +β”‚ β”‚ Return patient list β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ 4. User clicks "Save" β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ 5. Client-side validation (Zod) β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ 6. Conflict check (Server Action) β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€β–Ί No conflict ──► Create appointment β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ +β”‚ β”‚ Supabase INSERT (encounters) β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ +β”‚ β”‚ Optimistic UI update β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ +β”‚ β”‚ Toast: "Afspraak opgeslagen" β”‚ +β”‚ β”‚ β”‚ +β”‚ └──► Conflict ──► Show Conflict Dialog β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€β–Ί "Wijzigen" ──► Back to form β”‚ +β”‚ β”‚ β”‚ +β”‚ └──► "Toch inplannen" ──► Create with flag β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 3. Techstack Selectie + +🎯 **Doel:** Onderbouwde keuze van technologieΓ«n. + +### 3.1 Core Stack (Bestaand) + +| Component | Technologie | Versie | Argumentatie | +|-----------|-------------|--------|--------------| +| **Framework** | Next.js | 14.2.18 | Bestaande codebase, App Router, Server Components | +| **Runtime** | React | 18.3.1 | Bestaande codebase | +| **Language** | TypeScript | ^5 | Type safety, bestaande patterns | +| **Database** | Supabase PostgreSQL | - | Bestaande infra, RLS, realtime | +| **Auth** | Supabase Auth | ^0.7.0 | Bestaande implementatie | +| **Styling** | TailwindCSS | ^3.4.18 | Bestaande design system | +| **UI Components** | shadcn/ui | - | Bestaande component library | +| **Forms** | React Hook Form | ^7.66.1 | Bestaande pattern | +| **Validation** | Zod | ^4.1.12 | Bestaande pattern | +| **Date Handling** | date-fns | ^4.1.0 | Bestaande utility | + +### 3.2 Nieuwe Dependencies (Agenda-specifiek) + +| Component | Technologie | Versie | Argumentatie | Alternatieven | +|-----------|-------------|--------|--------------|---------------| +| **Calendar** | FullCalendar | ^6.1.x | Feature-rijk, React support, drag-drop | react-big-calendar, custom | +| **FC Core** | @fullcalendar/core | ^6.1.x | Base package | - | +| **FC React** | @fullcalendar/react | ^6.1.x | React wrapper | - | +| **FC DayGrid** | @fullcalendar/daygrid | ^6.1.x | Month/week grid view | - | +| **FC TimeGrid** | @fullcalendar/timegrid | ^6.1.x | Day/week time view | - | +| **FC Interaction** | @fullcalendar/interaction | ^6.1.x | Drag-drop, resize | - | +| **Data Fetching** | SWR | ^2.x | Caching, revalidation, optimistic | React Query | + +### 3.3 Package Installation + +```bash +# FullCalendar packages +pnpm add @fullcalendar/core @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction + +# Data fetching (optional, can use existing pattern) +pnpm add swr +``` + +### 3.4 Techstack Decision: FullCalendar + +**Waarom FullCalendar:** +- βœ… Mature library (10+ jaar development) +- βœ… Comprehensive API voor alle agenda features +- βœ… Built-in drag-drop met event resizing +- βœ… Mobile-responsive out of the box +- βœ… Goede TypeScript support +- βœ… Styling customizable via CSS variables +- βœ… Grote community, veel voorbeelden + +**Risico's & Mitigatie:** +| Risico | Mitigatie | +|--------|-----------| +| Styling conflicts met Tailwind | CSS isolation, custom CSS variables | +| Bundle size (~200KB) | Tree-shaking, alleen noodzakelijke plugins | +| Learning curve | Spike in week 1, prototype first | + +--- + +## 4. Datamodel + +🎯 **Doel:** Structuur van de data in de database. + +### 4.1 Bestaande Tabel: `encounters` + +De `encounters` tabel bestaat al en is FHIR-compliant. We gebruiken deze voor afspraken. + +```sql +-- Bestaande tabel structuur (uit database.types.ts) +CREATE TABLE encounters ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Relaties + patient_id UUID NOT NULL REFERENCES patients(patient_id), + practitioner_id UUID REFERENCES practitioners(practitioner_id), + intake_id UUID REFERENCES intakes(id), + intake_note_id UUID REFERENCES intake_notes(id), + organization_id UUID REFERENCES organizations(id), + + -- Timing (FHIR Period) + period_start TIMESTAMPTZ NOT NULL, + period_end TIMESTAMPTZ, + + -- Classification (FHIR Coding) + class_code TEXT, -- 'AMB' (ambulatory), 'VR' (virtual) + class_display TEXT, + type_code TEXT, -- 'intake', 'treatment', 'followup' + type_display TEXT, -- 'Intakegesprek', 'Behandeling', 'Follow-up' + + -- Status (FHIR enum) + status encounter_status NOT NULL DEFAULT 'planned', + -- Values: planned, in-progress, on-hold, completed, cancelled (actual DB enum) + + -- Priority (optional) + priority_code TEXT, + priority_display TEXT, + + -- Reason (array for multiple reasons) + reason_code TEXT[], + reason_display TEXT[], + + -- Notes + notes TEXT, + + -- Audit + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + identifier TEXT -- External identifier if needed +); + +-- Indexes for performance +CREATE INDEX idx_encounters_patient ON encounters(patient_id); +CREATE INDEX idx_encounters_practitioner ON encounters(practitioner_id); +CREATE INDEX idx_encounters_period ON encounters(period_start, period_end); +CREATE INDEX idx_encounters_status ON encounters(status); +``` + +### 4.2 Nieuw: Encounter-Report Linking (E0 Migration) + +```sql +-- Migration: Add encounter_id to reports table +ALTER TABLE reports +ADD COLUMN encounter_id UUID REFERENCES encounters(id); + +-- Index for quick lookups +CREATE INDEX idx_reports_encounter ON reports(encounter_id); + +-- Optional: Backfill existing reports based on matching dates/patients +-- (Post-MVP task) +``` + +### 4.3 Type Definitions + +```typescript +// lib/types/encounter.ts + +import { z } from 'zod'; +import type { Database } from '@/lib/supabase/database.types'; + +// Base types from database +export type Encounter = Database['public']['Tables']['encounters']['Row']; +export type EncounterInsert = Database['public']['Tables']['encounters']['Insert']; +export type EncounterUpdate = Database['public']['Tables']['encounters']['Update']; +export type EncounterStatus = Database['public']['Enums']['encounter_status']; + +// Appointment types (subset of FHIR encounter types for GGZ) +export const AppointmentType = { + INTAKE: 'intake', + TREATMENT: 'treatment', + FOLLOWUP: 'followup', +} as const; + +export type AppointmentTypeValue = typeof AppointmentType[keyof typeof AppointmentType]; + +export const AppointmentTypeDisplay: Record = { + intake: 'Intakegesprek', + treatment: 'Behandeling', + followup: 'Follow-up', +}; + +export const AppointmentTypeDuration: Record = { + intake: 60, + treatment: 45, + followup: 30, +}; + +// Zod Schemas for validation +export const CreateAppointmentSchema = z.object({ + patient_id: z.string().uuid('Patient ID moet een geldige UUID zijn'), + period_start: z.string().datetime('Ongeldige datum/tijd'), + duration_minutes: z.number().min(15).max(120), + type_code: z.enum(['intake', 'treatment', 'followup']), + notes: z.string().max(500).optional(), +}); + +export const UpdateAppointmentSchema = CreateAppointmentSchema.partial().extend({ + id: z.string().uuid(), + status: z.enum(['planned', 'in-progress', 'on-hold', 'completed', 'cancelled']).optional(), +}); + +export type CreateAppointmentInput = z.infer; +export type UpdateAppointmentInput = z.infer; + +// Extended type with relations +export interface AppointmentWithPatient extends Encounter { + patient: { + patient_id: string; + name_family: string; + name_given: string[]; + birth_date: string; + gender: string; + }; + reports?: { + id: string; + created_at: string; + }[]; +} + +// FullCalendar event type +export interface CalendarEvent { + id: string; + title: string; + start: Date; + end: Date; + extendedProps: { + patient_id: string; + patient_name: string; + type_code: AppointmentTypeValue; + type_display: string; + status: EncounterStatus; + notes?: string; + has_report: boolean; + }; + backgroundColor?: string; + borderColor?: string; + classNames?: string[]; +} +``` + +### 4.4 Entity Relationship Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ practitioners β”‚ β”‚ encounters β”‚ β”‚ patients β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ practitioner_id │◄──────│ practitioner_id β”‚ β”‚ patient_id β”‚ +β”‚ name_family β”‚ 1:N β”‚ patient_id │──────►│ name_family β”‚ +β”‚ name_given[] β”‚ β”‚ period_start β”‚ N:1 β”‚ name_given[] β”‚ +β”‚ ... β”‚ β”‚ period_end β”‚ β”‚ birth_date β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ type_code β”‚ β”‚ gender β”‚ + β”‚ status β”‚ β”‚ ... β”‚ + β”‚ notes β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ ... β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ 1:N (optional) + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ reports β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ id β”‚ + β”‚ encounter_id β”‚ ◄── NEW (E0 migration) + β”‚ patient_id β”‚ + β”‚ type β”‚ + β”‚ content β”‚ + β”‚ ... β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 5. API Ontwerp + +🎯 **Doel:** Overzicht van endpoints en server actions. + +### 5.1 API Routes + +| Endpoint | Method | Input | Output | Auth | Gebruik | +|----------|--------|-------|--------|------|---------| +| `/api/appointments` | GET | `?start=&end=&practitioner_id=` | `Appointment[]` | Required | Kalender data laden | +| `/api/appointments` | POST | `CreateAppointmentInput` | `Appointment` | Required | Nieuwe afspraak | +| `/api/appointments/[id]` | GET | - | `AppointmentWithPatient` | Required | Detail ophalen | +| `/api/appointments/[id]` | PATCH | `UpdateAppointmentInput` | `Appointment` | Required | Afspraak wijzigen | +| `/api/appointments/[id]` | DELETE | - | `{ success: true }` | Required | Soft delete (status=cancelled) | +| `/api/appointments/conflicts` | POST | `{ start, end, exclude_id? }` | `Appointment[]` | Required | Conflict check | +| `/api/patients/search` | GET | `?q=&limit=` | `Patient[]` | Required | Patient autocomplete | + +### 5.2 Server Actions (Preferred for Mutations) + +```typescript +// app/epd/agenda/actions.ts +'use server'; + +import { revalidatePath } from 'next/cache'; +import { createClient } from '@/lib/auth/server'; +import { CreateAppointmentSchema, UpdateAppointmentSchema } from './types'; +import type { CreateAppointmentInput, UpdateAppointmentInput } from './types'; + +export async function createAppointment(input: CreateAppointmentInput) { + // 1. Validate input + const validated = CreateAppointmentSchema.parse(input); + + // 2. Get authenticated user + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) throw new Error('Niet ingelogd'); + + // 3. Get practitioner_id from user + const { data: practitioner } = await supabase + .from('practitioners') + .select('practitioner_id') + .eq('user_id', user.id) + .single(); + + // 4. Calculate period_end + const periodStart = new Date(validated.period_start); + const periodEnd = new Date(periodStart.getTime() + validated.duration_minutes * 60000); + + // 5. Insert encounter + const { data, error } = await supabase + .from('encounters') + .insert({ + patient_id: validated.patient_id, + practitioner_id: practitioner?.practitioner_id, + period_start: periodStart.toISOString(), + period_end: periodEnd.toISOString(), + type_code: validated.type_code, + type_display: AppointmentTypeDisplay[validated.type_code], + status: 'planned', + notes: validated.notes, + class_code: 'AMB', + class_display: 'ambulatory', + }) + .select() + .single(); + + if (error) throw error; + + // 6. Revalidate cache + revalidatePath('/epd/agenda'); + + return data; +} + +export async function updateAppointment(input: UpdateAppointmentInput) { + const validated = UpdateAppointmentSchema.parse(input); + const supabase = await createClient(); + + const updateData: Record = {}; + + if (validated.period_start) { + updateData.period_start = validated.period_start; + if (validated.duration_minutes) { + const start = new Date(validated.period_start); + updateData.period_end = new Date(start.getTime() + validated.duration_minutes * 60000).toISOString(); + } + } + + if (validated.type_code) { + updateData.type_code = validated.type_code; + updateData.type_display = AppointmentTypeDisplay[validated.type_code]; + } + + if (validated.status) updateData.status = validated.status; + if (validated.notes !== undefined) updateData.notes = validated.notes; + + updateData.updated_at = new Date().toISOString(); + + const { data, error } = await supabase + .from('encounters') + .update(updateData) + .eq('id', validated.id) + .select() + .single(); + + if (error) throw error; + + revalidatePath('/epd/agenda'); + return data; +} + +export async function cancelAppointment(id: string) { + const supabase = await createClient(); + + const { data, error } = await supabase + .from('encounters') + .update({ + status: 'cancelled', + updated_at: new Date().toISOString(), + }) + .eq('id', id) + .select() + .single(); + + if (error) throw error; + + revalidatePath('/epd/agenda'); + return data; +} + +export async function checkConflicts( + start: string, + end: string, + excludeId?: string +): Promise { + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) throw new Error('Niet ingelogd'); + + // Get practitioner_id + const { data: practitioner } = await supabase + .from('practitioners') + .select('practitioner_id') + .eq('user_id', user.id) + .single(); + + if (!practitioner) return []; + + // Query overlapping appointments + let query = supabase + .from('encounters') + .select('*') + .eq('practitioner_id', practitioner.practitioner_id) + .neq('status', 'cancelled') + .lt('period_start', end) + .gt('period_end', start); + + if (excludeId) { + query = query.neq('id', excludeId); + } + + const { data, error } = await query; + + if (error) throw error; + return data || []; +} +``` + +### 5.3 API Route Implementation + +```typescript +// app/api/appointments/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/auth/server'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const start = searchParams.get('start'); + const end = searchParams.get('end'); + + if (!start || !end) { + return NextResponse.json( + { error: 'start en end parameters zijn verplicht' }, + { status: 400 } + ); + } + + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + return NextResponse.json({ error: 'Niet ingelogd' }, { status: 401 }); + } + + // Get practitioner + const { data: practitioner } = await supabase + .from('practitioners') + .select('practitioner_id') + .eq('user_id', user.id) + .single(); + + // Fetch appointments with patient data + const { data, error } = await supabase + .from('encounters') + .select(` + *, + patient:patients(patient_id, name_family, name_given, birth_date, gender), + reports(id, created_at) + `) + .eq('practitioner_id', practitioner?.practitioner_id) + .gte('period_start', start) + .lte('period_start', end) + .neq('status', 'cancelled') + .order('period_start', { ascending: true }); + + if (error) throw error; + + return NextResponse.json({ appointments: data }); + } catch (error) { + console.error('Error fetching appointments:', error); + return NextResponse.json( + { error: 'Kon afspraken niet laden' }, + { status: 500 } + ); + } +} +``` + +### 5.4 Patient Search API + +```typescript +// app/api/patients/search/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/auth/server'; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const query = searchParams.get('q') || ''; + const limit = parseInt(searchParams.get('limit') || '10'); + + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + return NextResponse.json({ error: 'Niet ingelogd' }, { status: 401 }); + } + + // Build search query + let dbQuery = supabase + .from('patients') + .select('patient_id, name_family, name_given, birth_date, gender') + .limit(limit); + + if (query.length >= 2) { + // Search on name (case-insensitive) + dbQuery = dbQuery.or(`name_family.ilike.%${query}%,name_given.cs.{${query}}`); + } + + const { data, error } = await dbQuery.order('name_family'); + + if (error) throw error; + + return NextResponse.json({ patients: data }); + } catch (error) { + console.error('Error searching patients:', error); + return NextResponse.json( + { error: 'Zoeken mislukt' }, + { status: 500 } + ); + } +} +``` + +--- + +## 6. Component Implementatie + +🎯 **Doel:** Technische details van UI componenten. + +### 6.1 Calendar View Component + +```typescript +// app/epd/agenda/components/calendar-view.tsx +'use client'; + +import { useRef, useCallback } from 'react'; +import FullCalendar from '@fullcalendar/react'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import type { EventClickArg, DateSelectArg, EventDropArg } from '@fullcalendar/core'; +import { nl } from 'date-fns/locale'; + +import { useAppointments } from '../hooks/use-appointments'; +import { useAppointmentMutations } from '../hooks/use-appointment-mutations'; +import { transformToCalendarEvents } from '../lib/appointment-utils'; +import { AppointmentModal } from './appointment-modal'; +import { AppointmentDetail } from './appointment-detail'; +import { CalendarToolbar } from './calendar-toolbar'; + +interface CalendarViewProps { + initialDate?: Date; + initialView?: 'timeGridDay' | 'timeGridWeek' | 'timeGridWorkWeek'; +} + +export function CalendarView({ + initialDate = new Date(), + initialView = 'timeGridWeek' +}: CalendarViewProps) { + const calendarRef = useRef(null); + + // Data fetching + const { appointments, isLoading, mutate } = useAppointments(); + const { updateAppointment } = useAppointmentMutations(); + + // State for modals + const [selectedSlot, setSelectedSlot] = useState(null); + const [selectedEvent, setSelectedEvent] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDetailOpen, setIsDetailOpen] = useState(false); + + // Transform appointments to FullCalendar events + const events = useMemo(() => + transformToCalendarEvents(appointments), + [appointments] + ); + + // Handlers + const handleDateSelect = useCallback((selectInfo: DateSelectArg) => { + setSelectedSlot(selectInfo); + setIsModalOpen(true); + }, []); + + const handleEventClick = useCallback((clickInfo: EventClickArg) => { + setSelectedEvent(clickInfo.event.toPlainObject() as CalendarEvent); + setIsDetailOpen(true); + }, []); + + const handleEventDrop = useCallback(async (dropInfo: EventDropArg) => { + const { event, revert } = dropInfo; + + try { + await updateAppointment({ + id: event.id, + period_start: event.start!.toISOString(), + duration_minutes: (event.end!.getTime() - event.start!.getTime()) / 60000, + }); + mutate(); // Revalidate + } catch (error) { + revert(); // Rollback on error + toast.error('Kon afspraak niet verzetten'); + } + }, [updateAppointment, mutate]); + + // Navigation handlers for toolbar + const handlePrev = () => calendarRef.current?.getApi().prev(); + const handleNext = () => calendarRef.current?.getApi().next(); + const handleToday = () => calendarRef.current?.getApi().today(); + const handleViewChange = (view: string) => calendarRef.current?.getApi().changeView(view); + + return ( +
+ setIsModalOpen(true)} + /> + +
+ setIsLoading(loading)} + /> +
+ + {/* Modals */} + { + setIsModalOpen(false); + setSelectedSlot(null); + mutate(); + }} + /> + + { + setIsDetailOpen(false); + setIsModalOpen(true); + }} + onDelete={() => { + mutate(); + setIsDetailOpen(false); + }} + /> +
+ ); +} +``` + +### 6.2 Appointment Modal Component + +```typescript +// app/epd/agenda/components/appointment-modal.tsx +'use client'; + +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useState, useTransition, useEffect } from 'react'; +import { format } from 'date-fns'; +import { nl } from 'date-fns/locale'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; + +import { PatientSearch } from './patient-search'; +import { ConflictDialog } from './conflict-dialog'; +import { createAppointment, updateAppointment, checkConflicts } from '../actions'; +import { CreateAppointmentSchema, AppointmentType, AppointmentTypeDuration } from '../types'; + +interface AppointmentModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + defaultValues?: { + date?: Date; + startTime?: Date; + appointment?: Appointment; // For edit mode + }; + onSuccess: () => void; +} + +export function AppointmentModal({ + open, + onOpenChange, + defaultValues, + onSuccess, +}: AppointmentModalProps) { + const [isPending, startTransition] = useTransition(); + const [conflicts, setConflicts] = useState([]); + const [showConflictDialog, setShowConflictDialog] = useState(false); + const [showNotes, setShowNotes] = useState(false); + + const isEdit = !!defaultValues?.appointment; + + const form = useForm({ + resolver: zodResolver(CreateAppointmentSchema), + defaultValues: { + patient_id: defaultValues?.appointment?.patient_id || '', + period_start: defaultValues?.startTime?.toISOString() || + defaultValues?.date?.toISOString() || + new Date().toISOString(), + duration_minutes: defaultValues?.appointment + ? /* calculate from period_end - period_start */ 45 + : 45, + type_code: defaultValues?.appointment?.type_code || 'treatment', + notes: defaultValues?.appointment?.notes || '', + }, + }); + + // Auto-update duration when type changes + const typeCode = form.watch('type_code'); + useEffect(() => { + if (!isEdit) { + form.setValue('duration_minutes', AppointmentTypeDuration[typeCode as keyof typeof AppointmentTypeDuration]); + } + }, [typeCode, isEdit, form]); + + // Real-time conflict check + const periodStart = form.watch('period_start'); + const durationMinutes = form.watch('duration_minutes'); + + useEffect(() => { + if (periodStart && durationMinutes) { + const start = new Date(periodStart); + const end = new Date(start.getTime() + durationMinutes * 60000); + + checkConflicts( + start.toISOString(), + end.toISOString(), + defaultValues?.appointment?.id + ).then(setConflicts); + } + }, [periodStart, durationMinutes, defaultValues?.appointment?.id]); + + const onSubmit = async (data: CreateAppointmentInput) => { + // Check for conflicts before submit + if (conflicts.length > 0) { + setShowConflictDialog(true); + return; + } + + await saveAppointment(data); + }; + + const saveAppointment = async (data: CreateAppointmentInput, ignoreConflict = false) => { + startTransition(async () => { + try { + if (isEdit) { + await updateAppointment({ ...data, id: defaultValues.appointment!.id }); + } else { + await createAppointment(data); + } + onSuccess(); + toast.success(isEdit ? 'Afspraak gewijzigd' : 'Afspraak opgeslagen'); + } catch (error) { + toast.error('Er ging iets mis'); + } + }); + }; + + return ( + <> + + + + + {isEdit ? 'Afspraak bewerken' : 'Nieuwe afspraak'} + + + +
+ {/* Patient Search */} +
+ + form.setValue('patient_id', patientId)} + error={form.formState.errors.patient_id?.message} + /> +
+ + {/* Date & Time (inline) */} +
+
+ + +
+
+ + +
+
+ + {/* Inline conflict warning */} + {conflicts.length > 0 && ( +
+

+ ⚠️ Overlap met: {conflicts[0].patient?.name_family} {format(new Date(conflicts[0].period_start), 'HH:mm')} +

+
+ )} + + {/* Duration (radio buttons) */} +
+ + form.setValue('duration_minutes', Number(v))} + className="flex gap-4" + > + {[30, 45, 60, 90].map((min) => ( +
+ + +
+ ))} +
+
+ + {/* Type (radio buttons) */} +
+ + form.setValue('type_code', v)} + className="flex gap-4" + > +
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Notes (collapsible) */} + {!showNotes ? ( + + ) : ( +
+ +