Implementatieplan
Overzicht
Section titled “Overzicht”Dit plan beschrijft de exacte stappen om de Werkbon app te bouwen op de Beam stack. Elke stap verwijst naar concrete bestanden uit de Beam codebase die gekopieerd of aangepast worden.
Fase 0: Infra + regels ├─ Supabase project aanmaken + auth config ├─ Cloudflare setup (Workers, Pages, KV) ├─ DNS: app.acspascal.nl ├─ Sentry project + alerting → angelo.vaudo@gmail.com └─ Project regels (CLAUDE.md, .claude/rules/)
Fase 1: Fundament (week 1-2) ├─ Monorepo setup ├─ Supabase schema + migraties └─ API middleware stack
Fase 2: CRUD (week 3-4) ├─ Dashboard auth + layout ├─ Contacten CRUD └─ Adres autocomplete
Fase 3: Kernflow (week 5-7) ├─ Meldingen + triage ├─ Klussen + inplannen └─ Werkbon invullen (mobile-first)
Fase 4: Integraties (week 8) ├─ Incoming webhook (marketingsite) ├─ Outgoing webhooks (Moneybird via Queue) └─ Klant notificaties (email)
Fase 5: Migratie (week 9-10) ├─ Data migratie script ├─ Parallel draai └─ Cut-overFase 0: Infra & Regels
Section titled “Fase 0: Infra & Regels”0.1 Supabase Project
Section titled “0.1 Supabase Project”Nieuw eigen Supabase project aanmaken (niet gedeeld met Beam).
Setup stappen:
- Supabase dashboard → New Project
- Region: EU West (Frankfurt)
- Auth configuratie:
- Email/password enabled
- Redirect URLs:
https://app.acspascal.nl/auth/callback,http://localhost:5173/auth/callback - Session duration: 30 dagen (configureerbaar, vervangt de WP hardcoded 30d)
- Disable signups (alleen invite-based, beheerder maakt accounts)
- Noteer:
SUPABASE_URL,SUPABASE_ANON_KEY,SUPABASE_SERVICE_ROLE_KEY
0.2 Cloudflare Setup
Section titled “0.2 Cloudflare Setup”| Resource | Naam | Doel |
|---|---|---|
| Workers project | werkbon-api | Hono API |
| Pages project | werkbon-app | React dashboard |
| KV Namespace | WERKBON_CACHE | Adres cache, KPIs |
| Queue | WERKBON_WEBHOOKS | Outgoing webhook retry |
DNS (app.acspascal.nl):
app.acspascal.nl→ CNAME naarwerkbon-app.pages.devapi.acspascal.nl→ Route in wrangler.toml
Wrangler secrets (set via CLI):
wrangler secret put SUPABASE_ANON_KEYwrangler secret put SUPABASE_SERVICE_ROLE_KEYwrangler secret put RESEND_API_KEYwrangler secret put WEBHOOK_SECRETwrangler secret put SENTRY_DSN0.3 Resend (Email)
Section titled “0.3 Resend (Email)”- Resend account aanmaken
- Domein verificatie:
acspascal.nl- DKIM record toevoegen aan DNS
- SPF record toevoegen aan DNS
- From address:
Werkbon <noreply@acspascal.nl> - API key →
wrangler secret put RESEND_API_KEY - Invite email template (dag-1 vereiste, hardcoded HTML zoals Beam):
- Subject:
[naam] heeft je uitgenodigd voor Werkbon - Body: uitnodigingslink + korte uitleg
- Fallback plain text
- Subject:
0.4 Environment Variables
Section titled “0.4 Environment Variables”Dashboard (.env):
VITE_SUPABASE_URL=https://[project].supabase.coVITE_SUPABASE_ANON_KEY=[anon_key]VITE_API_URL=https://api.acspascal.nlVITE_APP_URL=https://app.acspascal.nlVITE_SENTRY_DSN=[sentry_dsn]VITE_SENTRY_ENVIRONMENT=productionAPI (wrangler secrets):
SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYRESEND_API_KEYRESEND_FROM_EMAIL=Werkbon <noreply@acspascal.nl>WEBHOOK_SECRETSENTRY_DSN.env.example in repo root:
# DashboardVITE_SUPABASE_URL=VITE_SUPABASE_ANON_KEY=VITE_API_URL=http://localhost:8787VITE_APP_URL=http://localhost:5173VITE_SENTRY_DSN=VITE_SENTRY_ENVIRONMENT=development0.6 Sentry
Section titled “0.6 Sentry”- Nieuw Sentry project:
werkbon - Alerts → angelo.vaudo@gmail.com
Dashboard (React):
@sentry/reactmet deferred init:requestIdleCallback(() => initSentry(), { timeout: 3000 })SentryErrorBoundarywrapping hele app met fallback UI- Error sample rate: 100%, traces: 10%
- Session replays: 0% normaal, 100% bij errors (
maskAllText: true,blockAllMedia: true) - PII scrubbing: strip Authorization headers in
beforeSend - Browser extension noise filter via
denyUrls
API (CF Workers):
toucan-js(Sentry SDK voor Workers)- Strip Authorization/Cookie headers
- Attach userId + route context bij errors
0.4 Performance Targets
Section titled “0.4 Performance Targets”| Metric | Target | Meting |
|---|---|---|
| Dashboard laden (FCP) | < 1.5s | Lighthouse |
| Pagina navigatie | < 500ms | SWR cache hit |
| Werkbon submit | < 2s | API response time |
| Adres autocomplete | < 300ms | KV cache + Supabase |
| Auto-save draft | < 1s | Background save |
| Werkbon invullen (monteur) | < 60s | User testing |
| Triage per melding (beheerder) | < 15s | User testing |
0.5 Device Support
Section titled “0.5 Device Support”| Platform | Minimum | Doelgroep |
|---|---|---|
| iOS Safari | iOS 16+ | Monteurs (primair) |
| Chrome Android | 100+ | Monteurs (secundair) |
| Chrome Desktop | 100+ | Beheerders |
| Safari Desktop | 16+ | Beheerders |
Mobile-first voor monteur flows. Desktop-first voor beheerder flows. Wordt uiteindelijk PWA (service worker + offline cache + homescreen icon).
Fase 0.5: Project Regels
Section titled “Fase 0.5: Project Regels”Kopieer en pas de Beam project regels aan voor Werkbon:
CLAUDE.md (root)
Section titled “CLAUDE.md (root)”# Werkbon — Project Instructions
Werkbon is een field service management app. Monorepo met twee apps op Cloudflare.
| App | Stack | Domein ||-----|-------|--------|| Dashboard | Vite 6 + React 19 + SWR | `app.acspascal.nl` || API | Hono 4.7 op CF Workers | `api.acspascal.nl` || Shared | Types + utilities | Library |
**Docs:** `docs/` · **Regels:** `.claude/rules/` · **Per-app:** `apps/*/CLAUDE.md`
## Quick Reference
\`\`\`bashpnpm dev # Dashboard (5173) + API (8787)pnpm dev:app # Dashboard onlypnpm dev:api # API onlypnpm typecheck # TypeScript alle appspnpm lint # ESLint alle appspnpm build # Build alle apps\`\`\`
## Quality Gates
1. `pnpm typecheck` — 0 errors2. `pnpm lint` — 0 errors, 0 warnings3. `pnpm build` — voor geraakt(e) app(s)4. Security review — `.claude/rules/security.md`5. Documentatie update bij structurele wijzigingen.claude/rules/ (kopieer van Beam, pas aan)
Section titled “.claude/rules/ (kopieer van Beam, pas aan)”| Bestand | Kopieer van | Aanpassing |
|---|---|---|
code-quality.md | Beam code-quality.md | Verwijder Zustand regels (geen editor), voeg Werkbon-specifieke patronen toe |
security.md | Beam security.md | Voeg webhook secret check toe, verwijder SSRF (geen externe URL uploads) |
documentation.md | Beam documentation.md | Pas docs structuur aan voor Werkbon |
agents.md | Beam agents.md | Vervang beam-* agents door werkbon-* agents |
apps/app/CLAUDE.md
Section titled “apps/app/CLAUDE.md”# Werkbon Dashboard — App Instructions
Vite 6 + React 19 SPA op Cloudflare Pages.
## Stack- React 19 + React Router v7 (lazy routes)- SWR 2.3 (data fetching, dedup 30s)- React Hook Form + Zod (forms)- Tailwind 3.4 + tailwind-merge- @dnd-kit (drag-drop werkzaamheden)- Motion 12.6 (animaties)- Lucide React (iconen)
## Patronen
### SWR HooksAlle hooks: `dedupingInterval: 30_000`, `revalidateOnFocus: false`
### API ClientAlle calls via `api-client.ts` — nooit directe fetch
### Error Handlingtoast.error() voor API fouten, nooit error.message aan gebruiker tonen
## Routing- AuthLayout: `/login`, `/forgot-password`, `/reset-password`- AppLayout: `/`, `/meldingen`, `/klussen`, `/werkbonnen`, `/contacten`, `/instellingen`- Standalone: `/auth/callback`, `/invite/accept`
## Taal- UI: Nederlands- Code comments: Engels- Variabelen: Engelsapps/api/CLAUDE.md
Section titled “apps/api/CLAUDE.md”# Werkbon API — App Instructions
Hono 4.7 op Cloudflare Workers.
## Middleware volgorde (CRITICAL)Sentry → Logger → CORS → Health → [Public routes] → Auth → Rate limit → Routes
## Patronen- Zod validatie op alle input- supabase = user-scoped (RLS), adminSupabase = service role- Error: console.error() + generieke JSON response- Webhook: Queue voor betrouwbare delivery
## Routes- POST /reports/incoming — webhook (VOOR auth, met secret)- /reports, /jobs, /work-orders, /contacts, /addresses — CRUD (NA auth)Fase 1: Fundament
Section titled “Fase 1: Fundament”1.1 Monorepo setup
Section titled “1.1 Monorepo setup”Kopieer de Beam monorepo structuur en pas aan:
Bron: Beam root package.json + pnpm-workspace.yaml
packages: - 'apps/*' - 'packages/*' - 'supabase'// package.json (root){ "name": "werkbon", "private": true, "scripts": { "dev": "pnpm --parallel -r dev", "dev:app": "pnpm --filter @werkbon/app dev", "dev:api": "pnpm --filter @werkbon/api dev", "build": "pnpm -r build", "typecheck": "pnpm -r typecheck", "lint": "pnpm -r lint" }, "devDependencies": { "typescript": "^5.7.0" }, "engines": { "node": ">=20", "pnpm": ">=10" }}Directory structuur aanmaken:
werkbon/ (of: pagebuilder/apps/werkbon/)├── apps/│ ├── app/ # React dashboard (Vite)│ └── api/ # Hono API (CF Workers)├── packages/│ └── shared/ # Gedeelde types + utilities├── supabase/│ └── migrations/ # SQL migraties├── pnpm-workspace.yaml├── package.json└── tsconfig.jsonBeslissing: eigen repo of in Beam monorepo?
Aanbeveling: eigen repo. Redenen:
- Onafhankelijke deploy cycles
- Eigen Supabase project (of gedeeld, maar eigen migraties)
- Eigen CI/CD workflows
- Geen risico dat Beam changes Werkbon breken
1.2 Supabase Migraties
Section titled “1.2 Supabase Migraties”Volg exact het Beam migratie patroon. Maak deze bestanden:
Migration 001: Fundament
supabase/migrations/001_foundation.sqlInhoud:
update_updated_at_column()trigger functie (kopieer uit Beam 001)team_memberstabel (kopieer uit Beam 010, pas rollen aan:owner,admin,planner,monteur)get_team_owner_id(),get_user_role(),are_teammates()helper functies (kopieer uit Beam 011)- Bootstrap trigger:
on_auth_user_created→ maak team_members entry
Migration 002: Contacts
CREATE TABLE contacts ( -- Zie datamodel-nieuw.md voor volledig schema);-- + indexes + RLS + updated_at triggerMigration 003: Reports (meldingen) Migration 004: Jobs (klussen) + job_assignments Migration 005: Work Orders (werkbonnen) Migration 006: Activity Log Migration 007: Attachments Migration 008: Addresses (lookup) Migration 009: Material Catalog Migration 010: Views (job_details, work_order_details) Migration 011: Status transition triggers
Alle schema’s staan in Datamodel (Nieuw).
1.3 API Middleware Stack
Section titled “1.3 API Middleware Stack”Kopieer volledig uit Beam (apps/api/src/):
| Bestand | Kopieer van | Aanpassing |
|---|---|---|
src/index.ts | Beam index.ts | Routes wijzigen, webhook route VOOR auth |
src/types.ts | Beam types.ts | Env bindings aanpassen (geen R2 media bucket nodig in v1) |
src/middleware/auth.ts | Beam auth.ts | Geen aanpassing |
src/middleware/cors.ts | Beam cors.ts | DASHBOARD_ORIGIN aanpassen |
src/middleware/logger.ts | Beam logger.ts | Geen aanpassing |
src/middleware/rate-limit.ts | Beam rate-limit.ts | Limieten aanpassen |
src/middleware/sentry.ts | Beam sentry.ts | Geen aanpassing |
src/lib/supabase.ts | Beam supabase.ts | Geen aanpassing |
Nieuwe bestanden:
src/routes/ reports.ts # CRUD meldingen jobs.ts # CRUD klussen work-orders.ts # CRUD werkbonnen contacts.ts # CRUD + search + matching addresses.ts # Autocomplete cascade
src/services/ contact.service.ts # findOrCreate, matching, normalisatie address.service.ts # Lookup, Maasdelta detectie phone.service.ts # E.164 normalisatie (port van WP) webhook.service.ts # Outgoing naar Make.com/Moneybird moneybird.service.ts # Invoice details builder
src/scheduled/ cleanup.ts # Soft delete purge (30 dagen)wrangler.toml:
name = "werkbon-api"main = "src/index.ts"compatibility_date = "2026-04-01"compatibility_flags = ["nodejs_compat"]
[vars]SUPABASE_URL = ""DASHBOARD_ORIGIN = "https://app.acspascal.nl"ENVIRONMENT = "production"
[[kv_namespaces]]binding = "WERKBON_CACHE"
[triggers]crons = ["0 3 * * *"]Webhook routing (CRITICAL):
// src/index.ts — webhook VOOR auth middlewareconst publicRoutes = new Hono<{ Bindings: Env }>()
// Incoming webhook van marketingsite (geen Bearer auth, wel secret)publicRoutes.post('/reports/incoming', incomingWebhookHandler)
// Health checkpublicRoutes.get('/health', (c) => c.json({ status: 'ok' }))
app.route('/', publicRoutes)
// Auth middleware (alle routes hierna vereisen Bearer token)app.use('*', authMiddleware)
// Authenticated routesapp.route('/reports', reports)app.route('/jobs', jobs)app.route('/work-orders', workOrders)app.route('/contacts', contacts)app.route('/addresses', addresses)1.4 Packages: Shared
Section titled “1.4 Packages: Shared”packages/shared/├── src/│ ├── index.ts # cn() utility (kopieer van Beam)│ ├── types.ts # Contact, Report, Job, WorkOrder interfaces│ └── status.ts # Status enums + transition validatie├── package.json└── tsconfig.jsonTypes (gedeeld tussen app en api):
export type ContactType = 'maasdelta' | 'particulier' | 'zakelijk'export type ReportStatus = 'new' | 'accepted' | 'rejected' | 'merged' | 'on_hold'export type JobStatus = 'open' | 'assigned' | 'scheduled' | 'in_progress' | 'completed' | 'on_hold' | 'cancelled'export type WorkOrderStatus = 'draft' | 'submitted' | 'approved' | 'revision_requested'
export interface Contact { id: string type: ContactType company: string | null firstname: string | null lastname: string | null email: string | null phone: string | null streetname: string | null house_number: string | null addition: string | null zipcode: string | null city: string | null moneybird_id: string | null}
export interface Job { id: string contact_id: string report_id: string | null status: JobStatus type: ContactType address_override: boolean scheduled_date: string | null description: string | null // ...}
export interface WorkOrder { id: string job_id: string contact_id: string | null // null = erft van job status: WorkOrderStatus date: string work_items: WorkItem[] materials: Material[] signature_url: string | null}
export interface WorkItem { location: string[] work: string time: number | null date: string | null}
export interface Material { id: string label: string qty: number}Fase 2: CRUD
Section titled “Fase 2: CRUD”2.1 Dashboard Auth + Layout
Section titled “2.1 Dashboard Auth + Layout”Kopieer uit Beam dashboard (apps/dashboard/src/):
| Bestand | Kopieer van | Aanpassing |
|---|---|---|
src/main.tsx | Beam main.tsx | Sentry DSN, imports |
src/lib/auth-context.tsx | Beam auth-context.tsx | Geen |
src/lib/auth.ts | Beam auth.ts | Geen |
src/lib/api-client.ts | Beam api-client.ts | Endpoints aanpassen |
src/components/ui/modal.tsx | Beam modal.tsx | Geen |
src/components/toast.tsx | Beam toast.tsx | Geen |
vite.config.ts | Beam vite.config.ts | Proxy + chunks |
tailwind.config.js | Beam tailwind.config.js | Kleuren |
index.html | Beam index.html | Titel |
Nieuwe layout:
// Gebaseerd op Beam DashboardLayout.tsx maar met Werkbon navigatie:// - Dashboard// - Meldingen// - Klussen// - Werkbonnen (rapporten)// - Contacten// - InstellingenRoutes:
const router = createBrowserRouter([ { element: <AuthLayout />, children: [ { path: '/login', lazy: () => import('./pages/LoginPage') }, ], }, { element: <AppLayout />, children: [ { path: '/', lazy: () => import('./pages/DashboardPage') }, { path: '/meldingen', lazy: () => import('./pages/ReportsPage') }, { path: '/klussen', lazy: () => import('./pages/JobsPage') }, { path: '/klussen/nieuw', lazy: () => import('./pages/JobFormPage') }, { path: '/klussen/:id', lazy: () => import('./pages/JobDetailPage') }, { path: '/werkbonnen', lazy: () => import('./pages/WorkOrdersPage') }, { path: '/werkbonnen/nieuw/:jobId', lazy: () => import('./pages/WorkOrderFormPage') }, { path: '/werkbonnen/:id', lazy: () => import('./pages/WorkOrderFormPage') }, { path: '/contacten', lazy: () => import('./pages/ContactsPage') }, { path: '/instellingen', lazy: () => import('./pages/SettingsPage') }, ], },])2.2 Invite Flow (dag-1 vereiste)
Section titled “2.2 Invite Flow (dag-1 vereiste)”Werkbon is invite-only. De volledige invite flow moet werken voor de eerste monteur kan inloggen.
Database: team_invitations tabel (in Migration 001)
CREATE TABLE team_invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE, email TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'monteur' CHECK (role IN ('admin', 'planner', 'monteur')), token UUID NOT NULL DEFAULT gen_random_uuid(), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '7 days'), accepted_by UUID REFERENCES auth.users(id), responded_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (token));Bootstrap trigger (bij invite-accept, NIET bij signup):
-- Werkbon: users worden aangemaakt via invite, niet via signup-- De bootstrap trigger moet NIET automatisch een 'owner' team_members row aanmaken-- Alleen de eerste beheerder (handmatig aangemaakt) krijgt de 'owner' rol-- Genodigden krijgen hun rol via de invite flow (team.ts → POST /team/invitations/accept)API endpoints (kopieer van Beam team.ts, pas aan):
| Endpoint | Doel |
|---|---|
POST /team/invite | Uitnodiging sturen (beheerder/admin) |
DELETE /team/invitations/:id | Intrekken |
POST /team/invitations/:id/resend | Opnieuw versturen |
POST /team/invitations/accept | Accepteren (via magic link) |
POST /team/invitations/decline | Weigeren |
GET /team/invitations/info | Info voor accept pagina |
PATCH /team/members/:id/role | Rol wijzigen |
DELETE /team/members/:id | Lid verwijderen |
Pagina’s:
| Pagina | Route | Doel |
|---|---|---|
| AuthCallbackPage | /auth/callback | Verwerkt magic link tokens van Resend |
| InviteAcceptPage | /invite/accept | Toont invite info, accept/decline knoppen |
| ForgotPasswordPage | /forgot-password | Wachtwoord reset aanvragen |
| ResetPasswordPage | /reset-password | Nieuw wachtwoord instellen |
| UsersPage | /instellingen/gebruikers | Beheerder beheert team + invites |
Ghost user cleanup:
Beam roept delete_ghost_user RPC aan bij invite revoke. Kopieer deze Supabase functie.
2.3 Contacten CRUD
Section titled “2.3 Contacten CRUD”Patroon kopieer van Beam PagesPage.tsx:
src/lib/hooks/use-contacts.ts # SWR hook (patroon: use-pages.ts)src/lib/supabase/contacts.ts # Service layer (patroon: pages.ts)src/pages/ContactsPage.tsx # Lijst + zoeken + filters (patroon: PagesPage.tsx)src/components/contacts/ ContactList.tsx # Tabel met sortering ContactFilters.tsx # Type filter, zoekbalk ContactFormModal.tsx # Create/edit modal2.3 Adres Autocomplete
Section titled “2.3 Adres Autocomplete”Nieuw component, geen Beam equivalent:
src/components/forms/ AddressAutocomplete.tsx # 4-staps cascadeAPI route addresses.ts met 4 endpoints:
GET /addresses/streets?q=hoofd→ straatnamenGET /addresses/numbers?street=Hoofdstraat&city=Rotterdam→ huisnummersGET /addresses/additions?street=...&number=42→ toevoegingenGET /addresses/resolve?zipcode=3029AK&number=42&addition=A→ full address
Adres data uit KV cache (statisch, 30 dagen TTL).
Fase 3: Kernflow
Section titled “Fase 3: Kernflow”3.1 Meldingen + Triage
Section titled “3.1 Meldingen + Triage”src/pages/ReportsPage.tsx # Lijst met triage actiessrc/lib/hooks/use-reports.tssrc/lib/supabase/reports.tssrc/components/reports/ ReportCard.tsx # Compact kaartje voor triage ReportTriageActions.tsx # Accepteren/Afwijzen/On hold knoppen ReportDuplicateWarning.tsx # "Er staat al een melding op dit adres"Triage view: Card-based, alle info in 1 oogopslag, 1-klik acties.
3.2 Klussen + Inplannen
Section titled “3.2 Klussen + Inplannen”src/pages/JobsPage.tsx # Lijst met 7 status filterssrc/pages/JobFormPage.tsx # Create/edit + melding koppelensrc/pages/JobDetailPage.tsx # Detail + timeline + werkbonnensrc/lib/hooks/use-jobs.tssrc/lib/supabase/jobs.tssrc/components/jobs/ JobList.tsx JobFilters.tsx # 7 statussen, type, monteur JobDetailSheet.tsx # Side panel met details JobTimeline.tsx # Activity log chronologisch JobAssignModal.tsx # Monteur toewijzen + datumDagplanning monteur (v1 must-have):
src/pages/MijnKlussenPage.tsx # "Mijn klussen vandaag"Query: job_assignments WHERE user_id = auth.uid() AND jobs.scheduled_date = today
3.3 Werkbon Invullen (Mobile-First)
Section titled “3.3 Werkbon Invullen (Mobile-First)”Dit is de make-or-break feature. Mobile-first, 30 seconden doel.
src/pages/WorkOrderFormPage.tsx # Hoofdformuliersrc/lib/hooks/use-work-orders.tssrc/lib/hooks/use-auto-save.ts # Debounced save naar Supabasesrc/lib/supabase/work-orders.tssrc/components/work-orders/ WorkItemsEditor.tsx # @dnd-kit drag-drop MaterialEditor.tsx # Zoeken + qty SignatureCapture.tsx # Canvas handtekening WorkOrderPreview.tsx # Live preview JobContextSidebar.tsx # Melding info + eerdere werkbonnen DraftBanner.tsx # "Je hebt een onafgeronde werkbon"Auto-save:
export function useAutoSave(workOrderId: string, data: Partial<WorkOrder>) { const save = useDebouncedCallback(async (d) => { await supabase.from('work_orders').update(d).eq('id', workOrderId) }, 5000) // 5 seconden debounce
useEffect(() => { save(data) }, [data])
// Bewaar ook lokaal voor offline scenario useEffect(() => { localStorage.setItem(`draft:${workOrderId}`, JSON.stringify(data)) }, [data])}Frequent combo’s:
const FREQUENT_COMBOS: WorkItem[] = [ { location: ['Badkamer'], work: 'Lekkage verholpen', time: 1, date: null }, { location: ['Keuken'], work: 'Riool ontstopt met hogedruk', time: 1.5, date: null }, { location: ['Toilet'], work: 'WC ontstopt', time: 0.5, date: null }, // Configureerbaar per site via material_catalog of aparte tabel]Go-Live Vereisten (doorsnijdend)
Section titled “Go-Live Vereisten (doorsnijdend)”Deze items zijn niet gekoppeld aan 1 fase maar moeten klaar zijn voor productie.
Signature Storage
Section titled “Signature Storage”Werkbon handtekeningen opslaan via Supabase Storage (eenvoudiger dan R2 voor v1):
Bucket: werkbon-signaturesPad: {site_id}/{work_order_id}/signature.png- Upload via API:
POST /work-orders/:id/signature(multipart form) - Opslag: Supabase Storage bucket (public read, authenticated write)
signature_urlop work_orders tabel → Supabase Storage URL- Max grootte: 500KB (handtekening is ~50-200KB)
- Later (v2 foto’s): migreer naar R2 als volumes groeien
Server-Side Pagination
Section titled “Server-Side Pagination”Alle list endpoints moeten server-side paginatie ondersteunen:
// API patternapp.get('/jobs', async (c) => { const page = parseInt(c.req.query('page') || '1') const limit = Math.min(parseInt(c.req.query('limit') || '25'), 100) const offset = (page - 1) * limit
const { data, count } = await supabase .from('jobs') .select('*', { count: 'exact' }) .range(offset, offset + limit - 1) .order('created_at', { ascending: false })
return c.json({ items: data, total: count, page, limit })})
// SWR hook patternexport function useJobs(page: number, filters: JobFilters) { return useSWR( `jobs-${page}-${JSON.stringify(filters)}`, () => getJobs(page, filters), { keepPreviousData: true } )}Optimistic Updates
Section titled “Optimistic Updates”Kritisch voor het 15-seconden triage doel. Implementeer voor:
- Melding accepteren/afwijzen (triage)
- Klus status wijzigen
- Werkbon goedkeuren/terugsturen
// Pattern (kopieer van Beam use-pages.ts)await mutateJobs( async () => { await updateJobStatus(jobId, newStatus) return optimisticData }, { optimisticData, revalidate: false })KV Cache Warming (Adressen)
Section titled “KV Cache Warming (Adressen)”- Bij Supabase seed: script laadt adressen uit CSV →
addressestabel - Bij eerste API start: cron job of init functie vult KV cache
- Fallback: als KV miss → query Supabase direct, cache resultaat
- Verversen: admin actie “Cache verversen” of automatisch na 30 dagen TTL
// Cache warming functieasync function warmAddressCache(env: Env) { const supabase = createAdminClient(env) const { data } = await supabase.from('addresses').select('*') await env.WERKBON_CACHE.put('addresses:all', JSON.stringify(data), { expirationTtl: 86400 * 30 // 30 dagen })}API Response Format
Section titled “API Response Format”Consistente conventie voor alle endpoints:
// Success{ items: [...], total: 123, page: 1, limit: 25 } // lijst{ data: {...} } // enkel item{ success: true } // mutatie zonder data
// Error{ error: 'Beschrijving in het Nederlands' } // 4xx/5xxFase 4: Integraties
Section titled “Fase 4: Integraties”4.1 Incoming Webhook
Section titled “4.1 Incoming Webhook”Zie Datamodel (Nieuw) Flow 2 voor de volledige implementatie.
4.2 Outgoing Webhooks (Cloudflare Queue)
Section titled “4.2 Outgoing Webhooks (Cloudflare Queue)”src/scheduled/ queue-consumer.ts # Verwerkt webhook berichten// Bij werkbon goedkeuring:await c.env.WEBHOOK_QUEUE.send({ type: 'moneybird_invoice', work_order_id: workOrder.id, attempt: 0,})
// Queue consumer:export default { async queue(batch, env) { for (const msg of batch.messages) { try { await processWebhook(msg.body, env) msg.ack() } catch { if (msg.body.attempt < 5) { msg.retry({ delaySeconds: 60 * (msg.body.attempt + 1) }) } else { // Dead letter: log + notificeer beheerder await logFailedWebhook(msg.body, env) msg.ack() } } } }}4.3 Klant Notificaties (v1)
Section titled “4.3 Klant Notificaties (v1)”Minimaal 3 email triggers:
- Melding ontvangen → bevestigingsmail met status-check URL
- Klus ingepland → “Monteur komt [datum] tussen [tijd]”
- Werkbon goedgekeurd → “Het werk is afgerond”
Via Resend (al in Beam stack) of SendGrid.
Fase 5: Data Migratie
Section titled “Fase 5: Data Migratie”Zie Migratie Stack voor de volledige strategie:
- 6-fasen migratie script
- Blue-green deployment (week-voor-week)
- Validatie queries
- Rollback plan
GitHub Actions (kopieer van Beam, pas aan):
name: Deploy Werkbon Appon: push: paths: ['apps/app/**', 'packages/shared/**'] branches: [main]jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - run: pnpm install --frozen-lockfile - run: pnpm --filter @werkbon/app build - run: pnpm exec wrangler pages deploy apps/app/dist --project-name werkbon-app
# .github/workflows/deploy-api.ymlname: Deploy Werkbon APIon: push: paths: ['apps/api/**', 'packages/shared/**'] branches: [main]jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - run: pnpm install --frozen-lockfile - run: pnpm --filter @werkbon/api deployTijdlijn
Section titled “Tijdlijn”| Week | Wat | Uren (schatting) |
|---|---|---|
| 1 | Monorepo setup + Supabase migraties | 16 |
| 2 | API middleware + routes (contacts, addresses) | 16 |
| 3 | Dashboard auth + layout + contacten CRUD | 16 |
| 4 | Adres autocomplete + meldingen triage | 16 |
| 5 | Klussen CRUD + inplannen + dagplanning | 20 |
| 6 | Werkbon formulier (mobile-first) | 24 |
| 7 | Werkbon: werkzaamheden editor + materiaal + handtekening | 24 |
| 8 | Webhooks (incoming + outgoing) + email notificaties | 16 |
| 9 | Data migratie script + validatie | 16 |
| 10 | Parallel draai + testing + cut-over | 16 |
| Totaal | ~180 uur |
Test Strategie
Section titled “Test Strategie”Zelfde aanpak als Beam: Playwright E2E tests + TypeScript typecheck + ESLint.
E2E Tests (Playwright)
Section titled “E2E Tests (Playwright)”| Test | Wat | Prioriteit |
|---|---|---|
| Auth: login | Email + wachtwoord → dashboard | v1 |
| Auth: logout | Uitloggen → redirect /login | v1 |
| Contacten: CRUD | Aanmaken, bewerken, zoeken, verwijderen | v1 |
| Meldingen: triage | Aanmaken, accepteren, afwijzen | v1 |
| Klussen: CRUD | Aanmaken, toewijzen, inplannen | v1 |
| Klussen: status | Status wijzigen via modal | v1 |
| Werkbon: invullen | Werkzaamheden + materiaal + submit | v1 |
| Werkbon: auto-save | Draft opslaan + herstellen | v1 |
| Werkbon: goedkeuren | Beheerder keurt goed / stuurt terug | v1 |
| Adres: autocomplete | Straat → nummer → postcode chain | v1 |
| Webhook: incoming | POST /reports/incoming met secret | v1 |
| Mobile: responsive | Werkbon op 375px viewport | v1 |
CI Quality Gates
Section titled “CI Quality Gates”# In elke PR:- pnpm typecheck # 0 TypeScript errors- pnpm lint # 0 ESLint errors/warnings- pnpm build # Succesvolle build- pnpm test:e2e # Alle Playwright tests groenTest Credentials
Section titled “Test Credentials”Beheerder: beheerder@acspascal.nl / [test wachtwoord]Monteur: monteur@acspascal.nl / [test wachtwoord]Aanmaken via Supabase auth admin na project setup.
Backlog
Section titled “Backlog”Items die niet in v1 zitten maar wel gepland zijn:
Data & Migratie
Section titled “Data & Migratie”- Veld-voor-veld mapping WP → Supabase (per ACF field → kolom)
- Make.com scenario’s updaten naar nieuwe API endpoints
- Adres CSV import naar Supabase addresses tabel
- Materiaal CSV import naar material_catalog tabel
Functionaliteit
Section titled “Functionaliteit”- Monteur training / handleiding
- Email templates ontwerpen (melding ontvangen, klus ingepland, werk afgerond)
- Direct Moneybird API i.p.v. Make.com (fase 2)
- Backup strategie documenteren (Supabase PITR)
- PWA: service worker + offline cache + homescreen icon
- Foto’s bij werkbon (v2)
- In-app comments beheerder ↔ monteur (v2)
- Materiaal catalogus met prijzen (v2)
- Monteur weekplanning / capaciteitsoverzicht (v2)
- CSV import voor woningcorporatie bulk meldingen (v2)
- Rapporten export (PDF/Excel) (v2)
- WhatsApp integratie (v3)
- Route optimalisatie (v3)
- Barcode scanner materiaal (v3)
- Klant portaal (v3)
- Voorraad beheer (v3)
Referenties
Section titled “Referenties”Beam bestanden om te kopiëren
Section titled “Beam bestanden om te kopiëren”# Monorepopackage.json → Root workspace configpnpm-workspace.yaml → Workspace definitie
# API (100% kopieerbaar)apps/api/src/middleware/auth.ts → Bearer token → Supabaseapps/api/src/middleware/cors.ts → CORS configapps/api/src/middleware/logger.ts → Structured loggingapps/api/src/middleware/rate-limit.ts → Sliding windowapps/api/src/middleware/sentry.ts → Error trackingapps/api/src/lib/supabase.ts → Client helpersapps/api/src/routes/pages.ts → Route pattern (validation + CRUD)
# Dashboard (kopieer + aanpassen)apps/dashboard/src/main.tsx → Entry + Sentryapps/dashboard/src/lib/auth-context.tsx → Auth providerapps/dashboard/src/lib/auth.ts → Login/logoutapps/dashboard/src/lib/api-client.ts → Typed API clientapps/dashboard/src/components/ui/modal.tsx → Modal + AnimatePresenceapps/dashboard/src/components/toast.tsx → Toast notificationsapps/dashboard/src/lib/hooks/use-pages.ts → SWR hook patroonapps/dashboard/src/lib/supabase/pages.ts → Service layer patroonapps/dashboard/src/pages/PagesPage.tsx → List + filter patroonapps/dashboard/vite.config.ts → Build configapps/dashboard/tailwind.config.js → Tailwind setup
# Sharedpackages/shared/src/index.ts → cn() utilitypackages/shared/package.json → Export config
# Supabasesupabase/migrations/001_*.sql → Table + trigger patroonsupabase/migrations/010_*.sql → Team + RLS helperssupabase/migrations/011_*.sql → RLS policy patroon