As little data as possible
input data:
- Admin Email
- Admin Password
- Org name
Bypasses:
Frontend
OnboardingAdminRequest—first_name,last_name,phone_numberchanged fromrequiredtonullableOnboardingOrgNameRequest—kvk_numberchanged fromrequiredtonullableOnboardingAddressRequest—zipcode,number,street,citychanged fromrequiredtonullableAddress.vue— Frontend address guard replaced with a direct call tovalidateGoNext()(skips the null-check on street/city before posting)OnboardingCareRequest—care_types,care_legislationschanged fromrequiredtonullableOnboardingMentorsTypeRequest—mentor_type_idschanged fromrequiredtonullableOnboardingTerminologyRequest—singular,pluralchanged fromrequiredtonullable
Backend — runtime fixes
Customer.php(addressZipcode mutator) — type hint changed fromstringto?stringto avoid crash on null inputOnboardingController::store()— null fallbacks added for non-nullable DB columns:contact_phone ?? '',address_zipcode ?? '',address_number ?? 0,address_street ?? '',address_city ?? ''
Backend — SetupCustomerAction workarounds
- Terminology — skipped entirely; tenant
terminologytable stays empty (causes UI to fall back to English'client'instead of Dutch'cliënt') - Settings — seeded unconditionally with all-null values;
SaveSettingsActionapplies its own defaults (notablysend_birthday_emails_settings ?? true→ birthday emails default to enabled) - Mentor types — replaced user-selected IDs with unconditional seeding of all 4 enum cases (
Begeleider,Gezinscoördinator,Casemanager,Zorgcoördinator) - Invited colleagues (users) — skipped entirely
- Default location (Hoofdlocatie) — skipped entirely; tenant has zero locations
- Birthday email — skipped entirely
- Introductory meeting email — skipped entirely (Round 3: re-enable
saveIntroductoryMeetingEmail()inSetupCustomerAction— the method uses hardcoded Dutch defaults and needs no user input, so it's safe to call unconditionally)
Results:
- Uses default client (English) for clients instead of clienten (Dutch)
- No zorgtypes are active.
Broken: Cannot activate a client without locations
Changing a client from "Geïnteresseerd" to "Actief" via the Casemanagement tab opens the "Startdatum zorg aanpassen" modal, which has no location field. The backend requires locationIds to be filled in and returns { errors: { locationIds: ["Vul een location ids in"] } }. The frontend swallows this into a generic "Er is een probleem met de ingevulde gegevens" error with no way for the user to fix it — the field simply doesn't exist in the modal. With zero locations in the tenant, activating a client is impossible.
More broadly: UpdateClientCaseManagement always requires location_ids, so every save on the Casemanagement tab fails without a location — not just the status change. This is relevant for organizations that do ambulante begeleiding (visiting clients at home) and have no physical location.
Same issue exists when editing users — UpdateUserRequest also requires location_ids, so editing any user profile fails without at least one location in the tenant.
Broken: Planning infinite redirect loop
With zero active zorgtypes, navigating to /planning causes an infinite redirect loop between ScheduleOverview and AmbulatoryScheduleOverview. Each page's onMounted redirects to the other when its own care type is inactive — with neither active, they bounce forever until the browser throws "Te veel aanroepen van locatie- of geschiedenis-API's in een kort tijdsbestek."
ScheduleOverview.vueline 95: redirects to ambulatory ifisDailyCareTypeActive === falseAmbulatoryScheduleOverview.vueline 97: redirects to schedule ifisAmbulatoryCareTypeActive === false
Neither guard handles the case where both are inactive.
- Hard crash on login if settings row is missing:
No query results for model [App\Models\Customer\Settings]— the app does not gracefully handle a missing settings row; skippingSaveSettingsActionentirely makes the app unusable. - Birthday email setting defaults to
trueregardless of user choice —SaveSettingsActionuses$data->send_birthday_emails_settings ?? true; anynullinput silently enables birthday emails.
other findings unrelated to chaos:
Client portal last login not tracked:
- The "Ninja account" modal shows "Laatste login: Nog nooit ingelogd" even after the client has logged in. Last login timestamp is not being recorded for client portal logins — either the login action doesn't update a
last_login_atfield, or the modal reads from the wrong field/guard.
"Account: Actief" badge is ambiguous:
- The client header shows both "Geïnteresseerd" (workflow status) and "Account: Actief" (portal account exists) side by side. "Actief" here means the portal account is active, not that the client workflow status is active — but placed next to each other they look like two conflicting status indicators for the same thing.
Client status vs portal access:
- A client in "Geïnteresseerd" status (no intake done) can have "Account: Actief" and fully access the client portal — submitting dagreflecties, viewing planning, etc. Portal account activation is independent of the client workflow status. It's unclear whether a geïnteresseerde client should have portal access before an intake is completed.
Dashboard welcome banner:
- "Gefeliciteerd, je account is succesvol aangemaakt" banner appears on the second employee login, not the first. The first-login detection or banner dismissal state is off by one.
Client portal — Gegevens page (address field):
- When a client has no address set, the address field renders the literal string
"null null"twice instead of a dash, empty string, or a proper Dutch empty state. The null values are being concatenated into a string rather than guarded against.
CreateClientFormModal.vue:
- Line 140:
ModalFormRowfor "Begeleiders toewijzen" wraps the mentor type selector (theMultiSelectfor picking e.g. Begeleider/Gezinscoördinator) - Line 141:
FormError property="mentorTypeUserIds"is placed inside that row — so the error formentorTypeUserIdsrenders directly under "Begeleiders toewijzen" - But
mentorTypeUserIdsis actually populated by the next rows (lines 154–173), the per-type user selectors ("Begeleiders" dropdown) The FormError is attached to the wrong parent row. It should sit below the per-type user selector rows, not inside the type picker row. The error message belongs after the dynamically generated mentor user rows, not under the mentor type multiselect.
Frontend says locaties is required, backend does not care.
Client email is not required to create a client (frontend and backend both accept no email). However, if you enable "Uitnodigen per e-mail", email becomes required — but this is only revealed after submitting, not upfront. No inline indication that the field becomes required when the invite option is toggled.
Email templates cannot be created via the UI:
- Instellingen → E-mails only has an edit page, no create page. Email templates (birthday email, introductory meeting email) are exclusively seeded during onboarding. Since we skipped both, the emails page is empty with no way to recover — a tenant in this state would need direct DB intervention to get email templates back.
"Berichten" nav item is a platform subscription module, not a tenant toggle:
- The "Berichten" nav item is gated on
MODULES.MESSAGES_ENABLED, which reads from themessages_enabledcolumn on the centralCustomerrecord. This is a billing/subscription flag toggled by a platform admin in the central admin panel (Customer overview → "Berichten" switch, or the Customer detail subscriptions card). It is not set during onboarding and cannot be enabled by the customer admin themselves.
Birthday email setting defaults to enabled regardless of onboarding choice:
messages_enabled(central, subscription) andsend_birthday_emails_settings(tenant Settings table) are two separate concepts. The birthday email toggle (send_birthday_emails_settings→client_birthday_congratulations_enabledin Settings) can be changed after onboarding via the tenant's own settings UI. However, becauseSaveSettingsActionuses$data->send_birthday_emails_settings ?? true, passingnull(which our workaround does) silently defaults it to enabled — so a tenant that explicitly said "no birthday emails" during onboarding ends up with birthday emails on.
Document categories are legislation-agnostic:
- All 10 document categories (Zorgovereenkomst, Intakeverslag, etc.) are inserted by a tenant migration with hardcoded
requiredandshowcasevalues, regardless of which care legislations (Wmo, Wlz, Jeugdwet) the organization actually operates under. We passed no care legislations during onboarding, yet all categories appeared with the same defaults. Required documents differ per legislation in practice — the current setup doesn't reflect that.
Begeleidertype form — zorgtypes dropdown shows inactive types:
- The "Zorgtypes" dropdown in both the create and edit forms for begeleidertype shows all zorgtypes regardless of their
activeflag. Inactive zorgtypes (e.g. Begeleiding, Dagbesteding when both are deactivated) still appear as selectable options, allowing a mentor type to be linked to an inactive zorgtype. It is also possible to make a new begeleider type with a deactivated zorgtype.
InlineEditModifiers.vue (dagreflectie):
field-keyis passed as a prop butInlineEditModifiershas a fragment root, so Vue cannot auto-inherit the attribute and throws:[Vue warn]: Extraneous non-props attributes (field-key) were passed to component but could not be automatically inherited because component renders fragment or text or teleport root nodes.
ClientsWithTasks.vue (dashboard, removed by EMMIE-1087):
isLoadingis only set tofalseinside awatchonallActiveClients— if the client store loads but returns empty, the watch fires butisLoadingwas never reset, causing the "Taken" widget to spin forever with no empty state or timeout.
Round 2
input data:
- Admin Email
- Admin Password
- Org name
Reapplied createDefaultLocation in SetupCustomerAction. Also made another organization with same parameters except MDO also disabled.
things that work now
Editing user profile (if location is loaded during ProfileController::update() which it isn't). Activating a client (if you know that BSN is also required which is a bit hidden). Making edits in caseload.
Broken by gutting
Terminology
fallback to english clients instead of clienten. Setting backend default to dutch translation should make this easy.
Editing terminology when nothing exists in backend breaks modal for editing client terminology. Line 67: terminology is populated from terminologyStore.all.value — whatever the store fetched from the API. With an empty terminology table in the DB, the store returns an empty array, so v-for="term in terminology" renders nothing. There's no fallback to seed or create missing rows — the modal just shows an empty table with headers and no way for the user to add a term. A tenant in this state would need direct DB intervention to get terminology rows back, same as the email templates. Same as the email templates since it's the same pattern: both are exclusively seeded during onboarding with no UI recovery path.
other findings unrelated to chaos
Location form address errors not shown
Saving a location with invalid address fields (missing postcode, number, street, or city) fails silently — no inline validation errors appear on the form. The backend returns errors keyed as address.zipcode, address.number, etc., but LocationForm.vue passes error-prefix="" to AddressFormColumn, which disables the prefixed FormError wrappers (empty string is falsy). The inner FormError components listen for bare keys (zipcode, number) that never match. Fix: pass error-prefix="address." in LocationForm.vue.
Editing user profile.
ProfileController missing locations eager load
ProfileController::update() and destroyProfilePicture() call UserResource::fromModel(), which accesses $model->locations at line 79. Neither method includes 'locations' in its load()/loadMissing() call, so Laravel throws a LazyLoadingViolationException on every profile save/picture delete.
UserController has the same pattern but works correctly — UpdateUserAction::execute() already loads locations on line 87, and updateCaseloadModeEnabled/destroyProfilePicture both include 'locations' in their loadMissing() calls.
Root cause: location_ids was added to UserResource in March 2026 (commit 07ea06425c1, "medewerkers aan locaties koppelen") without updating ProfileController's load lists. preventLazyLoading() has been active since January 2025, so the crash is guaranteed on any profile edit.
Fix: Add 'locations' to the load() in update() and loadMissing() in destroyProfilePicture() in ProfileController.php.
"Account intrekken" lands on wrong badge state
Pressing "Account intrekken" on an active client account calls updateAccount(clientId, false), which sets has_registered = false on the backend. However, account_invite_sent_at is never cleared — it was set when the invite email was sent and is never touched again.
The badge in ClientAccountStatusBadge.vue determines state as follows:
hasRegistered = true→ "Actief"hasRegistered = false+accountInviteSentAtis set → "Uitgenodigd"hasRegistered = false+ noaccountInviteSentAt→ "Geen"
After revoking, the client lands on "Uitgenodigd" — implying a pending usable invite. But the client_invite_token was cleared to null on registration (the client portal's registration action sets client_invite_token = null and has_registered = true). So the invite is gone, yet the badge suggests otherwise. The client is stuck: they cannot re-register without a new invite being sent manually.
Fix: updateAccount should clear account_invite_sent_at when setting has_registered = false, so the badge correctly falls through to "Geen".
MDO interval setting visible and editable when MDO is disabled
Instellingen → Algemene instellingen always shows the "MDO interval" row, even when MDO_ENABLED is false (MDO subscription off). The edit button works too — the value can be saved to the tenant settings table. GeneralOverview.vue builds the settings list unconditionally — filteredAndSortedSettings only sorts, it never filters by module. The individual client MDO interval row in SettingsRow.vue is correctly gated with v-if="mdoIsEnabled", but the org-level setting has no such guard. Fix: filter the mdoInterval entry from settings.value when !mdoIsEnabled.value, mirroring the same guard already applied in client/fields.ts line 525.
No filter for client portal account status
The client overview has 17 filters (name, workflow status, tasks, locations, dates, etc.) but no filter for portal account state. The "Account: Actief / Uitgenodigd / Geen" badge is visible in the table, but there is no way to filter or export clients by whether they have an active account, a pending invite, or no account at all. Finding all clients with a portal account requires scrolling through the full list manually.
CreateIntakeContactAction crashes on fresh tenants
SaveSettingsAction tries to look up the ID of a "persoonlijk begeleider" relation type to store as default_intake_contact_relation_type_id. Since relation types are never seeded during onboarding, this lookup returns null. CreateIntakeContactAction explicitly guards against null and throws RuntimeException: "default_intake_contact_relation_type_id setting is not configured" — so any flow that auto-creates an intake contact hard crashes on every real new tenant. Not caused by our gutted onboarding; it affects normal onboarding too.
We should add the following defaults during onboarding:
moedervaderwettelijke vertegenwoordiger(less descriptiveparentoption)wmo-consultantpersoonlijke begeleider
Client comment field is never seeded: The comment column on the clients table (the inline "Notitie" widget on the client overview) is never populated by ClientFactory. Every seeded client shows "Notitie toevoegen" with nothing there.
Maybe a warning about missing BSN when trying to activate a client before trying to activate them.
Round 2.5 because I was sleepy last time
input data:
- Admin Email
- Admin Password
- Org name
Reapplied createDefaultLocation in SetupCustomerAction. Also made another organization with same parameters except MDO also disabled. Also enabled birthday emails for fun.
Zero active zorgtypes
The backend has a guard in UpdateProvidedCareTypeAction that throws LastActiveCareTypeException when deactivating the last active care type ("Er moet altijd een vorm van geleverde zorg actief zijn"). This makes zero active care types unreachable through the normal UI — only reachable by gutting the onboarding as we did.
Consequences with zero active zorgtypes:
- Planning redirect loop (already documented in round 1) —
ScheduleOverviewredirects to ambulatory when daily is inactive;AmbulatoryScheduleOverviewredirects back to daily when ambulatory is inactive. Both inactive = infinite loop. - Side nav Planning link — defaults to daily schedule when ambulatory is inactive, immediately entering the redirect loop.
- Global search Planning entry — same pattern, same loop entry point.
- Schedule Layout tabs — both tabs hidden by
v-if, leaving an empty tab bar above the redirect loop. - Registration creation — unscheduled client lists are silently empty;
unscheduledRegistrations.tsfilters out both lists when their respective care type is inactive. No error, just nothing to act on.
Round 3 implication: Care type selection during onboarding must stay required. The backend guard enforces it post-setup but onboarding needs to enforce it too, so a tenant never starts in this state.
Transactional emails are not tenant-customizable
There are two tiers of automated emails in the system:
Tenant-customizable (stored in Email table, editable via Instellingen → E-mails):
- Birthday email (
EmailNameEnum::CLIENT_BIRTHDAY) - Introductory meeting email (
EmailNameEnum::INTRODUCTORY_MEETING)
Hardcoded Mailables (PHP classes, no tenant control):
RegisterClientAccount— client portal inviteClientComplementInvite— complement/data request inviteUserInvite— employee inviteUserInviteColleague— colleague invite
Tenants cannot customize the content or language of transactional emails. An organization with English-speaking clients receives a hardcoded Dutch account invite with no way to change it. The berichten module is a separate paid feature — these are operational emails every tenant needs regardless.
Revamp implication: Transactional emails should become tenant-customizable templates in the Email table, seeded with Dutch defaults during onboarding and editable after, consistent with how birthday and introductory meeting emails already work.
Registraties zorgtype filter shows non-existent types
The daily registrations view has a zorgtype filter that shows zorgtypes even when the tenant has none configured (zero active zorgtypes). Either the filter reads from a hardcoded enum on the frontend rather than the tenant's active zorgtypes from the API, or the backend returns all types regardless of the active flag.
No email preview feature
There is no way to preview an email template before it is sent. The only way to verify content and variable rendering is to trigger the actual flow and check the mail client. This surfaces a related problem: when onboarding data is incomplete (null first name, null address, empty org details), template tags that depend on that data silently render as empty strings or fallback values — "Beste Onbekend," "0 in ." — with no upfront indication that the tag has nothing to resolve. A preview feature would make this visible before a real email reaches a client or employee.
Round 3
input data:
- Admin Email
- Admin Password
- Org name
Default seeded
- CreateDefaultLocation (but no address like before)
- both care types (preventing planning from hardcrashing)
- Care legislations (financing problems, quite small)
- Default dutch terminology (if none given, use defaults)
- Kennismakingsgesprek email.
Admin user not linked to default location
CreateAdminUserAction never assigns the admin user to any location. CreateDefaultLocationAction runs after the admin user is created and does not link existing users to the new location either. As a result, the onboarding admin has zero locations — which excludes them from bulk messages when a location filter is applied (even with only one location in the tenant). The admin needs to manually link themselves to the hoofdlocatie after onboarding.
Update on warning-banner second-login.
The "Gefeliciteerd, je account is succesvol aangemaakt" dashboard banner appears on the second login for the admin who completed onboarding. This appears to be a problem exclusive for the admin who created the organisation.
Missing location address not handled gracefully
The default location is created during onboarding but has no address (street, city, postal code). Several places in the app that display or use location address data do not guard against null — they silently render empty strings, zeroes, or broken output rather than showing a clear empty state or prompting the user to complete their location details. A new tenant who skips the address step during onboarding has no obvious indication that missing address data will affect anything downstream. Observed on the admin domain customer detail page — the address fields simply render blank with no prompt to complete them.
Bezetting mini thing shows a NaN/0 on absolute values.
Administratief Medewerker cannot access Administratie despite the name
Navigating to administratie as an Administratief Medewerker triggers multiple permission error toasts (3–4) in sequence from the cached-store-check middleware firing store refreshes the role lacks permission for, then a route guard redirects them back to the dashboard. The net experience is a flurry of error toasts followed by a silent redirect — no clean "geen toegang" page, no explanation. The role name implies administrative access but the actual permission set does not include it.
Administratief Medewerker missing multiple READ permissions
The PermissionSeeder grants READ for provided-care-types and opening-days to Beheerder, Zorgcoördinator, Begeleider, and Planner — but not Administratief Medewerker (role ID 4). These are likely not the only gaps; the role appears to be systematically excluded from many basic READ operations despite the name implying administrative access.
Compounding issue: cached-store-check fires unconditionally — cached-store-check.ts registers global store refreshes (including providedCareTypeStore) that fire for all authenticated users on every API response that contains a changed hash, regardless of role. An Administratief Medewerker gets a permission error toast on any navigation that triggers a hash bump — observed when navigating to the Medewerkers page. The middleware has no role-gate before dispatching the refresh.
Side-effect: location opening days display as 0/0 for this role even when days exist, because the READ request 403s silently and the store stays empty.