Verbeterrapport
Overzicht
Section titled “Overzicht”Dit rapport bundelt de bevindingen van 4 parallelle audits:
- Schema design audit (datamodel-nieuw)
- UX journey audit (user-journeys)
- Migratie architectuur audit (migratie-stack)
- WP broncode performance audit (huidige implementatie)
Elke bevinding is beoordeeld op ernst en gegroepeerd per thema.
1. Schema Design
Section titled “1. Schema Design”CRITICAL: assigned_to UUID[] moet junction table worden
Section titled “CRITICAL: assigned_to UUID[] moet junction table worden”Het huidige schema gebruikt assigned_to UUID[] (Postgres array) op de jobs tabel. Dit is problematisch:
- Geen foreign key constraint mogelijk op array elementen
- Geen index op individuele array waarden (alleen GIN, langzaam)
- Geen RLS mogelijk per toewijzing
- Niet queryable: “geef mij alle klussen van monteur X” vereist
@>operator
Fix: Aparte job_assignments tabel:
CREATE TABLE job_assignments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), job_id UUID NOT NULL REFERENCES jobs(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role TEXT DEFAULT 'assigned' CHECK (role IN ('assigned', 'lead')), assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), assigned_by UUID REFERENCES auth.users(id), UNIQUE (job_id, user_id));
CREATE INDEX idx_assignments_user ON job_assignments(user_id);CREATE INDEX idx_assignments_job ON job_assignments(job_id);Voordelen:
- “Mijn klussen vandaag”:
WHERE user_id = auth.uid() AND job.scheduled_date = today - Wie heeft toegewezen en wanneer (audit trail)
- Lead monteur vs helper onderscheid
CRITICAL: work_items en materials als JSONB — validatie ontbreekt
Section titled “CRITICAL: work_items en materials als JSONB — validatie ontbreekt”JSONB is flexibel maar er is geen database-level validatie. Een lege string of ongeldige structuur wordt geaccepteerd.
Fix: Voeg CHECK constraint toe:
ALTER TABLE work_orders ADD CONSTRAINT work_items_valid CHECK (jsonb_typeof(work_items) = 'array');
ALTER TABLE work_orders ADD CONSTRAINT materials_valid CHECK (jsonb_typeof(materials) = 'array');Overweging: Voor materiaal zou een lookup tabel beter zijn:
CREATE TABLE material_catalog ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE, name TEXT NOT NULL, category TEXT, unit TEXT DEFAULT 'stuk', active BOOLEAN DEFAULT true, UNIQUE (site_id, name));
-- work_order_materials als junction met qtyCREATE TABLE work_order_materials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), work_order_id UUID NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE, material_id UUID NOT NULL REFERENCES material_catalog(id), quantity NUMERIC(10,2) NOT NULL DEFAULT 1, note TEXT);Voordelen: materiaal namen consistent, rapportage op materiaalverbruik, voorraad tracking (v3).
IMPORTANT: activity_log partitioneren
Section titled “IMPORTANT: activity_log partitioneren”De activity_log groeit onbeperkt. Bij 100 klussen/dag met gemiddeld 5 events = 500 rows/dag = 180.000/jaar.
Fix: Time-based partitioning:
CREATE TABLE activity_log ( ... created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()) PARTITION BY RANGE (created_at);
CREATE TABLE activity_log_2026 PARTITION OF activity_log FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');Of: archive naar cold storage na 90 dagen.
IMPORTANT: Soft-delete op contacts conflicteert met FK RESTRICT
Section titled “IMPORTANT: Soft-delete op contacts conflicteert met FK RESTRICT”reports en jobs hebben REFERENCES contacts(id) ON DELETE RESTRICT. Maar als een contact soft-deleted wordt (deleted_at IS NOT NULL), bestaan de FK referenties nog. Het contact kan dan niet hard-deleted worden.
Fix: Gebruik ON DELETE SET NULL i.p.v. RESTRICT, of voeg een check toe:
-- Voorkom soft-delete als er actieve jobs/reports zijnCREATE OR REPLACE FUNCTION prevent_contact_soft_delete()RETURNS TRIGGER AS $$BEGIN IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN IF EXISTS (SELECT 1 FROM jobs WHERE contact_id = NEW.id AND status NOT IN ('completed', 'cancelled')) THEN RAISE EXCEPTION 'Contact heeft actieve klussen'; END IF; END IF; RETURN NEW;END;$$ LANGUAGE plpgsql;IMPORTANT: RLS user_site_ids() performance
Section titled “IMPORTANT: RLS user_site_ids() performance”user_site_ids() wordt op elke query aangeroepen via RLS. Dit is een subquery die sites + team_members joint.
Fix: Markeer als STABLE (al gedaan) maar overweeg ook:
-- Materialized in een session variabele bij loginSET app.current_site_id = 'uuid-here';
-- RLS check wordt dan:CREATE POLICY "..." ON jobs FOR SELECT USING (site_id = current_setting('app.current_site_id')::uuid);Dit elimineert de subquery volledig. De Hono middleware zet de session variabele na auth.
NICE-TO-HAVE: Status transitie validatie
Section titled “NICE-TO-HAVE: Status transitie validatie”Momenteel kan een klus van ‘completed’ terug naar ‘open’ gezet worden. Dat is waarschijnlijk ongewenst.
CREATE OR REPLACE FUNCTION validate_job_status_transition()RETURNS TRIGGER AS $$DECLARE valid_transitions jsonb := '{ "open": ["assigned", "cancelled"], "assigned": ["scheduled", "open", "cancelled"], "scheduled": ["in_progress", "assigned", "cancelled"], "in_progress": ["completed", "on_hold", "cancelled"], "on_hold": ["in_progress", "cancelled"], "completed": [], "cancelled": [] }'::jsonb;BEGIN IF OLD.status = NEW.status THEN RETURN NEW; END IF; IF NOT valid_transitions->OLD.status ? NEW.status THEN RAISE EXCEPTION 'Ongeldige status transitie: % → %', OLD.status, NEW.status; END IF; RETURN NEW;END;$$ LANGUAGE plpgsql;2. UX Gaps
Section titled “2. UX Gaps”MUST-FIX: Offline/slechte connectie voor monteur
Section titled “MUST-FIX: Offline/slechte connectie voor monteur”Monteurs werken in kelders, tunnels, landelijk gebied. De 30-seconden werkbon is zinloos als de app niet laadt.
v1 minimum: Optimistic UI + retry queue. Form state lokaal opslaan (localStorage), sync wanneer online.
v2: Service Worker met offline cache voor:
- Materiaal catalogus (static data)
- Huidige dagplanning (pre-fetched)
- Werkbon draft opslaan offline → sync bij verbinding
MUST-FIX: Monteur wordt gestoord tijdens werkbon
Section titled “MUST-FIX: Monteur wordt gestoord tijdens werkbon”Telefoon gaat, klant vraagt iets, monteur moet weg. Huidige WP: alles kwijt.
Oplossing: Auto-save is al in het schema (status: 'draft'). Implementatie:
- Debounced save elke 5 seconden bij form changes
- “Je hebt een onafgeronde werkbon” banner op de homepage
- Draft lijst in het monteur menu
SHOULD-FIX: Bijkomend werk (“kun je ook even naar X kijken”)
Section titled “SHOULD-FIX: Bijkomend werk (“kun je ook even naar X kijken”)”Monteur is bij klant voor lekkage, klant vraagt: “De kraan in de keuken lekt ook, kun je dat meteen meenemen?”
Opties:
- Extra werkregel toevoegen aan huidige werkbon (eenvoudigst)
- Tweede werkbon aanmaken op dezelfde klus
- Nieuwe melding + klus aanmaken ter plaatse (te veel overhead)
Aanbeveling: Optie 1 voor v1 (gewoon een extra work_item), optie 2 als het apart gefactureerd moet worden.
SHOULD-FIX: Bulk triage voor woningcorporaties
Section titled “SHOULD-FIX: Bulk triage voor woningcorporaties”Maasdelta stuurt soms 20 meldingen tegelijk (renovatieproject). 1-voor-1 triageren duurt te lang.
Fix: Bulk acties op de meldingen pagina:
- Selecteer meerdere → “Accepteren als klussen”
- Optioneel: zelfde monteur toewijzen, zelfde datum
- Scheelt 80% van de klikken
SHOULD-FIX: Herplanning bij ziekte
Section titled “SHOULD-FIX: Herplanning bij ziekte”Monteur is ziek, 5 klussen moeten herplant. Nu: 1-voor-1 opnieuw toewijzen.
Fix: “Herplannen” actie op monteur niveau:
- Selecteer monteur → “Alle klussen van vandaag herplannen”
- Kies nieuwe monteur of datum
- Bulk update
NICE-TO-HAVE: Klant status communicatie (v2)
Section titled “NICE-TO-HAVE: Klant status communicatie (v2)”In v1 geen klantportaal. Maar minimaal:
- Email bij inplanning: “Uw monteur komt [datum] tussen [tijd]”
- Email na werkbon goedkeuring: “Het werk is afgerond” + factuur
NICE-TO-HAVE: Scheduling conflicten
Section titled “NICE-TO-HAVE: Scheduling conflicten”Twee klussen op hetzelfde tijdslot bij dezelfde monteur. Nu: geen waarschuwing.
Fix: Soft warning bij inplannen: “Piet heeft al een klus op dinsdag 9:00-11:00. Toch inplannen?”
NICE-TO-HAVE: Kleurenblind-vriendelijke statussen
Section titled “NICE-TO-HAVE: Kleurenblind-vriendelijke statussen”De status kleuren (rood/oranje/groen) zijn niet kleurenblind-vriendelijk.
Fix: Voeg iconen toe naast kleuren:
- Open: blauw cirkel
- Assigned: persoon icoon
- Scheduled: kalender icoon
- In progress: spinner/pijl
- Completed: vinkje
- Cancelled: kruis
3. Migratie Architectuur
Section titled “3. Migratie Architectuur”CRITICAL: Contact matching race condition
Section titled “CRITICAL: Contact matching race condition”Twee gelijktijdige webhooks van de marketingsite met dezelfde klant → twee contacten aangemaakt.
Fix: Database-level UNIQUE constraint + upsert:
-- Unieke index op genormaliseerde email (als email niet leeg is)CREATE UNIQUE INDEX idx_contacts_unique_email ON contacts(site_id, lower(email)) WHERE email IS NOT NULL AND email != '' AND deleted_at IS NULL;
-- Upsert in de APIINSERT INTO contacts (site_id, email, firstname, ...)VALUES ($1, $2, $3, ...)ON CONFLICT (site_id, lower(email)) WHERE email IS NOT NULL AND email != '' AND deleted_at IS NULLDO UPDATE SET firstname = COALESCE(NULLIF(EXCLUDED.firstname, ''), contacts.firstname), updated_at = NOW()RETURNING *;CRITICAL: Webhook betrouwbaarheid (outgoing)
Section titled “CRITICAL: Webhook betrouwbaarheid (outgoing)”Huidige WP: fire-and-forget wp_remote_post(). Als Make.com down is → data verloren.
Fix: Cloudflare Queue:
// In de API route (na werkbon goedkeuring):await c.env.WEBHOOK_QUEUE.send({ type: 'moneybird_invoice', work_order_id: workOrder.id, attempt: 1,});
// Queue consumer (apart Worker):export default { async queue(batch, env) { for (const msg of batch.messages) { try { await sendToMakeWebhook(msg.body); msg.ack(); } catch { msg.retry({ delaySeconds: 60 * msg.body.attempt }); } } }};Voordelen: automatic retry, dead letter queue, geen data verlies.
IMPORTANT: Site_id patroon — single vs multi-tenant
Section titled “IMPORTANT: Site_id patroon — single vs multi-tenant”Het Beam site_id patroon is ontworpen voor multi-tenant (meerdere sites per account). Werkbon is single-company.
Aanbeveling: Houd site_id maar maak het transparant:
- Bij registratie: automatisch 1 site aanmaken
- In de UI: geen site switcher tonen
- In de API: site_id automatisch uit auth context
- Later: multi-tenant is gratis (al ingebouwd)
IMPORTANT: Adres database caching
Section titled “IMPORTANT: Adres database caching”De addresses tabel is statisch (verandert alleen bij CSV import). Elke autocomplete query hit de database.
Fix: Cache in KV:
// Bij import: cache naar KVawait env.WERKBON_CACHE.put( `addresses:${siteId}`, JSON.stringify(allAddresses), { expirationTtl: 86400 * 30 } // 30 dagen);
// Bij autocomplete: lees uit KV, filter client-sideconst cached = await env.WERKBON_CACHE.get(`addresses:${siteId}`, 'json');if (cached) return filterAddresses(cached, searchTerm);Of: stuur de hele tabel naar de client bij login (< 1MB voor ~10.000 adressen) en doe alle filtering client-side. Elimineert AJAX volledig.
IMPORTANT: Data migratie strategie ontbreekt
Section titled “IMPORTANT: Data migratie strategie ontbreekt”Er is geen concreet migratieplan voor de data.
Nodig:
- WP export script (PHP): dump contacts, jobs, files naar JSON
- Transform script (Node): WP post_meta → flat columns, deduplicate contacts
- Supabase import script: insert met conflict handling
- Validatie: tellingen vergelijken, steekproef controle
- Parallel draai: 2 weken WP en nieuwe app naast elkaar
- Rollback: als iets misgaat, terug naar WP
OPTIMIZATION: Direct Moneybird API i.p.v. Make.com
Section titled “OPTIMIZATION: Direct Moneybird API i.p.v. Make.com”Make.com is een extra hop (en kosten) voor wat een simpele API call is.
Overweeg: Direct Moneybird API aanroepen vanuit de Hono Worker:
- Moneybird API is goed gedocumenteerd
- Scheelt Make.com abonnement
- Minder latency (1 hop minder)
- Meer controle over error handling
Nadeel: zelf OAuth token management bouwen. Maar met Cloudflare KV voor token opslag is dat beheersbaar.
4. WP Performance Anti-Patterns (NIET repliceren)
Section titled “4. WP Performance Anti-Patterns (NIET repliceren)”Query Counts (huidige WP implementatie)
Section titled “Query Counts (huidige WP implementatie)”| Operatie | Queries | Probleem |
|---|---|---|
| Jobs lijst (50 items) | 4-5 + 1.250 meta lookups | Individuele get_post_meta in loop |
| Reports lijst (50 items) | 5-7 + full table scans | Dynamic JOINs op postmeta |
| Adres autocomplete (1 toets) | 2-3 per keystroke | Geen debounce, geen cache |
| Job aanmaken (API) | 15-25 | 5 LEFT JOINs voor contact matching |
| Contact upsert (worst case) | 520+ | N+1 loop: 50 kandidaten x 10 meta reads |
Supabase Equivalenten
Section titled “Supabase Equivalenten”| WP Anti-Pattern | Supabase Best Practice |
|---|---|
| get_post_meta() in loop | .select('*, contacts(*)') met JOIN |
| Dynamic JOIN construction | Supabase .or() / .in() filters |
| Geen transacties | supabase.rpc() met PL/pgSQL functie |
| Fire-and-forget webhook | Cloudflare Queue met retry |
| Geen debounce op AJAX | Client-side debounce (300ms) + KV cache |
| Hardcoded posts_per_page=50 | Cursor-based pagination |
| String processing in SQL | Genormaliseerde kolommen + index |
| Static $cache in PHP | SWR cache (30s dedup) |
5. Aanbevelingen Prioriteit
Section titled “5. Aanbevelingen Prioriteit”Verwerk in schema (datamodel-nieuw.md)
Section titled “Verwerk in schema (datamodel-nieuw.md)”| # | Bevinding | Ernst | Status |
|---|---|---|---|
| 1 | assigned_to UUID[] → junction table | Critical | Verwerkt: job_assignments tabel |
| 2 | Contact matching race condition | Critical | Verwerkt: UNIQUE index op email |
| 3 | origin mist ‘website’ op contacts | Critical | Verwerkt: CHECK constraint bijgewerkt |
| 4 | Attachments tabel ontbreekt | Important | Verwerkt: attachments tabel toegevoegd |
| 5 | Activity log UUID → BIGINT PK | Important | Verwerkt: BIGINT IDENTITY |
| 6 | Contacts mist identity constraint | Important | Verwerkt: CHECK (company OR firstname) |
| 7 | Correlated subqueries in job_details view | Important | Verwerkt: LATERAL join |
| 8 | work_order_details mist contactgegevens | Important | Verwerkt: JOIN op effective contact |
| 9 | Material catalog tabel | Important | Verwerkt: material_catalog tabel |
| 10 | RLS user_site_ids() performance | Important | Gedocumenteerd: session variabele aanbeveling |
| 11 | Status transitie validatie | Nice-to-have | Gedocumenteerd: trigger voorbeeld |
| 12 | Activity log partitioning | Nice-to-have | Gedocumenteerd: kwartaal partities |
Verwerk in user-journeys.md + datamodel
Section titled “Verwerk in user-journeys.md + datamodel”| # | Bevinding | Ernst | Status |
|---|---|---|---|
| 1 | revision_requested status op werkbon | Must-fix | Verwerkt in schema + status model |
| 2 | Dagplanning monteur → v1 (was v2) | Must-fix | Verwerkt in roadmap |
| 3 | Klant notificaties → v1 (was v2) | Must-fix | Verwerkt in roadmap |
| 4 | Klant status-check URL (public_token) | Must-fix | Verwerkt in schema (reports tabel) |
| 5 | Email of phone verplicht op webhook | Must-fix | Verwerkt in Zod schema |
| 6 | started_at/completed_at op jobs | Important | Verwerkt in schema |
| 7 | Offline: lokale opslag + sync indicator | Must-fix | Verwerkt in v1 scope |
| 8 | Duplicate-detectie meldingen | Must-fix | Verwerkt in v1 scope |
| 9 | Gestoord tijdens werkbon | Must-fix | Auto-save al in schema |
| 10 | Bijkomend werk op locatie | Should-fix | Gedocumenteerd in edge cases |
| 11 | Bulk triage woningcorporatie | Should-fix | Gedocumenteerd in edge cases |
| 12 | Herplanning bij ziekte | Should-fix | Gedocumenteerd in edge cases |
| 13 | In-app comments beheerder↔monteur | Should-fix | Toegevoegd aan v2 roadmap |
| 14 | Touch targets 48px minimum | Should-fix | Gedocumenteerd |
| 15 | Kleurenblind: iconen naast kleuren | Must-fix | Gedocumenteerd |
| 16 | i18n framework vanaf begin | Nice-to-have | Gedocumenteerd |
Verwerk in migratie-stack.md
Section titled “Verwerk in migratie-stack.md”| # | Bevinding | Ernst | Status |
|---|---|---|---|
| 1 | Blue-green deployment strategie | Critical | Verwerkt: week-voor-week plan |
| 2 | Data validatie queries na migratie | Critical | Verwerkt: 4 validatie queries |
| 3 | Rollback plan | Critical | Verwerkt: 30-dagen strategie |
| 4 | Incoming webhook buiten auth middleware | Critical | Verwerkt: route ordering |
| 5 | Moneybird ID continuïteit | Critical | Verwerkt: fase 1 migratie |
| 6 | Database seeding (addresses) | Critical | Verwerkt: fase 5 migratie |
| 7 | Webhook reliability (Queue) | Important | Verwerkt: Cloudflare Queue |
| 8 | Service layer documenteren | Important | Verwerkt: services/ structuur |
| 9 | Feature parity checklist | Important | Verwerkt: 15-item checklist |
| 10 | Kosten schatting | Important | Verwerkt: ~$5/maand |
| 11 | Telefoon normalisatie porten | Important | Verwerkt: phone.service.ts |
| 12 | Adres cache (KV) | Optimization | Gedocumenteerd |
| 13 | Direct Moneybird API (fase 2) | Optimization | Gedocumenteerd |