+
+ );
+}
diff --git a/docs/zgw-implementation.md b/docs/zgw-implementation.md
index ef19b0df..b45a65de 100644
--- a/docs/zgw-implementation.md
+++ b/docs/zgw-implementation.md
@@ -1,303 +1,303 @@
-# ZGW Implementation Knowledge Base
-
-Shared knowledge file for sub-agents working on Procest's ZGW API implementation.
-**Read this file before starting work. Append new learnings at the bottom.**
-
-## Architecture
-
-### Controller Split (per ZGW register)
-
-| Controller | Register | zgwApi value | Resources |
-|---|---|---|---|
-| `ZrcController` | Zaken | `zaken` | zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten, zaakobjecten, klantcontacten |
-| `ZtcController` | Catalogi | `catalogi` | catalogussen, zaaktypen, statustypen, resultaattypen, roltypen, eigenschappen, informatieobjecttypen, besluittypen, zaaktype-informatieobjecttypen |
-| `BrcController` | Besluiten | `besluiten` | besluiten, besluitinformatieobjecten |
-| `DrcController` | Documenten | `documenten` | enkelvoudiginformatieobjecten, objectinformatieobjecten, gebruiksrechten, verzendingen |
-| `NrcController` | Notificaties | `notificaties` | kanaal, abonnement |
-| `AcController` | Autorisaties | `autorisaties` | applicaties (uses ConsumerMapper, NOT OpenRegister objects) |
-
-### Shared Service: `ZgwService`
-
-All controllers depend on `ZgwService` (`lib/Service/ZgwService.php`). Key methods:
-
-**CRUD orchestration** (handles auth, mapping, validation, save, notification):
-- `handleIndex(IRequest, zgwApi, resource)` — paginated list
-- `handleCreate(IRequest, zgwApi, resource, ?zaakClosed, hasForceer)` — create with business rules
-- `handleShow(IRequest, zgwApi, resource, uuid)` — get single
-- `handleUpdate(IRequest, zgwApi, resource, uuid, partial, ?parentZtDraft, ?zaakClosed, hasForceer)` — PUT/PATCH
-- `handleDestroy(IRequest, zgwApi, resource, uuid, ?parentZtDraft, ?zaakClosed, hasForceer)` — DELETE
-
-**Utility methods:**
-- `validateJwtAuth(IRequest)` — returns JSONResponse on failure, null on success
-- `loadMappingConfig(zgwApi, resource)` — loads Twig mapping from IAppConfig
-- `getRequestBody(IRequest)` — parses JSON body (with malformed JSON fallback)
-- `buildBaseUrl(IRequest, zgwApi, resource)` — constructs ZGW-style URL
-- `createOutboundMapping/createInboundMapping(mappingConfig)` — builds Mapping objects
-- `applyOutboundMapping/applyInboundMapping(...)` — executes Twig-based field translation
-- `translateQueryParams(params, mappingConfig)` — ZGW query params to OpenRegister filters
-- `consumerHasScope(IRequest, component, scope)` — checks JWT consumer scopes
-- `publishNotification(zgwApi, resource, resourceUrl, actie)` — sends to NRC subscribers
-- `buildValidationError(ruleResult)` — formats validation error response
-- `unavailableResponse()` / `mappingNotFoundResponse(zgwApi, resource)` — standard error responses
-
-**OpenRegister access:**
-- `getObjectService()` — OpenRegister ObjectService (find, saveObject, deleteObject, buildSearchQuery, searchObjectsPaginated)
-- `getConsumerMapper()` — OpenRegister ConsumerMapper (for AC)
-- `getZgwMappingService()` — Procest's ZgwMappingService (IAppConfig storage)
-- `getBusinessRulesService()` — ZgwBusinessRulesService
-- `getDocumentService()` — ZgwDocumentService (file storage)
-- `getLogger()` — PSR LoggerInterface
-
-**Cross-register resolvers:**
-- `resolveZaakClosed(resource, existingData)` — checks if zaak has einddatum (for zrc-007)
-- `resolveZaakClosedFromBody(resource, body)` — same but from request body (sub-resource creation)
-- `resolveParentZaaktypeDraft(resource, existingData)` — checks if parent zaaktype is concept (for ztc-010)
-
-### Other Services
-
-- `ZgwBusinessRulesService` — validates VNG business rules before save. Call via `zgwService->getBusinessRulesService()->validate(...)`
-- `ZgwMappingService` — stores/retrieves Twig mapping configs from IAppConfig
-- `ZgwPaginationHelper` — wraps results in ZGW HAL-style `{count, next, previous, results}`
-- `ZgwDocumentService` — stores binary files in Nextcloud filesystem at `/admin/files/procest/documenten/{uuid}/{filename}`
-- `NotificatieService` — delivers notifications to NRC subscribers via HTTP POST
-
-## Business Rules by Register
-
-### ZRC (Zaken)
-- **zrc-007**: Closed zaak protection — zaak sub-resources cannot be modified when the parent zaak has an `einddatum`, unless the consumer has `zaken.geforceerd-bijwerken` scope
-- **zrc-007a**: When creating a status whose statustype has `isEindstatus=true`, automatically set the parent zaak's `einddatum` to the `datumStatusGezet` date
-- Zaakeigenschappen are nested sub-resources (`/zaken/{zaakUuid}/zaakeigenschappen`)
-- `_zoek` endpoint delegates to index and returns HTTP 201 (not 200)
-
-### ZTC (Catalogi)
-- **ztc-010**: Sub-resources of a published (non-concept) zaaktype cannot be modified or deleted
-- **ztc-004**: Resultaattype `afleidingswijze` in [eigenschap, zaakobject, ander_datumkenmerk] requires `datumkenmerk`
-- **ztc-005**: `afleidingswijze` in [afgehandeld, termijn] forbids `einddatumBekend=true`
-- **ztc-006**: `afleidingswijze` in [zaakobject, ander_datumkenmerk] requires `objecttype`
-- Publish endpoints set `isDraft=false` on zaaktypen, besluittypen, informatieobjecttypen
-
-### DRC (Documenten)
-- **drc-009**: Document must be locked before updates. Lock ID must be provided and must match.
-- Binary content (`inhoud`) is stored as base64 in the request, decoded and saved to filesystem
-- `inhoud` is NOT stored in OpenRegister — only as a Nextcloud file
-- Lock/unlock uses `locked` (bool) and `lockId` (string) fields on the OpenRegister object
-- On destroy, stored files must be cleaned up via `documentService->deleteFiles(uuid)`
-
-### BRC (Besluiten)
-- **brc-001**: Standard besluit CRUD (create, update, patch) with besluittype validation
-- **brc-002**: Identificatie uniqueness under verantwoordelijke_organisatie; immutable on update
-- **brc-003a**: BIO informatieobject URL validation — must resolve to a valid EIO
-- **brc-004a/b**: BesluitInformatieObject is immutable — PUT/PATCH returns 405
-- **brc-005a**: Cross-register OIO sync — creating a BIO also creates an OIO in DRC with objectType=besluit
-- **brc-005b**: Deleting a BIO also deletes the corresponding OIO in DRC
-- **brc-006a**: Zaak-besluit relation — checks both directions (BT.caseTypes -> ZT UUID, and ZT.decisionTypes -> BT omschrijving/UUID)
-- **brc-007**: BesluitInformatieObject validates that informatieobjecttype is in besluittype.informatieobjecttypen
-- **brc-008a**: BIO create validates IOT is in BT.informatieobjecttypen
-- **brc-009**: Cascade delete — deleting a besluit also deletes related BIOs and their OIOs in DRC; audit trail returns 404 for deleted besluiten
-
-### NRC (Notificaties)
-- `notificatieCreate` endpoint just echoes the body back with HTTP 201
-- Standard CRUD for kanaal and abonnement resources
-
-### AC (Autorisaties)
-- Completely custom — maps OpenRegister Consumers to ZGW Applicatie format
-- Does NOT use the standard CRUD flow (no Twig mapping, no ObjectService)
-- `show('consumer')` with `?clientId=...` is a special lookup pattern
-
-## ZGW Standard Quirks & Workarounds
-
-### Malformed JSON in VNG test collections
-The VNG Postman test collections sometimes send unquoted Postman variables in JSON bodies (e.g., `"field": {{var}}` instead of `"field": "{{var}}"`). The `getRequestBody()` method in ZgwService handles this with a regex fallback that quotes unquoted values.
-
-### Boolean normalization
-OpenRegister may store booleans as strings (`"true"`, `"1"`) or integers (`1`, `0`). Always normalize before comparing:
-```php
-if ($val === 'true' || $val === '1' || $val === 1) { $val = true; }
-```
-
-### Identifier type casting
-OpenRegister DB may return `identifier` as an integer even when stored as string. Always cast before saving back:
-```php
-if (isset($data['identifier']) && is_int($data['identifier'])) {
- $data['identifier'] = (string) $data['identifier'];
-}
-```
-
-### PATCH merge strategy
-For partial updates, only English fields whose corresponding ZGW fields were in the request body should be merged. The reverse mapping is inspected to determine which English keys correspond to which ZGW fields. Existing array values must be re-encoded as JSON strings before merging (OpenRegister deserializes them).
-
-### UUID extraction from URLs
-ZGW resources reference each other by full URL (`http://host/api/zgw/zaken/v1/zaken/{uuid}`). Extract UUIDs with:
-```php
-preg_match('/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i', $url, $matches);
-```
-
-### OpenRegister ObjectService API
-- `find($uuid, register: $reg, schema: $schema)` — may return object or array
-- `saveObject(register: $reg, schema: $schema, object: $data, uuid: $uuid)` — uuid optional for create
-- `deleteObject(uuid: $uuid)` — delete by UUID
-- `buildSearchQuery(requestParams: [...], register: $reg, schema: $schema)` — build query
-- `searchObjectsPaginated(query: $query)` — returns `['results' => [...], 'total' => N]`
-- Always handle both array and object returns: `is_array($obj) ? $obj : $obj->jsonSerialize()`
-
-## Testing
-
-### Per-register test commands
-```bash
-# Run inside container or let the script delegate
-bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder setUp # Initialize test data
-bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder ZRC
-bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder ZTC
-bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder DRC
-bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder BRC
-bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder NRC
-bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder AC
-bash procest/tests/zgw/run-zgw-tests.sh --business-only --folder ztc # Business rules
-```
-
-### After making changes
-1. Clear OPcache: `docker exec nextcloud apache2ctl graceful`
-2. Run the relevant register's tests
-3. Compare failures against baseline — new failures = regression
-
-### Known pre-existing test failures (baseline 2026-03-08)
-These failures exist before the controller split and are NOT regressions:
-- **ZRC**: zaakobjecten CRUD fails due to unresolved `{{zaakobject_url}}` Postman variable
-- **ZTC**: zaaktype-informatieobjecttypen and zaaktypen PATCH/DELETE have comparison/status issues
-- **DRC**: objectinformatieobjecten fail due to unresolved `{{objectinformatieobject_url}}`
-- **BRC**: besluitinformatieobjecten fail due to unresolved `{{bio_url}}`
-- **AC**: applicatie delete fails due to unresolved `{{created_applicatie_url}}`
-- **NRC**: All tests pass
-
----
-
-## Learnings Log
-
-_Sub-agents: append new discoveries below this line. Include the date, register, and what you learned._
-
-### 2026-03-08 — DRC: drc-009 lock enforcement response format
-
-The VNG business rules tests for drc-009 expect error responses in a specific `invalidParams` format:
-```json
-{
- "detail": "...",
- "invalidParams": [
- { "name": "...", "code": "...", "reason": "..." }
- ]
-}
-```
-
-Key distinctions:
-- **drc-009a/b** (update while unlocked): `name: 'nonFieldErrors'`, `code: 'unlocked'`
-- **drc-009d** (PUT without lock): `name: 'lock'`, `code: 'required'` (field-level validation)
-- **drc-009e** (PATCH without lock): `name: 'nonFieldErrors'`, `code: 'missing-lock-id'` (non-field error)
-- **drc-009h/i** (wrong lock ID): `name: 'nonFieldErrors'`, `code: 'incorrect-lock-id'`
-
-The PUT vs PATCH distinction matters: PUT treats `lock` as a required field, PATCH treats it as a missing lock enforcement error.
-
-### 2026-03-08 — DRC: Force unlock scope check
-
-Force unlock (drc-009k) requires checking `documenten.geforceerd-bijwerken` scope via `consumerHasScope()`. When the lock ID doesn't match or is missing, the unlock endpoint checks if the consumer has this scope. If yes, force unlock succeeds (204). If no, returns 400.
-
-The `consumerHasScope()` method returns `true` (bypass) when:
-- `consumerMapper` is null (OpenRegister not loaded)
-- No client_id in JWT
-- Consumer not found in database
-- Consumer has `superuser: true`
-
-### 2026-03-08 — DRC: OAS test lock status code
-
-The OAS test collection expects lock to return HTTP 201, while the business rules test (drc-009c) expects HTTP 200. The ZGW standard specifies 200 for lock. The lock endpoint returns 200.
-
-The OAS unlock test expects 201, but the ZGW standard and business rules (drc-009k) expect 204. The unlock endpoint returns 204.
-
-### 2026-03-08 — DRC: Boolean normalization for locked field
-
-OpenRegister may store boolean fields as strings (`"true"`, `"1"`) or integers (`1`, `0`). The lock/unlock/checkDocumentLock methods must normalize the `locked` field value before comparison.
-
-### 2026-03-08 — ZRC business rules implementation
-
-**zrc-002a**: Unique identificatie enforcement. Added `checkIdentificatieUnique()` in `ZgwBusinessRulesService` that searches OpenRegister for existing zaken with the same `identifier` (and `sourceOrganisation`). Returns 400 with `identificatie-niet-uniek` error code.
-
-**zrc-003d**: Invalid informatieobject URL validation. Added `validateInformatieobjectUrl()` that validates both URL format AND that the UUID resolves to an actual document in OpenRegister (`document_schema`).
-
-**zrc-004a/b/c**: ZaakInformatieObject enrichment. Business rules now set `aardRelatieWeergave = "Hoort bij, omgekeerd: kent"` and `registratiedatum = date("Y-m-d")` on create. These values are forced immutable on update/patch. Since the `caseDocument` schema does NOT have a `relationshipType` field, these values are injected directly into the outbound response at the controller level via `enrichZioResponse()` and `enrichZioJsonResponse()`.
-
-**zrc-005a/b**: Cross-register OIO sync. `ZrcController::create()` now calls `syncCreateObjectInformatieObject()` after creating a ZaakInformatieObject, which creates a corresponding ObjectInformatieObject in the DRC register. `ZrcController::destroy()` captures ZIO data before deletion and calls `syncDeleteObjectInformatieObject()` on success. OIO search uses `relatedObject` and `document` fields from the `objectinformatieobject` mapping config.
-
-**zrc-006a/b/c**: Authorization-based filtering. Added `getConsumerAuthorisaties()` to `ZgwService` that returns the consumer's per-component authorization entries. `ZrcController::index()` now calls `filterZakenByAuthorisation()` which filters results based on `maxVertrouwelijkheidaanduiding`. `show()` calls `checkZaakReadAccess()` for the same check on individual zaken. `create()` checks `zaken.aanmaken` scope. All return 403 with `{"code": "permission_denied"}`.
-
-**zrc-007**: Closed zaak protection. The validation error now includes a top-level `code: "permission_denied"` field in the response, matching the VNG test expectation (`pm.response.json().code`). This required changes to both `ZgwBusinessRulesService` (adding `'code' => 'permission_denied'` to the rule result) and `ZgwService::buildValidationError()` (propagating the `code` field to the response data).
-
-**Key finding**: The `VERTROUWELIJKHEID_LEVELS` ordering for authorization filtering is: openbaar(1) < beperkt_openbaar(2) < intern(3) < zaakvertrouwelijk(4) < vertrouwelijk(5) < confidentieel(6) < geheim(7) < zeer_geheim(8). Consumer's `maxVertrouwelijkheidaanduiding` sets the ceiling — zaken with a higher level are filtered out.
-
-### 2026-03-08 — AC business rules implementation
-
-**ac-001**: ClientId uniqueness. `validateClientIdUniqueness()` iterates all existing consumers and checks both the primary `name` field and any extra clientIds stored in `authorizationConfiguration.clientIds`. Returns 400 with `clientId-exists` error code.
-
-**ac-002a/b**: heeftAlleAutorisaties consistency. `validateAutorisatieConsistency()` checks: if `heeftAlleAutorisaties=true` and `autorisaties` is non-empty, returns 400 with `ambiguous-authorizations-specified`. If `heeftAlleAutorisaties=false` and `autorisaties` is empty (and explicitly provided), returns 400 with `missing-authorizations`.
-
-**ac-003a-f**: Scope-based field validation. `validateAutorisatieScopes()` checks each autorisatie entry: for `zrc` component with scope containing "zaken", requires `zaaktype` and `maxVertrouwelijkheidaanduiding`. For `drc` with "documenten", requires `informatieobjecttype` and `maxVertrouwelijkheidaanduiding`. For `brc` with "besluiten", requires `besluittype`.
-
-**Multiple clientIds support**: The Consumer entity stores only one `name`, so extra clientIds beyond the first are stored in `authorizationConfiguration.clientIds`. The `consumerToApplicatie()` method reconstructs the full list.
-
-**Validation ordering**: Business rules (ac-002, ac-003) must run BEFORE uniqueness checks (ac-001). If uniqueness fires first on test data with pre-existing clientIds, the actual business rule errors are masked.
-
-**Bug fix**: `ConsumerMapper::createFromArray()` parameter is named `$object` not `$data` -- using named parameter `data:` caused "Unknown named parameter $data" errors. Fixed to use `object:`.
-
-**Index filter**: Added support for both `clientId` (singular) and `clientIds` (plural) query parameters, since the OAS cleanup test uses `clientIds` (plural).
-
-**Pre-existing setUp issue**: The "Create Zaaktype" step in the ac business rules setUp has a TypeError in its test script (`Cannot read properties of undefined (reading '0')`) -- this is a pre-existing Postman collection issue, not an AC regression.
-
-## ZTC Cross-Reference URL Enrichment (ztc-0xx)
-
-### Architecture
-
-ZTC types (zaaktypen, besluittypen) contain cross-reference arrays (informatieobjecttypen, besluittypen, deelzaaktypen, gerelateerdeZaaktypen) that must return valid URLs pointing to published, date-valid objects. This is implemented in two phases:
-
-**Write path (business rules)**: `ZgwZtcRulesService` resolves omschrijving/identificatie strings to object UUIDs at creation time, storing them via `_directFields` to bypass Twig mapping limitations with arrays.
-
-**Read path (enrichment)**: `ZtcController::enrichZaaktype()` and `enrichBesluittype()` expand stored UUIDs to full URLs, using identifier-based expansion to include all versions of the same logical type. `filterValidUrls()` then removes concept or date-invalid entries.
-
-### Key Patterns
-
-**`_directFields` mechanism**: Array fields that Twig cannot handle (drops to empty strings) are stored via a special `_directFields` key in the enriched body. `ZgwService::handleCreate()` and `handleUpdate()` extract these and merge them directly into the English data, bypassing the Twig mapping.
-
-**Identifier-based expansion at read time**: For deelzaaktypen and gerelateerdeZaaktypen, the enrichment code looks up each stored UUID's identifier, then finds ALL objects with that identifier. This ensures that ZT2 (created after ZT1 but with the same identifier) appears in ZT1's deelzaaktypen even though ZT2 didn't exist when ZT1 was created.
-
-**ZIOT-based IOT enrichment**: informatieobjecttypen on zaaktypen are NOT stored directly. Instead, ZIOT (zaaktype-informatieobjecttype) records link ZTs to IOTs. The enrichment queries ZIOTs for the zaaktype, looks up each IOT's name, finds ALL IOTs with that name, and lets filterValidUrls select valid ones.
-
-### Schema Considerations
-
-**`relatedCaseTypes` must be type `array` in OR schema (not `string`)**: OpenRegister auto-parses JSON strings back to arrays. If the schema says `string`, the PATCH flow fails because: (1) existing data is read as array (OR auto-parsed it), (2) json_encode for Twig, (3) after merge, json_decode back to array, (4) OR rejects "expected string, got array". Changing the schema to `array` with items of type `object` resolves this.
-
-**`$arrayKeys` tracking in PATCH flow**: `ZgwService::handleUpdate()` tracks which existing fields were originally arrays before json_encoding them for Twig. After merge, only those fields get decoded back to arrays. This prevents string-typed fields containing JSON from being incorrectly decoded.
-
-### ZIOT omschrijving resolution
-
-VNG tests may send ZIOT `informatieobjecttype` as an omschrijving string that happens to be UUID-shaped. `rulesZaaktypeinformatieobjecttypenCreate` handles this by: (1) if it's a URL, keep as-is; (2) if it's a bare UUID, verify it exists in OR -- if not, fall back to name-based lookup; (3) if not a UUID, resolve by name. This prevents storing non-existent UUIDs when the value is actually an omschrijving that looks like a UUID.
-
-### 2026-03-08 — BRC business rules implementation
-
-**brc-003a fix**: The `validateInformatieobjectUrl()` in `ZgwRulesBase` had a bug where `extractUuid()` returning null caused the validation to silently pass (the `if ($ioUuid !== null && $this->objectService !== null)` condition was skipped). Fixed by adding an explicit null check that returns 400 when UUID extraction fails.
-
-**brc-005a/b fix (OpenRegister)**: `MetadataHydrationHandler::hydrateObjectMetadata()` line 100 assumed `$objectData['object']` was always a nested array, but ObjectInformatieObject has `object` as a URL string. When the OIO data had `['document' => url, 'object' => url, 'objectType' => 'besluit']`, the code used the URL string as the business data array, causing a TypeError. Fixed by adding `is_array()` check.
-
-**brc-006a**: ZGW zaak-besluit relation requires checking BOTH directions: (1) BesluitType.caseTypes contains the zaaktype UUID, and (2) ZaakType.decisionTypes contains the BesluitType omschrijving or UUID. The `decisionTypes` array field was added to the caseType schema and the zaaktype mapping was updated to store `besluittypen` as `decisionTypes`.
-
-**brc-009c/d/e (cascade delete)**: Deleting a besluit must cascade delete BIOs and OIOs. The key challenge was that OpenRegister's `ObjectService::deleteObject()` performs a soft delete via `ObjectEntityMapper::update()`, but the update method checks `shouldUseMagicMapper` which reads the register's `configuration` JSON. If the register has no magic mapping configuration, the update falls through to the blob table path, leaving the magic table row unchanged.
-
-**Fix**: Register 7 (Procest) needed explicit magic mapping configuration (`{"schemas": {"": {"magicMapping": true}}}`) set on its `configuration` column. Without this, `isMagicMappingEnabledForSchema()` returns false and soft deletes in magic tables silently fail.
-
-**Important**: When calling `deleteObject()` during cascade operations (where we're deleting related objects, not the primary resource), pass `_rbac: false, _multitenancy: false` to avoid permission issues with related objects in other schemas.
-
-**Audit trail for deleted resources**: `BrcController::audittrailIndex()` checks if the parent resource exists before returning audit trail data. If the resource was soft-deleted, `find()` throws `DoesNotExistException`, and the controller returns 404. This satisfies brc-009d.
-
-### 2026-03-08 — OpenRegister Magic Mapper soft delete gotcha
-
-**CRITICAL**: OpenRegister's `ObjectService::deleteObject()` performs soft delete by calling `ObjectEntityMapper::update()`. But `update()` checks `shouldUseMagicMapperForRegisterSchema()` which reads `Register::isMagicMappingEnabledForSchema()`. If the register's `configuration` column is NULL or doesn't have the schema listed with `magicMapping: true`, the update falls through to the blob table `parent::update()` call, which operates on `oc_openregister_objects` (the blob table). Since the object only exists in the magic table (`oc_openregister_table_{register}_{schema}`), the soft delete appears to succeed (returns true) but the magic table row is NOT updated.
-
-**Workaround**: Ensure the register has proper `configuration` JSON with all schemas listed. Example:
-```json
-{"schemas": {"decisionDocument": {"magicMapping": true}, "decision": {"magicMapping": true}}}
-```
+# ZGW Implementation Knowledge Base
+
+Shared knowledge file for sub-agents working on Procest's ZGW API implementation.
+**Read this file before starting work. Append new learnings at the bottom.**
+
+## Architecture
+
+### Controller Split (per ZGW register)
+
+| Controller | Register | zgwApi value | Resources |
+|---|---|---|---|
+| `ZrcController` | Zaken | `zaken` | zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten, zaakobjecten, klantcontacten |
+| `ZtcController` | Catalogi | `catalogi` | catalogussen, zaaktypen, statustypen, resultaattypen, roltypen, eigenschappen, informatieobjecttypen, besluittypen, zaaktype-informatieobjecttypen |
+| `BrcController` | Besluiten | `besluiten` | besluiten, besluitinformatieobjecten |
+| `DrcController` | Documenten | `documenten` | enkelvoudiginformatieobjecten, objectinformatieobjecten, gebruiksrechten, verzendingen |
+| `NrcController` | Notificaties | `notificaties` | kanaal, abonnement |
+| `AcController` | Autorisaties | `autorisaties` | applicaties (uses ConsumerMapper, NOT OpenRegister objects) |
+
+### Shared Service: `ZgwService`
+
+All controllers depend on `ZgwService` (`lib/Service/ZgwService.php`). Key methods:
+
+**CRUD orchestration** (handles auth, mapping, validation, save, notification):
+- `handleIndex(IRequest, zgwApi, resource)` — paginated list
+- `handleCreate(IRequest, zgwApi, resource, ?zaakClosed, hasForceer)` — create with business rules
+- `handleShow(IRequest, zgwApi, resource, uuid)` — get single
+- `handleUpdate(IRequest, zgwApi, resource, uuid, partial, ?parentZtDraft, ?zaakClosed, hasForceer)` — PUT/PATCH
+- `handleDestroy(IRequest, zgwApi, resource, uuid, ?parentZtDraft, ?zaakClosed, hasForceer)` — DELETE
+
+**Utility methods:**
+- `validateJwtAuth(IRequest)` — returns JSONResponse on failure, null on success
+- `loadMappingConfig(zgwApi, resource)` — loads Twig mapping from IAppConfig
+- `getRequestBody(IRequest)` — parses JSON body (with malformed JSON fallback)
+- `buildBaseUrl(IRequest, zgwApi, resource)` — constructs ZGW-style URL
+- `createOutboundMapping/createInboundMapping(mappingConfig)` — builds Mapping objects
+- `applyOutboundMapping/applyInboundMapping(...)` — executes Twig-based field translation
+- `translateQueryParams(params, mappingConfig)` — ZGW query params to OpenRegister filters
+- `consumerHasScope(IRequest, component, scope)` — checks JWT consumer scopes
+- `publishNotification(zgwApi, resource, resourceUrl, actie)` — sends to NRC subscribers
+- `buildValidationError(ruleResult)` — formats validation error response
+- `unavailableResponse()` / `mappingNotFoundResponse(zgwApi, resource)` — standard error responses
+
+**OpenRegister access:**
+- `getObjectService()` — OpenRegister ObjectService (find, saveObject, deleteObject, buildSearchQuery, searchObjectsPaginated)
+- `getConsumerMapper()` — OpenRegister ConsumerMapper (for AC)
+- `getZgwMappingService()` — Procest's ZgwMappingService (IAppConfig storage)
+- `getBusinessRulesService()` — ZgwBusinessRulesService
+- `getDocumentService()` — ZgwDocumentService (file storage)
+- `getLogger()` — PSR LoggerInterface
+
+**Cross-register resolvers:**
+- `resolveZaakClosed(resource, existingData)` — checks if zaak has einddatum (for zrc-007)
+- `resolveZaakClosedFromBody(resource, body)` — same but from request body (sub-resource creation)
+- `resolveParentZaaktypeDraft(resource, existingData)` — checks if parent zaaktype is concept (for ztc-010)
+
+### Other Services
+
+- `ZgwBusinessRulesService` — validates VNG business rules before save. Call via `zgwService->getBusinessRulesService()->validate(...)`
+- `ZgwMappingService` — stores/retrieves Twig mapping configs from IAppConfig
+- `ZgwPaginationHelper` — wraps results in ZGW HAL-style `{count, next, previous, results}`
+- `ZgwDocumentService` — stores binary files in Nextcloud filesystem at `/admin/files/procest/documenten/{uuid}/{filename}`
+- `NotificatieService` — delivers notifications to NRC subscribers via HTTP POST
+
+## Business Rules by Register
+
+### ZRC (Zaken)
+- **zrc-007**: Closed zaak protection — zaak sub-resources cannot be modified when the parent zaak has an `einddatum`, unless the consumer has `zaken.geforceerd-bijwerken` scope
+- **zrc-007a**: When creating a status whose statustype has `isEindstatus=true`, automatically set the parent zaak's `einddatum` to the `datumStatusGezet` date
+- Zaakeigenschappen are nested sub-resources (`/zaken/{zaakUuid}/zaakeigenschappen`)
+- `_zoek` endpoint delegates to index and returns HTTP 201 (not 200)
+
+### ZTC (Catalogi)
+- **ztc-010**: Sub-resources of a published (non-concept) zaaktype cannot be modified or deleted
+- **ztc-004**: Resultaattype `afleidingswijze` in [eigenschap, zaakobject, ander_datumkenmerk] requires `datumkenmerk`
+- **ztc-005**: `afleidingswijze` in [afgehandeld, termijn] forbids `einddatumBekend=true`
+- **ztc-006**: `afleidingswijze` in [zaakobject, ander_datumkenmerk] requires `objecttype`
+- Publish endpoints set `isDraft=false` on zaaktypen, besluittypen, informatieobjecttypen
+
+### DRC (Documenten)
+- **drc-009**: Document must be locked before updates. Lock ID must be provided and must match.
+- Binary content (`inhoud`) is stored as base64 in the request, decoded and saved to filesystem
+- `inhoud` is NOT stored in OpenRegister — only as a Nextcloud file
+- Lock/unlock uses `locked` (bool) and `lockId` (string) fields on the OpenRegister object
+- On destroy, stored files must be cleaned up via `documentService->deleteFiles(uuid)`
+
+### BRC (Besluiten)
+- **brc-001**: Standard besluit CRUD (create, update, patch) with besluittype validation
+- **brc-002**: Identificatie uniqueness under verantwoordelijke_organisatie; immutable on update
+- **brc-003a**: BIO informatieobject URL validation — must resolve to a valid EIO
+- **brc-004a/b**: BesluitInformatieObject is immutable — PUT/PATCH returns 405
+- **brc-005a**: Cross-register OIO sync — creating a BIO also creates an OIO in DRC with objectType=besluit
+- **brc-005b**: Deleting a BIO also deletes the corresponding OIO in DRC
+- **brc-006a**: Zaak-besluit relation — checks both directions (BT.caseTypes -> ZT UUID, and ZT.decisionTypes -> BT omschrijving/UUID)
+- **brc-007**: BesluitInformatieObject validates that informatieobjecttype is in besluittype.informatieobjecttypen
+- **brc-008a**: BIO create validates IOT is in BT.informatieobjecttypen
+- **brc-009**: Cascade delete — deleting a besluit also deletes related BIOs and their OIOs in DRC; audit trail returns 404 for deleted besluiten
+
+### NRC (Notificaties)
+- `notificatieCreate` endpoint just echoes the body back with HTTP 201
+- Standard CRUD for kanaal and abonnement resources
+
+### AC (Autorisaties)
+- Completely custom — maps OpenRegister Consumers to ZGW Applicatie format
+- Does NOT use the standard CRUD flow (no Twig mapping, no ObjectService)
+- `show('consumer')` with `?clientId=...` is a special lookup pattern
+
+## ZGW Standard Quirks & Workarounds
+
+### Malformed JSON in VNG test collections
+The VNG Postman test collections sometimes send unquoted Postman variables in JSON bodies (e.g., `"field": {{var}}` instead of `"field": "{{var}}"`). The `getRequestBody()` method in ZgwService handles this with a regex fallback that quotes unquoted values.
+
+### Boolean normalization
+OpenRegister may store booleans as strings (`"true"`, `"1"`) or integers (`1`, `0`). Always normalize before comparing:
+```php
+if ($val === 'true' || $val === '1' || $val === 1) { $val = true; }
+```
+
+### Identifier type casting
+OpenRegister DB may return `identifier` as an integer even when stored as string. Always cast before saving back:
+```php
+if (isset($data['identifier']) && is_int($data['identifier'])) {
+ $data['identifier'] = (string) $data['identifier'];
+}
+```
+
+### PATCH merge strategy
+For partial updates, only English fields whose corresponding ZGW fields were in the request body should be merged. The reverse mapping is inspected to determine which English keys correspond to which ZGW fields. Existing array values must be re-encoded as JSON strings before merging (OpenRegister deserializes them).
+
+### UUID extraction from URLs
+ZGW resources reference each other by full URL (`http://host/api/zgw/zaken/v1/zaken/{uuid}`). Extract UUIDs with:
+```php
+preg_match('/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i', $url, $matches);
+```
+
+### OpenRegister ObjectService API
+- `find($uuid, register: $reg, schema: $schema)` — may return object or array
+- `saveObject(register: $reg, schema: $schema, object: $data, uuid: $uuid)` — uuid optional for create
+- `deleteObject(uuid: $uuid)` — delete by UUID
+- `buildSearchQuery(requestParams: [...], register: $reg, schema: $schema)` — build query
+- `searchObjectsPaginated(query: $query)` — returns `['results' => [...], 'total' => N]`
+- Always handle both array and object returns: `is_array($obj) ? $obj : $obj->jsonSerialize()`
+
+## Testing
+
+### Per-register test commands
+```bash
+# Run inside container or let the script delegate
+bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder setUp # Initialize test data
+bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder ZRC
+bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder ZTC
+bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder DRC
+bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder BRC
+bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder NRC
+bash procest/tests/zgw/run-zgw-tests.sh --oas-only --folder AC
+bash procest/tests/zgw/run-zgw-tests.sh --business-only --folder ztc # Business rules
+```
+
+### After making changes
+1. Clear OPcache: `docker exec nextcloud apache2ctl graceful`
+2. Run the relevant register's tests
+3. Compare failures against baseline — new failures = regression
+
+### Known pre-existing test failures (baseline 2026-03-08)
+These failures exist before the controller split and are NOT regressions:
+- **ZRC**: zaakobjecten CRUD fails due to unresolved `{{zaakobject_url}}` Postman variable
+- **ZTC**: zaaktype-informatieobjecttypen and zaaktypen PATCH/DELETE have comparison/status issues
+- **DRC**: objectinformatieobjecten fail due to unresolved `{{objectinformatieobject_url}}`
+- **BRC**: besluitinformatieobjecten fail due to unresolved `{{bio_url}}`
+- **AC**: applicatie delete fails due to unresolved `{{created_applicatie_url}}`
+- **NRC**: All tests pass
+
+---
+
+## Learnings Log
+
+_Sub-agents: append new discoveries below this line. Include the date, register, and what you learned._
+
+### 2026-03-08 — DRC: drc-009 lock enforcement response format
+
+The VNG business rules tests for drc-009 expect error responses in a specific `invalidParams` format:
+```json
+{
+ "detail": "...",
+ "invalidParams": [
+ { "name": "...", "code": "...", "reason": "..." }
+ ]
+}
+```
+
+Key distinctions:
+- **drc-009a/b** (update while unlocked): `name: 'nonFieldErrors'`, `code: 'unlocked'`
+- **drc-009d** (PUT without lock): `name: 'lock'`, `code: 'required'` (field-level validation)
+- **drc-009e** (PATCH without lock): `name: 'nonFieldErrors'`, `code: 'missing-lock-id'` (non-field error)
+- **drc-009h/i** (wrong lock ID): `name: 'nonFieldErrors'`, `code: 'incorrect-lock-id'`
+
+The PUT vs PATCH distinction matters: PUT treats `lock` as a required field, PATCH treats it as a missing lock enforcement error.
+
+### 2026-03-08 — DRC: Force unlock scope check
+
+Force unlock (drc-009k) requires checking `documenten.geforceerd-bijwerken` scope via `consumerHasScope()`. When the lock ID doesn't match or is missing, the unlock endpoint checks if the consumer has this scope. If yes, force unlock succeeds (204). If no, returns 400.
+
+The `consumerHasScope()` method returns `true` (bypass) when:
+- `consumerMapper` is null (OpenRegister not loaded)
+- No client_id in JWT
+- Consumer not found in database
+- Consumer has `superuser: true`
+
+### 2026-03-08 — DRC: OAS test lock status code
+
+The OAS test collection expects lock to return HTTP 201, while the business rules test (drc-009c) expects HTTP 200. The ZGW standard specifies 200 for lock. The lock endpoint returns 200.
+
+The OAS unlock test expects 201, but the ZGW standard and business rules (drc-009k) expect 204. The unlock endpoint returns 204.
+
+### 2026-03-08 — DRC: Boolean normalization for locked field
+
+OpenRegister may store boolean fields as strings (`"true"`, `"1"`) or integers (`1`, `0`). The lock/unlock/checkDocumentLock methods must normalize the `locked` field value before comparison.
+
+### 2026-03-08 — ZRC business rules implementation
+
+**zrc-002a**: Unique identificatie enforcement. Added `checkIdentificatieUnique()` in `ZgwBusinessRulesService` that searches OpenRegister for existing zaken with the same `identifier` (and `sourceOrganisation`). Returns 400 with `identificatie-niet-uniek` error code.
+
+**zrc-003d**: Invalid informatieobject URL validation. Added `validateInformatieobjectUrl()` that validates both URL format AND that the UUID resolves to an actual document in OpenRegister (`document_schema`).
+
+**zrc-004a/b/c**: ZaakInformatieObject enrichment. Business rules now set `aardRelatieWeergave = "Hoort bij, omgekeerd: kent"` and `registratiedatum = date("Y-m-d")` on create. These values are forced immutable on update/patch. Since the `caseDocument` schema does NOT have a `relationshipType` field, these values are injected directly into the outbound response at the controller level via `enrichZioResponse()` and `enrichZioJsonResponse()`.
+
+**zrc-005a/b**: Cross-register OIO sync. `ZrcController::create()` now calls `syncCreateObjectInformatieObject()` after creating a ZaakInformatieObject, which creates a corresponding ObjectInformatieObject in the DRC register. `ZrcController::destroy()` captures ZIO data before deletion and calls `syncDeleteObjectInformatieObject()` on success. OIO search uses `relatedObject` and `document` fields from the `objectinformatieobject` mapping config.
+
+**zrc-006a/b/c**: Authorization-based filtering. Added `getConsumerAuthorisaties()` to `ZgwService` that returns the consumer's per-component authorization entries. `ZrcController::index()` now calls `filterZakenByAuthorisation()` which filters results based on `maxVertrouwelijkheidaanduiding`. `show()` calls `checkZaakReadAccess()` for the same check on individual zaken. `create()` checks `zaken.aanmaken` scope. All return 403 with `{"code": "permission_denied"}`.
+
+**zrc-007**: Closed zaak protection. The validation error now includes a top-level `code: "permission_denied"` field in the response, matching the VNG test expectation (`pm.response.json().code`). This required changes to both `ZgwBusinessRulesService` (adding `'code' => 'permission_denied'` to the rule result) and `ZgwService::buildValidationError()` (propagating the `code` field to the response data).
+
+**Key finding**: The `VERTROUWELIJKHEID_LEVELS` ordering for authorization filtering is: openbaar(1) < beperkt_openbaar(2) < intern(3) < zaakvertrouwelijk(4) < vertrouwelijk(5) < confidentieel(6) < geheim(7) < zeer_geheim(8). Consumer's `maxVertrouwelijkheidaanduiding` sets the ceiling — zaken with a higher level are filtered out.
+
+### 2026-03-08 — AC business rules implementation
+
+**ac-001**: ClientId uniqueness. `validateClientIdUniqueness()` iterates all existing consumers and checks both the primary `name` field and any extra clientIds stored in `authorizationConfiguration.clientIds`. Returns 400 with `clientId-exists` error code.
+
+**ac-002a/b**: heeftAlleAutorisaties consistency. `validateAutorisatieConsistency()` checks: if `heeftAlleAutorisaties=true` and `autorisaties` is non-empty, returns 400 with `ambiguous-authorizations-specified`. If `heeftAlleAutorisaties=false` and `autorisaties` is empty (and explicitly provided), returns 400 with `missing-authorizations`.
+
+**ac-003a-f**: Scope-based field validation. `validateAutorisatieScopes()` checks each autorisatie entry: for `zrc` component with scope containing "zaken", requires `zaaktype` and `maxVertrouwelijkheidaanduiding`. For `drc` with "documenten", requires `informatieobjecttype` and `maxVertrouwelijkheidaanduiding`. For `brc` with "besluiten", requires `besluittype`.
+
+**Multiple clientIds support**: The Consumer entity stores only one `name`, so extra clientIds beyond the first are stored in `authorizationConfiguration.clientIds`. The `consumerToApplicatie()` method reconstructs the full list.
+
+**Validation ordering**: Business rules (ac-002, ac-003) must run BEFORE uniqueness checks (ac-001). If uniqueness fires first on test data with pre-existing clientIds, the actual business rule errors are masked.
+
+**Bug fix**: `ConsumerMapper::createFromArray()` parameter is named `$object` not `$data` -- using named parameter `data:` caused "Unknown named parameter $data" errors. Fixed to use `object:`.
+
+**Index filter**: Added support for both `clientId` (singular) and `clientIds` (plural) query parameters, since the OAS cleanup test uses `clientIds` (plural).
+
+**Pre-existing setUp issue**: The "Create Zaaktype" step in the ac business rules setUp has a TypeError in its test script (`Cannot read properties of undefined (reading '0')`) -- this is a pre-existing Postman collection issue, not an AC regression.
+
+## ZTC Cross-Reference URL Enrichment (ztc-0xx)
+
+### Architecture
+
+ZTC types (zaaktypen, besluittypen) contain cross-reference arrays (informatieobjecttypen, besluittypen, deelzaaktypen, gerelateerdeZaaktypen) that must return valid URLs pointing to published, date-valid objects. This is implemented in two phases:
+
+**Write path (business rules)**: `ZgwZtcRulesService` resolves omschrijving/identificatie strings to object UUIDs at creation time, storing them via `_directFields` to bypass Twig mapping limitations with arrays.
+
+**Read path (enrichment)**: `ZtcController::enrichZaaktype()` and `enrichBesluittype()` expand stored UUIDs to full URLs, using identifier-based expansion to include all versions of the same logical type. `filterValidUrls()` then removes concept or date-invalid entries.
+
+### Key Patterns
+
+**`_directFields` mechanism**: Array fields that Twig cannot handle (drops to empty strings) are stored via a special `_directFields` key in the enriched body. `ZgwService::handleCreate()` and `handleUpdate()` extract these and merge them directly into the English data, bypassing the Twig mapping.
+
+**Identifier-based expansion at read time**: For deelzaaktypen and gerelateerdeZaaktypen, the enrichment code looks up each stored UUID's identifier, then finds ALL objects with that identifier. This ensures that ZT2 (created after ZT1 but with the same identifier) appears in ZT1's deelzaaktypen even though ZT2 didn't exist when ZT1 was created.
+
+**ZIOT-based IOT enrichment**: informatieobjecttypen on zaaktypen are NOT stored directly. Instead, ZIOT (zaaktype-informatieobjecttype) records link ZTs to IOTs. The enrichment queries ZIOTs for the zaaktype, looks up each IOT's name, finds ALL IOTs with that name, and lets filterValidUrls select valid ones.
+
+### Schema Considerations
+
+**`relatedCaseTypes` must be type `array` in OR schema (not `string`)**: OpenRegister auto-parses JSON strings back to arrays. If the schema says `string`, the PATCH flow fails because: (1) existing data is read as array (OR auto-parsed it), (2) json_encode for Twig, (3) after merge, json_decode back to array, (4) OR rejects "expected string, got array". Changing the schema to `array` with items of type `object` resolves this.
+
+**`$arrayKeys` tracking in PATCH flow**: `ZgwService::handleUpdate()` tracks which existing fields were originally arrays before json_encoding them for Twig. After merge, only those fields get decoded back to arrays. This prevents string-typed fields containing JSON from being incorrectly decoded.
+
+### ZIOT omschrijving resolution
+
+VNG tests may send ZIOT `informatieobjecttype` as an omschrijving string that happens to be UUID-shaped. `rulesZaaktypeinformatieobjecttypenCreate` handles this by: (1) if it's a URL, keep as-is; (2) if it's a bare UUID, verify it exists in OR -- if not, fall back to name-based lookup; (3) if not a UUID, resolve by name. This prevents storing non-existent UUIDs when the value is actually an omschrijving that looks like a UUID.
+
+### 2026-03-08 — BRC business rules implementation
+
+**brc-003a fix**: The `validateInformatieobjectUrl()` in `ZgwRulesBase` had a bug where `extractUuid()` returning null caused the validation to silently pass (the `if ($ioUuid !== null && $this->objectService !== null)` condition was skipped). Fixed by adding an explicit null check that returns 400 when UUID extraction fails.
+
+**brc-005a/b fix (OpenRegister)**: `MetadataHydrationHandler::hydrateObjectMetadata()` line 100 assumed `$objectData['object']` was always a nested array, but ObjectInformatieObject has `object` as a URL string. When the OIO data had `['document' => url, 'object' => url, 'objectType' => 'besluit']`, the code used the URL string as the business data array, causing a TypeError. Fixed by adding `is_array()` check.
+
+**brc-006a**: ZGW zaak-besluit relation requires checking BOTH directions: (1) BesluitType.caseTypes contains the zaaktype UUID, and (2) ZaakType.decisionTypes contains the BesluitType omschrijving or UUID. The `decisionTypes` array field was added to the caseType schema and the zaaktype mapping was updated to store `besluittypen` as `decisionTypes`.
+
+**brc-009c/d/e (cascade delete)**: Deleting a besluit must cascade delete BIOs and OIOs. The key challenge was that OpenRegister's `ObjectService::deleteObject()` performs a soft delete via `ObjectEntityMapper::update()`, but the update method checks `shouldUseMagicMapper` which reads the register's `configuration` JSON. If the register has no magic mapping configuration, the update falls through to the blob table path, leaving the magic table row unchanged.
+
+**Fix**: Register 7 (Procest) needed explicit magic mapping configuration (`{"schemas": {"": {"magicMapping": true}}}`) set on its `configuration` column. Without this, `isMagicMappingEnabledForSchema()` returns false and soft deletes in magic tables silently fail.
+
+**Important**: When calling `deleteObject()` during cascade operations (where we're deleting related objects, not the primary resource), pass `_rbac: false, _multitenancy: false` to avoid permission issues with related objects in other schemas.
+
+**Audit trail for deleted resources**: `BrcController::audittrailIndex()` checks if the parent resource exists before returning audit trail data. If the resource was soft-deleted, `find()` throws `DoesNotExistException`, and the controller returns 404. This satisfies brc-009d.
+
+### 2026-03-08 — OpenRegister Magic Mapper soft delete gotcha
+
+**CRITICAL**: OpenRegister's `ObjectService::deleteObject()` performs soft delete by calling `ObjectEntityMapper::update()`. But `update()` checks `shouldUseMagicMapperForRegisterSchema()` which reads `Register::isMagicMappingEnabledForSchema()`. If the register's `configuration` column is NULL or doesn't have the schema listed with `magicMapping: true`, the update falls through to the blob table `parent::update()` call, which operates on `oc_openregister_objects` (the blob table). Since the object only exists in the magic table (`oc_openregister_table_{register}_{schema}`), the soft delete appears to succeed (returns true) but the magic table row is NOT updated.
+
+**Workaround**: Ensure the register has proper `configuration` JSON with all schemas listed. Example:
+```json
+{"schemas": {"decisionDocument": {"magicMapping": true}, "decision": {"magicMapping": true}}}
+```
diff --git a/eslint.config.js b/eslint.config.js
index 7487fcf5..c4e09bd5 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -37,5 +37,6 @@ module.exports = defineConfig([{
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-unresolved': ['error', { ignore: ['^@conduction/nextcloud-vue'] }],
+ 'import/named': 'off',
},
}])
diff --git a/l10n/en.json b/l10n/en.json
index f6aa9c90..0f5225a0 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -1,347 +1,377 @@
{
- "translations": {
- "+{n} today": "+{n} today",
- "0 today": "0 today",
- "1 day": "1 day",
- "1 day overdue": "1 day overdue",
- "1 month": "1 month",
- "1 week": "1 week",
- "1 year": "1 year",
- "A status type with this order already exists": "A status type with this order already exists",
- "Actions": "Actions",
- "Active": "Active",
- "Activity": "Activity",
- "Add": "Add",
- "Add Participant": "Add Participant",
- "Add Status Type": "Add Status Type",
- "Add a note...": "Add a note...",
- "Add note": "Add note",
- "All": "All",
- "All caught up!": "All caught up!",
- "All your items are completed": "All your items are completed",
- "Are you sure you want to delete this case?": "Are you sure you want to delete this case?",
- "Are you sure you want to delete this task?": "Are you sure you want to delete this task?",
- "Assign Handler": "Assign Handler",
- "Assign handler...": "Assign handler...",
- "Assignee": "Assignee",
- "At least one status type must be defined": "At least one status type must be defined",
- "At least one status type must be marked as final": "At least one status type must be marked as final",
- "Available": "Available",
- "Back to list": "Back to list",
- "CASE": "CASE",
- "Calculated deadline": "Calculated deadline",
- "Cancel": "Cancel",
- "Cannot delete: active cases are using this type": "Cannot delete: active cases are using this type",
- "Cannot publish:": "Cannot publish:",
- "Case": "Case",
- "Case Information": "Case Information",
- "Case Type": "Case Type",
- "Case Type Management": "Case Type Management",
- "Case Types": "Case Types",
- "Case created with type \\": "Case created with type \\",
- "Case schema": "Case schema",
- "Case sensitive": "Case sensitive",
- "Case type": "Case type",
- "Case type \\": "Case type \\",
- "Case type has expired (valid until {date})": "Case type has expired (valid until {date})",
- "Case type is not yet valid (valid from {date})": "Case type is not yet valid (valid from {date})",
- "Case type is required": "Case type is required",
- "Case type schema": "Case type schema",
- "Case: {id}": "Case: {id}",
- "Cases": "Cases",
- "Cases and tasks assigned to you will appear here": "Cases and tasks assigned to you will appear here",
- "Cases by Status": "Cases by Status",
- "Cases overview": "Cases overview",
- "Change status": "Change status",
- "Change status...": "Change status...",
- "Closed on {date}": "Closed on {date}",
- "Comma-separated keywords": "Comma-separated keywords",
- "Complete": "Complete",
- "Completed": "Completed",
- "Completed This Month": "Completed This Month",
- "Completed on {date}": "Completed on {date}",
- "Confidential": "Confidential",
- "Confidentiality": "Confidentiality",
- "Configuration": "Configuration",
- "Configuration saved": "Configuration saved",
- "Configure case types": "Configure case types",
- "Configure property mappings between English OpenRegister fields and Dutch ZGW API fields": "Configure property mappings between English OpenRegister fields and Dutch ZGW API fields",
- "Confirm": "Confirm",
- "Create case": "Create case",
- "Create task": "Create task",
- "Dashboard": "Dashboard",
- "Days elapsed": "Days elapsed",
- "Deadline": "Deadline",
- "Deadline & Timing": "Deadline & Timing",
- "Deadline extended from {old} to {new}. Reason: {reason}": "Deadline extended from {old} to {new}. Reason: {reason}",
- "Deadline: {date}": "Deadline: {date}",
- "Decision schema": "Decision schema",
- "Delete": "Delete",
- "Delete case type \"{title}\"?": "Delete case type \"{title}\"?",
- "Delete status type \"{name}\"?": "Delete status type \"{name}\"?",
- "Description": "Description",
- "Disable": "Disable",
- "Disabled": "Disabled",
- "Documentation": "Documentation",
- "Draft": "Draft",
- "Drag to reorder": "Drag to reorder",
- "Due date": "Due date",
- "Due this week": "Due this week",
- "Due today": "Due today",
- "Due tomorrow": "Due tomorrow",
- "Edit": "Edit",
- "Edit ZGW Mapping: {key}": "Edit ZGW Mapping: {key}",
- "Enable this mapping": "Enable this mapping",
- "Enabled": "Enabled",
- "Enter case title...": "Enter case title...",
- "Enter task title...": "Enter task title...",
- "Extend Deadline": "Extend Deadline",
- "Extend deadline": "Extend deadline",
- "Extension allowed": "Extension allowed",
- "Extension period": "Extension period",
- "Extension period is required when extension is allowed": "Extension period is required when extension is allowed",
- "Extension: allowed (+{period})": "Extension: allowed (+{period})",
- "Extension: already extended": "Extension: already extended",
- "Extension: not allowed": "Extension: not allowed",
- "External": "External",
- "Failed to add participant": "Failed to add participant",
- "Failed to add status type": "Failed to add status type",
- "Failed to delete case type": "Failed to delete case type",
- "Failed to delete status type": "Failed to delete status type",
- "Failed to delete status type \"{name}\"": "Failed to delete status type \"{name}\"",
- "Failed to load dashboard data": "Failed to load dashboard data",
- "Failed to save": "Failed to save",
- "Failed to save case type": "Failed to save case type",
- "Final": "Final",
- "Final status": "Final status",
- "General": "General",
- "Handler": "Handler",
- "Handler action": "Handler action",
- "High": "High",
- "Highly confidential": "Highly confidential",
- "Identifier": "Identifier",
- "Initial status": "Initial status",
- "Initiator action": "Initiator action",
- "Install OpenRegister": "Install OpenRegister",
- "Internal": "Internal",
- "Invalid JSON in one of the mapping fields: {error}": "Invalid JSON in one of the mapping fields: {error}",
- "Keywords": "Keywords",
- "Link to a case (optional)": "Link to a case (optional)",
- "Linked Case": "Linked Case",
- "Low": "Low",
- "Manage case types and their configurations": "Manage case types and their configurations",
- "Manage cases and workflows": "Manage cases and workflows",
- "Mapping saved successfully": "Mapping saved successfully",
- "Missing required fields: {fields}": "Missing required fields: {fields}",
- "Must be a valid ISO 8601 duration (e.g., P28D)": "Must be a valid ISO 8601 duration (e.g., P28D)",
- "Must be a valid ISO 8601 duration (e.g., P42D)": "Must be a valid ISO 8601 duration (e.g., P42D)",
- "Must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks, P2M for 2 months)": "Must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks, P2M for 2 months)",
- "Must be a valid ISO 8601 duration (e.g., P56D)": "Must be a valid ISO 8601 duration (e.g., P56D)",
- "My Tasks": "My Tasks",
- "My Work": "My Work",
- "Name": "Name",
- "Name *": "Name *",
- "New Case": "New Case",
- "New Case Type": "New Case Type",
- "New Task": "New Task",
- "New task": "New task",
- "No Procest register configured": "No Procest register configured",
- "No activity yet": "No activity yet",
- "No cases found": "No cases found",
- "No deadline": "No deadline",
- "No items assigned to you": "No items assigned to you",
- "No mapping configured for %s": "No mapping configured for %s",
- "No open cases": "No open cases",
- "No overdue cases": "No overdue cases",
- "No participants assigned": "No participants assigned",
- "No reason provided": "No reason provided",
- "No recent activity": "No recent activity",
- "No result recorded yet": "No result recorded yet",
- "No settings available yet": "No settings available yet",
- "No status types defined. Add at least one to publish this case type.": "No status types defined. Add at least one to publish this case type.",
- "No tasks found": "No tasks found",
- "No tasks yet": "No tasks yet",
- "No widgets configured": "No widgets configured",
- "Normal": "Normal",
- "Not configured": "Not configured",
- "Not set": "Not set",
- "Notification text": "Notification text",
- "Notify": "Notify",
- "Notify initiator": "Notify initiator",
- "Only published case types can be set as default": "Only published case types can be set as default",
- "Open Cases": "Open Cases",
- "OpenRegister is required": "OpenRegister is required",
- "Optional description...": "Optional description...",
- "Order": "Order",
- "Order *": "Order *",
- "Order is required": "Order is required",
- "Origin": "Origin",
- "Overdue": "Overdue",
- "Overdue Cases": "Overdue Cases",
- "Participant": "Participant",
- "Participants": "Participants",
- "Please fix the validation errors": "Please fix the validation errors",
- "Please select a result type": "Please select a result type",
- "Priority": "Priority",
- "Processing deadline": "Processing deadline",
- "Processing time": "Processing time",
- "Procest": "Procest",
- "Procest needs the OpenRegister app to store and manage data. Please install OpenRegister from the app store to get started.": "Procest needs the OpenRegister app to store and manage data. Please install OpenRegister from the app store to get started.",
- "Procest settings": "Procest settings",
- "Property Mapping (outbound: English → Dutch)": "Property Mapping (outbound: English → Dutch)",
- "Public": "Public",
- "Publication required": "Publication required",
- "Publication text": "Publication text",
- "Publish": "Publish",
- "Published": "Published",
- "Purpose": "Purpose",
- "Query Parameter Mapping": "Query Parameter Mapping",
- "Reason": "Reason",
- "Reassign": "Reassign",
- "Reassign handler to:": "Reassign handler to:",
- "Recent Activity": "Recent Activity",
- "Reference process": "Reference process",
- "Refresh dashboard": "Refresh dashboard",
- "Register": "Register",
- "Register ID": "Register ID",
- "Register and schema settings": "Register and schema settings",
- "Remove this participant?": "Remove this participant?",
- "Request Extension": "Request Extension",
- "Reset": "Reset",
- "Responsible unit": "Responsible unit",
- "Restricted": "Restricted",
- "Result": "Result",
- "Result (required)": "Result (required)",
- "Result is required when closing a case": "Result is required when closing a case",
- "Result schema": "Result schema",
- "Retry": "Retry",
- "Reverse Mapping (inbound: Dutch → English)": "Reverse Mapping (inbound: Dutch → English)",
- "Role schema": "Role schema",
- "Role type": "Role type",
- "Save": "Save",
- "Save the case type first before adding status types.": "Save the case type first before adding status types.",
- "Saved successfully": "Saved successfully",
- "Schema ID": "Schema ID",
- "Secret": "Secret",
- "Select a case type...": "Select a case type...",
- "Select due date": "Select due date",
- "Select priority": "Select priority",
- "Select result type...": "Select result type...",
- "Select role type...": "Select role type...",
- "Select user...": "Select user...",
- "Service target": "Service target",
- "Set as default": "Set as default",
- "Show completed": "Show completed",
- "Source Register": "Source Register",
- "Source Schema": "Source Schema",
- "Start": "Start",
- "Start date": "Start date",
- "Started": "Started",
- "Status": "Status",
- "Status Timeline": "Status Timeline",
- "Status changed from \\": "Status changed from \\",
- "Status changed to \\": "Status changed to \\",
- "Status schema": "Status schema",
- "Status type name is required": "Status type name is required",
- "Status type schema": "Status type schema",
- "Statuses": "Statuses",
- "Subject": "Subject",
- "TASK": "TASK",
- "Task": "Task",
- "Task Information": "Task Information",
- "Task schema": "Task schema",
- "Tasks": "Tasks",
- "Terminate": "Terminate",
- "Terminated": "Terminated",
- "This case has {count} linked tasks. Are you sure you want to delete it?": "This case has {count} linked tasks. Are you sure you want to delete it?",
- "This will delete the case type and all {count} status types. Continue?": "This will delete the case type and all {count} status types. Continue?",
- "This will extend the deadline by {period}.": "This will extend the deadline by {period}.",
- "Title": "Title",
- "Title is required": "Title is required",
- "Top secret": "Top secret",
- "Track and manage tasks": "Track and manage tasks",
- "Trigger": "Trigger",
- "Type: {type}": "Type: {type}",
- "Unknown": "Unknown",
- "Unnamed case": "Unnamed case",
- "Unnamed task": "Unnamed task",
- "Unpublish": "Unpublish",
- "Unpublishing this case type will prevent new cases from being created. Existing cases will continue to function. Continue?": "Unpublishing this case type will prevent new cases from being created. Existing cases will continue to function. Continue?",
- "Upcoming": "Upcoming",
- "Updated: {fields}": "Updated: {fields}",
- "Urgent": "Urgent",
- "User settings will appear here in a future update.": "User settings will appear here in a future update.",
- "Username": "Username",
- "Username (optional)": "Username (optional)",
- "Valid from": "Valid from",
- "Valid until": "Valid until",
- "Value Mappings (enum translations)": "Value Mappings (enum translations)",
- "View all activity": "View all activity",
- "View all my work": "View all my work",
- "View all overdue": "View all overdue",
- "View case": "View case",
- "View task": "View task",
- "Welcome to Procest! Get started by creating your first case or task using the buttons above.": "Welcome to Procest! Get started by creating your first case or task using the buttons above.",
- "Welcome to Procest! Get started by creating your first case type in Settings.": "Welcome to Procest! Get started by creating your first case type in Settings.",
- "Why is an extension needed?": "Why is an extension needed?",
- "Widget not available": "Widget not available",
- "ZGW API Mapping": "ZGW API Mapping",
- "ZGW Resource": "ZGW Resource",
- "action needed": "action needed",
- "all on track": "all on track",
- "avg {days} days": "avg {days} days",
- "by {user}": "by {user}",
- "completed": "completed",
- "e.g., P28D (28 days)": "e.g., P28D (28 days)",
- "e.g., P42D (42 days)": "e.g., P42D (42 days)",
- "e.g., P56D (56 days)": "e.g., P56D (56 days)",
- "just now": "just now",
- "no data": "no data",
- "none due today": "none due today",
- "open": "open",
- "overdue": "overdue",
- "tasks": "tasks",
- "yesterday": "yesterday",
- "{days} days": "{days} days",
- "{days} days ago": "{days} days ago",
- "{days} days overdue": "{days} days overdue",
- "{days} days remaining": "{days} days remaining",
- "{field} is required": "{field} is required",
- "{from} \\u2014 (no end)": "{from} \\u2014 (no end)",
- "{hours} hours ago": "{hours} hours ago",
- "{min} min ago": "{min} min ago",
- "{n} days": "{n} days",
- "{n} due today": "{n} due today",
- "{n} months": "{n} months",
- "{n} weeks": "{n} weeks",
- "{n} years": "{n} years",
- "File not found.": "File not found.",
- "Document is already locked.": "Document is already locked.",
- "Document is not locked.": "Document is not locked.",
- "Not found.": "Not found.",
- "This document has no pending chunked upload.": "This document has no pending chunked upload.",
- "Invalid chunk configuration.": "Invalid chunk configuration.",
- "Invalid sequence number. Expected 1-%s.": "Invalid sequence number. Expected 1-%s.",
- "No file content received.": "No file content received.",
- "You do not have the correct permissions for this action.": "You do not have the correct permissions for this action.",
- "The document cannot be deleted: there are related ObjectInformatieObjecten.": "The document cannot be deleted: there are related ObjectInformatieObjecten.",
- "The document cannot be deleted.": "The document cannot be deleted.",
- "Only locked documents may be edited.": "Only locked documents may be edited.",
- "The document is not locked. Lock the document first.": "The document is not locked. Lock the document first.",
- "Lock ID is required for editing a locked document.": "Lock ID is required for editing a locked document.",
- "Lock ID is missing from the request.": "Lock ID is missing from the request.",
- "Lock ID does not match.": "Lock ID does not match.",
- "Lock ID does not match the stored lock.": "Lock ID does not match the stored lock.",
- "When heeftAlleAutorisaties is true, autorisaties must not be specified. When heeftAlleAutorisaties is false, autorisaties must be specified.": "When heeftAlleAutorisaties is true, autorisaties must not be specified. When heeftAlleAutorisaties is false, autorisaties must be specified.",
- "When heeftAlleAutorisaties is false, autorisaties must be specified.": "When heeftAlleAutorisaties is false, autorisaties must be specified.",
- "zaaktype is required when a scope related to zaken is specified.": "zaaktype is required when a scope related to zaken is specified.",
- "maxVertrouwelijkheidaanduiding is required when a scope related to zaken is specified.": "maxVertrouwelijkheidaanduiding is required when a scope related to zaken is specified.",
- "informatieobjecttype is required when a scope related to documenten is specified.": "informatieobjecttype is required when a scope related to documenten is specified.",
- "maxVertrouwelijkheidaanduiding is required when a scope related to documenten is specified.": "maxVertrouwelijkheidaanduiding is required when a scope related to documenten is specified.",
- "besluittype is required when a scope related to besluiten is specified.": "besluittype is required when a scope related to besluiten is specified.",
- "Forced unlocking is not allowed without the correct scope.": "Forced unlocking is not allowed without the correct scope.",
- "Lock ID does not match and forced unlocking is not allowed.": "Lock ID does not match and forced unlocking is not allowed.",
- "productenOfDiensten contains a value not present in the zaaktype.": "productenOfDiensten contains a value not present in the zaaktype.",
- "Product '%s' is not allowed for this zaaktype.": "Product '%s' is not allowed for this zaaktype."
- }
+ "translations": {
+ "'Valid from' date must be set": "'Valid from' date must be set",
+ "'Valid until' must be after 'Valid from'": "'Valid until' must be after 'Valid from'",
+ "+{n} today": "+{n} today",
+ "0 today": "0 today",
+ "1 day": "1 day",
+ "1 day overdue": "1 day overdue",
+ "1 month": "1 month",
+ "1 week": "1 week",
+ "1 year": "1 year",
+ "A status type with this order already exists": "A status type with this order already exists",
+ "Actions": "Actions",
+ "Active": "Active",
+ "Activity": "Activity",
+ "Add": "Add",
+ "Add Participant": "Add Participant",
+ "Add Status Type": "Add Status Type",
+ "Add a note...": "Add a note...",
+ "Add note": "Add note",
+ "All": "All",
+ "All caught up!": "All caught up!",
+ "All your items are completed": "All your items are completed",
+ "Are you sure you want to delete this case?": "Are you sure you want to delete this case?",
+ "Are you sure you want to delete this task?": "Are you sure you want to delete this task?",
+ "Are you sure you want to delete this?": "Are you sure you want to delete this?",
+ "Assign Handler": "Assign Handler",
+ "Assign handler...": "Assign handler...",
+ "Assignee": "Assignee",
+ "At least one status type must be defined": "At least one status type must be defined",
+ "At least one status type must be marked as final": "At least one status type must be marked as final",
+ "Available": "Available",
+ "Back to list": "Back to list",
+ "CASE": "CASE",
+ "Calculated deadline": "Calculated deadline",
+ "Cancel": "Cancel",
+ "Cannot delete: active cases are using this type": "Cannot delete: active cases are using this type",
+ "Cannot publish:": "Cannot publish:",
+ "Case": "Case",
+ "Case Information": "Case Information",
+ "Case Type": "Case Type",
+ "Case Type Management": "Case Type Management",
+ "Case Types": "Case Types",
+ "Case created successfully": "Case created successfully",
+ "Case created with type '{type}'": "Case created with type '{type}'",
+ "Case created with type \\": "Case created with type \\",
+ "Case deleted successfully": "Case deleted successfully",
+ "Case schema": "Case schema",
+ "Case sensitive": "Case sensitive",
+ "Case type": "Case type",
+ "Case type '{name}' is a draft and cannot be used to create cases": "Case type '{name}' is a draft and cannot be used to create cases",
+ "Case type \\": "Case type \\",
+ "Case type has expired (valid until {date})": "Case type has expired (valid until {date})",
+ "Case type is not yet valid (valid from {date})": "Case type is not yet valid (valid from {date})",
+ "Case type is required": "Case type is required",
+ "Case type schema": "Case type schema",
+ "Case updated successfully": "Case updated successfully",
+ "Case: {id}": "Case: {id}",
+ "Cases": "Cases",
+ "Cases and tasks assigned to you will appear here": "Cases and tasks assigned to you will appear here",
+ "Cases by Status": "Cases by Status",
+ "Cases overview": "Cases overview",
+ "Change status": "Change status",
+ "Change status...": "Change status...",
+ "Closed": "Closed",
+ "Closed on {date}": "Closed on {date}",
+ "Comma-separated keywords": "Comma-separated keywords",
+ "Complete": "Complete",
+ "Completed": "Completed",
+ "Completed This Month": "Completed This Month",
+ "Completed on {date}": "Completed on {date}",
+ "Confidential": "Confidential",
+ "Confidentiality": "Confidentiality",
+ "Configuration": "Configuration",
+ "Configuration saved": "Configuration saved",
+ "Configure case types": "Configure case types",
+ "Configure property mappings between English OpenRegister fields and Dutch ZGW API fields": "Configure property mappings between English OpenRegister fields and Dutch ZGW API fields",
+ "Confirm": "Confirm",
+ "Create case": "Create case",
+ "Create task": "Create task",
+ "Created": "Created",
+ "Dashboard": "Dashboard",
+ "Days elapsed": "Days elapsed",
+ "Deadline": "Deadline",
+ "Deadline & Timing": "Deadline & Timing",
+ "Deadline extended from {old} to {new}. Reason: {reason}": "Deadline extended from {old} to {new}. Reason: {reason}",
+ "Deadline: {date}": "Deadline: {date}",
+ "Decision schema": "Decision schema",
+ "Delete": "Delete",
+ "Delete case type \"{title}\"?": "Delete case type \"{title}\"?",
+ "Delete status type \"{name}\"?": "Delete status type \"{name}\"?",
+ "Description": "Description",
+ "Disable": "Disable",
+ "Disabled": "Disabled",
+ "Document is already locked.": "Document is already locked.",
+ "Document is not locked.": "Document is not locked.",
+ "Documentation": "Documentation",
+ "Draft": "Draft",
+ "Drag to reorder": "Drag to reorder",
+ "Due date": "Due date",
+ "Due this week": "Due this week",
+ "Due today": "Due today",
+ "Due tomorrow": "Due tomorrow",
+ "Edit": "Edit",
+ "Edit ZGW Mapping: {key}": "Edit ZGW Mapping: {key}",
+ "Enable this mapping": "Enable this mapping",
+ "Enabled": "Enabled",
+ "Enter case title...": "Enter case title...",
+ "Enter task title...": "Enter task title...",
+ "Extend Deadline": "Extend Deadline",
+ "Extend deadline": "Extend deadline",
+ "Extension allowed": "Extension allowed",
+ "Extension period": "Extension period",
+ "Extension period is required when extension is allowed": "Extension period is required when extension is allowed",
+ "Extension: allowed (+{period})": "Extension: allowed (+{period})",
+ "Extension: already extended": "Extension: already extended",
+ "Extension: not allowed": "Extension: not allowed",
+ "External": "External",
+ "Failed to add participant": "Failed to add participant",
+ "Failed to add status type": "Failed to add status type",
+ "Failed to delete case type": "Failed to delete case type",
+ "Failed to delete status type": "Failed to delete status type",
+ "Failed to delete status type \"{name}\"": "Failed to delete status type \"{name}\"",
+ "Failed to load dashboard data": "Failed to load dashboard data",
+ "Failed to save": "Failed to save",
+ "Failed to save case type": "Failed to save case type",
+ "File not found.": "File not found.",
+ "Final": "Final",
+ "Final status": "Final status",
+ "Forced unlocking is not allowed without the correct scope.": "Forced unlocking is not allowed without the correct scope.",
+ "General": "General",
+ "Handler": "Handler",
+ "Handler action": "Handler action",
+ "High": "High",
+ "Highly confidential": "Highly confidential",
+ "Identifier": "Identifier",
+ "Initial status": "Initial status",
+ "Initiator action": "Initiator action",
+ "Install OpenRegister": "Install OpenRegister",
+ "Internal": "Internal",
+ "Invalid JSON in one of the mapping fields: {error}": "Invalid JSON in one of the mapping fields: {error}",
+ "Invalid chunk configuration.": "Invalid chunk configuration.",
+ "Invalid sequence number. Expected 1-%s.": "Invalid sequence number. Expected 1-%s.",
+ "Keywords": "Keywords",
+ "Link to a case (optional)": "Link to a case (optional)",
+ "Linked Case": "Linked Case",
+ "Loading...": "Loading...",
+ "Lock ID does not match and forced unlocking is not allowed.": "Lock ID does not match and forced unlocking is not allowed.",
+ "Lock ID does not match the stored lock.": "Lock ID does not match the stored lock.",
+ "Lock ID does not match.": "Lock ID does not match.",
+ "Lock ID is missing from the request.": "Lock ID is missing from the request.",
+ "Lock ID is required for editing a locked document.": "Lock ID is required for editing a locked document.",
+ "Low": "Low",
+ "Manage case types and their configurations": "Manage case types and their configurations",
+ "Manage cases and workflows": "Manage cases and workflows",
+ "Manage your cases and tasks": "Manage your cases and tasks",
+ "Mapping saved successfully": "Mapping saved successfully",
+ "Missing required fields: {fields}": "Missing required fields: {fields}",
+ "Must be a valid ISO 8601 duration (e.g., P28D)": "Must be a valid ISO 8601 duration (e.g., P28D)",
+ "Must be a valid ISO 8601 duration (e.g., P42D)": "Must be a valid ISO 8601 duration (e.g., P42D)",
+ "Must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks, P2M for 2 months)": "Must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks, P2M for 2 months)",
+ "Must be a valid ISO 8601 duration (e.g., P56D)": "Must be a valid ISO 8601 duration (e.g., P56D)",
+ "My Tasks": "My Tasks",
+ "My Work": "My Work",
+ "Name": "Name",
+ "Name *": "Name *",
+ "New Case": "New Case",
+ "New Case Type": "New Case Type",
+ "New Task": "New Task",
+ "New case": "New case",
+ "New task": "New task",
+ "Next": "Next",
+ "No Procest register configured": "No Procest register configured",
+ "No activity yet": "No activity yet",
+ "No cases found": "No cases found",
+ "No deadline": "No deadline",
+ "No file content received.": "No file content received.",
+ "No items assigned to you": "No items assigned to you",
+ "No mapping configured for %s": "No mapping configured for %s",
+ "No open cases": "No open cases",
+ "No overdue cases": "No overdue cases",
+ "No participants assigned": "No participants assigned",
+ "No reason provided": "No reason provided",
+ "No recent activity": "No recent activity",
+ "No result recorded yet": "No result recorded yet",
+ "No settings available yet": "No settings available yet",
+ "No status types defined. Add at least one to publish this case type.": "No status types defined. Add at least one to publish this case type.",
+ "No tasks found": "No tasks found",
+ "No tasks yet": "No tasks yet",
+ "No widgets configured": "No widgets configured",
+ "Normal": "Normal",
+ "Not configured": "Not configured",
+ "Not found.": "Not found.",
+ "Not set": "Not set",
+ "Notification text": "Notification text",
+ "Notify": "Notify",
+ "Notify initiator": "Notify initiator",
+ "Only locked documents may be edited.": "Only locked documents may be edited.",
+ "Only published case types can be set as default": "Only published case types can be set as default",
+ "Open Cases": "Open Cases",
+ "OpenRegister is required": "OpenRegister is required",
+ "Optional description...": "Optional description...",
+ "Order": "Order",
+ "Order *": "Order *",
+ "Order is required": "Order is required",
+ "Origin": "Origin",
+ "Overdue": "Overdue",
+ "Overdue Cases": "Overdue Cases",
+ "Participant": "Participant",
+ "Participants": "Participants",
+ "Please fix the validation errors": "Please fix the validation errors",
+ "Please select a result type": "Please select a result type",
+ "Previous": "Previous",
+ "Priority": "Priority",
+ "Processing deadline": "Processing deadline",
+ "Processing time": "Processing time",
+ "Procest": "Procest",
+ "Procest needs the OpenRegister app to store and manage data. Please install OpenRegister from the app store to get started.": "Procest needs the OpenRegister app to store and manage data. Please install OpenRegister from the app store to get started.",
+ "Procest settings": "Procest settings",
+ "Product '%s' is not allowed for this zaaktype.": "Product '%s' is not allowed for this zaaktype.",
+ "Property Mapping (outbound: English → Dutch)": "Property Mapping (outbound: English → Dutch)",
+ "Public": "Public",
+ "Publication required": "Publication required",
+ "Publication text": "Publication text",
+ "Publish": "Publish",
+ "Published": "Published",
+ "Purpose": "Purpose",
+ "Query Parameter Mapping": "Query Parameter Mapping",
+ "Reason": "Reason",
+ "Reassign": "Reassign",
+ "Reassign handler to:": "Reassign handler to:",
+ "Recent Activity": "Recent Activity",
+ "Reference process": "Reference process",
+ "Refresh dashboard": "Refresh dashboard",
+ "Register": "Register",
+ "Register ID": "Register ID",
+ "Register and schema settings": "Register and schema settings",
+ "Remove this participant?": "Remove this participant?",
+ "Request Extension": "Request Extension",
+ "Reset": "Reset",
+ "Responsible unit": "Responsible unit",
+ "Restricted": "Restricted",
+ "Result": "Result",
+ "Result (required)": "Result (required)",
+ "Result is required when closing a case": "Result is required when closing a case",
+ "Result schema": "Result schema",
+ "Retry": "Retry",
+ "Reverse Mapping (inbound: Dutch → English)": "Reverse Mapping (inbound: Dutch → English)",
+ "Role schema": "Role schema",
+ "Role type": "Role type",
+ "Save": "Save",
+ "Save the case type first before adding status types.": "Save the case type first before adding status types.",
+ "Saved successfully": "Saved successfully",
+ "Schema ID": "Schema ID",
+ "Search": "Search",
+ "Secret": "Secret",
+ "Select a case type...": "Select a case type...",
+ "Select due date": "Select due date",
+ "Select priority": "Select priority",
+ "Select result type...": "Select result type...",
+ "Select role type...": "Select role type...",
+ "Select user...": "Select user...",
+ "Service target": "Service target",
+ "Set as default": "Set as default",
+ "Settings": "Settings",
+ "Show completed": "Show completed",
+ "Source Register": "Source Register",
+ "Source Schema": "Source Schema",
+ "Start": "Start",
+ "Start date": "Start date",
+ "Started": "Started",
+ "Status": "Status",
+ "Status Timeline": "Status Timeline",
+ "Status changed from '{from}' to '{to}'": "Status changed from '{from}' to '{to}'",
+ "Status changed from \\": "Status changed from \\",
+ "Status changed to '{status}'": "Status changed to '{status}'",
+ "Status changed to \\": "Status changed to \\",
+ "Status schema": "Status schema",
+ "Status type name is required": "Status type name is required",
+ "Status type schema": "Status type schema",
+ "Statuses": "Statuses",
+ "Subject": "Subject",
+ "TASK": "TASK",
+ "Task": "Task",
+ "Task Information": "Task Information",
+ "Task created successfully": "Task created successfully",
+ "Task schema": "Task schema",
+ "Task updated successfully": "Task updated successfully",
+ "Tasks": "Tasks",
+ "Terminate": "Terminate",
+ "Terminated": "Terminated",
+ "The document cannot be deleted.": "The document cannot be deleted.",
+ "The document cannot be deleted: there are related ObjectInformatieObjecten.": "The document cannot be deleted: there are related ObjectInformatieObjecten.",
+ "The document is not locked. Lock the document first.": "The document is not locked. Lock the document first.",
+ "This case has {count} linked tasks. Are you sure you want to delete it?": "This case has {count} linked tasks. Are you sure you want to delete it?",
+ "This document has no pending chunked upload.": "This document has no pending chunked upload.",
+ "This will delete the case type and all {count} status types. Continue?": "This will delete the case type and all {count} status types. Continue?",
+ "This will extend the deadline by {period}.": "This will extend the deadline by {period}.",
+ "Title": "Title",
+ "Title is required": "Title is required",
+ "Top secret": "Top secret",
+ "Track and manage tasks": "Track and manage tasks",
+ "Trigger": "Trigger",
+ "Type: {type}": "Type: {type}",
+ "Unknown": "Unknown",
+ "Unnamed case": "Unnamed case",
+ "Unnamed task": "Unnamed task",
+ "Unpublish": "Unpublish",
+ "Unpublishing this case type will prevent new cases from being created. Existing cases will continue to function. Continue?": "Unpublishing this case type will prevent new cases from being created. Existing cases will continue to function. Continue?",
+ "Upcoming": "Upcoming",
+ "Updated": "Updated",
+ "Updated: {fields}": "Updated: {fields}",
+ "Urgent": "Urgent",
+ "User settings will appear here in a future update.": "User settings will appear here in a future update.",
+ "Username": "Username",
+ "Username (optional)": "Username (optional)",
+ "Valid from": "Valid from",
+ "Valid until": "Valid until",
+ "Value Mappings (enum translations)": "Value Mappings (enum translations)",
+ "View all activity": "View all activity",
+ "View all my work": "View all my work",
+ "View all overdue": "View all overdue",
+ "View case": "View case",
+ "View task": "View task",
+ "Welcome to Procest": "Welcome to Procest",
+ "Welcome to Procest! Get started by creating your first case or task using the buttons above.": "Welcome to Procest! Get started by creating your first case or task using the buttons above.",
+ "Welcome to Procest! Get started by creating your first case type in Settings.": "Welcome to Procest! Get started by creating your first case type in Settings.",
+ "When heeftAlleAutorisaties is false, autorisaties must be specified.": "When heeftAlleAutorisaties is false, autorisaties must be specified.",
+ "When heeftAlleAutorisaties is true, autorisaties must not be specified. When heeftAlleAutorisaties is false, autorisaties must be specified.": "When heeftAlleAutorisaties is true, autorisaties must not be specified. When heeftAlleAutorisaties is false, autorisaties must be specified.",
+ "Why is an extension needed?": "Why is an extension needed?",
+ "Widget not available": "Widget not available",
+ "You do not have the correct permissions for this action.": "You do not have the correct permissions for this action.",
+ "ZGW API Mapping": "ZGW API Mapping",
+ "ZGW Resource": "ZGW Resource",
+ "action needed": "action needed",
+ "all on track": "all on track",
+ "avg {days} days": "avg {days} days",
+ "besluittype is required when a scope related to besluiten is specified.": "besluittype is required when a scope related to besluiten is specified.",
+ "by {user}": "by {user}",
+ "closed": "closed",
+ "completed": "completed",
+ "e.g., P28D (28 days)": "e.g., P28D (28 days)",
+ "e.g., P42D (42 days)": "e.g., P42D (42 days)",
+ "e.g., P56D (56 days)": "e.g., P56D (56 days)",
+ "high": "high",
+ "in_progress": "in progress",
+ "informatieobjecttype is required when a scope related to documenten is specified.": "informatieobjecttype is required when a scope related to documenten is specified.",
+ "just now": "just now",
+ "low": "low",
+ "maxVertrouwelijkheidaanduiding is required when a scope related to documenten is specified.": "maxVertrouwelijkheidaanduiding is required when a scope related to documenten is specified.",
+ "maxVertrouwelijkheidaanduiding is required when a scope related to zaken is specified.": "maxVertrouwelijkheidaanduiding is required when a scope related to zaken is specified.",
+ "no data": "no data",
+ "none due today": "none due today",
+ "normal": "normal",
+ "open": "open",
+ "overdue": "overdue",
+ "productenOfDiensten contains a value not present in the zaaktype.": "productenOfDiensten contains a value not present in the zaaktype.",
+ "tasks": "tasks",
+ "urgent": "urgent",
+ "yesterday": "yesterday",
+ "zaaktype is required when a scope related to zaken is specified.": "zaaktype is required when a scope related to zaken is specified.",
+ "{days} days": "{days} days",
+ "{days} days ago": "{days} days ago",
+ "{days} days overdue": "{days} days overdue",
+ "{days} days remaining": "{days} days remaining",
+ "{field} is required": "{field} is required",
+ "{from} \\u2014 (no end)": "{from} \\u2014 (no end)",
+ "{from} — (no end)": "{from} — (no end)",
+ "{hours} hours ago": "{hours} hours ago",
+ "{min} min ago": "{min} min ago",
+ "{n} days": "{n} days",
+ "{n} due today": "{n} due today",
+ "{n} months": "{n} months",
+ "{n} weeks": "{n} weeks",
+ "{n} years": "{n} years"
+ }
}
diff --git a/l10n/nl.json b/l10n/nl.json
index f35cc8d2..d0361ec9 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -1,347 +1,382 @@
{
- "translations": {
- "+{n} today": "+{n} vandaag",
- "0 today": "0 vandaag",
- "1 day": "1 dag",
- "1 day overdue": "1 dag te laat",
- "1 month": "1 maand",
- "1 week": "1 week",
- "1 year": "1 jaar",
- "A status type with this order already exists": "Er bestaat al een statustype met deze volgorde",
- "Actions": "Acties",
- "Active": "Actief",
- "Activity": "Activiteit",
- "Add": "Toevoegen",
- "Add Participant": "Deelnemer toevoegen",
- "Add Status Type": "Statustype toevoegen",
- "Add a note...": "Notitie toevoegen...",
- "Add note": "Notitie toevoegen",
- "All": "Alle",
- "All caught up!": "Alles bijgewerkt!",
- "All your items are completed": "Al uw items zijn afgerond",
- "Are you sure you want to delete this case?": "Weet u zeker dat u deze zaak wilt verwijderen?",
- "Are you sure you want to delete this task?": "Weet u zeker dat u deze taak wilt verwijderen?",
- "Assign Handler": "Behandelaar toewijzen",
- "Assign handler...": "Behandelaar toewijzen...",
- "Assignee": "Toegewezen aan",
- "At least one status type must be defined": "Er moet ten minste één statustype worden gedefinieerd",
- "At least one status type must be marked as final": "Ten minste één statustype moet als definitief worden gemarkeerd",
- "Available": "Beschikbaar",
- "Back to list": "Terug naar lijst",
- "CASE": "ZAAK",
- "Calculated deadline": "Berekende deadline",
- "Cancel": "Annuleren",
- "Cannot delete: active cases are using this type": "Kan niet verwijderen: actieve zaken gebruiken dit type",
- "Cannot publish:": "Kan niet publiceren:",
- "Case": "Zaak",
- "Case Information": "Zaak informatie",
- "Case Type": "Zaaktype",
- "Case Type Management": "Zaaktype beheer",
- "Case Types": "Zaaktypen",
- "Case created with type \\": "Zaak aangemaakt met type \\",
- "Case schema": "Zaak schema",
- "Case sensitive": "Hoofdlettergevoelig",
- "Case type": "Zaaktype",
- "Case type \\": "Zaaktype \\",
- "Case type has expired (valid until {date})": "Zaaktype is verlopen (geldig tot {date})",
- "Case type is not yet valid (valid from {date})": "Zaaktype is nog niet geldig (geldig vanaf {date})",
- "Case type is required": "Zaaktype is verplicht",
- "Case type schema": "Zaaktype schema",
- "Case: {id}": "Zaak: {id}",
- "Cases": "Zaken",
- "Cases and tasks assigned to you will appear here": "Zaken en taken die aan u zijn toegewezen verschijnen hier",
- "Cases by Status": "Zaken per status",
- "Cases overview": "Zaken overzicht",
- "Change status": "Status wijzigen",
- "Change status...": "Status wijzigen...",
- "Closed on {date}": "Gesloten op {date}",
- "Comma-separated keywords": "Kommagescheiden trefwoorden",
- "Complete": "Voltooien",
- "Completed": "Afgerond",
- "Completed This Month": "Deze maand afgerond",
- "Completed on {date}": "Afgerond op {date}",
- "Confidential": "Vertrouwelijk",
- "Confidentiality": "Vertrouwelijkheid",
- "Configuration": "Configuratie",
- "Configuration saved": "Configuratie opgeslagen",
- "Configure case types": "Zaaktypen configureren",
- "Configure property mappings between English OpenRegister fields and Dutch ZGW API fields": "Configureer eigenschap-mappings tussen Engelse OpenRegister velden en Nederlandse ZGW API velden",
- "Confirm": "Bevestigen",
- "Create case": "Zaak aanmaken",
- "Create task": "Taak aanmaken",
- "Dashboard": "Dashboard",
- "Days elapsed": "Dagen verstreken",
- "Deadline": "Deadline",
- "Deadline & Timing": "Deadline & Timing",
- "Deadline extended from {old} to {new}. Reason: {reason}": "Deadline verlengd van {old} naar {new}. Reden: {reason}",
- "Deadline: {date}": "Deadline: {date}",
- "Decision schema": "Besluit schema",
- "Delete": "Verwijderen",
- "Delete case type \"{title}\"?": "Zaaktype \"{title}\" verwijderen?",
- "Delete status type \"{name}\"?": "Statustype \"{name}\" verwijderen?",
- "Description": "Omschrijving",
- "Disable": "Uitschakelen",
- "Disabled": "Uitgeschakeld",
- "Documentation": "Documentatie",
- "Draft": "Concept",
- "Drag to reorder": "Sleep om te herordenen",
- "Due date": "Deadline",
- "Due this week": "Deze week verlopen",
- "Due today": "Vandaag verlopen",
- "Due tomorrow": "Morgen verlopen",
- "Edit": "Bewerken",
- "Edit ZGW Mapping: {key}": "ZGW Mapping bewerken: {key}",
- "Enable this mapping": "Deze mapping inschakelen",
- "Enabled": "Ingeschakeld",
- "Enter case title...": "Voer zaaktitel in...",
- "Enter task title...": "Voer taaktitel in...",
- "Extend Deadline": "Deadline verlengen",
- "Extend deadline": "Deadline verlengen",
- "Extension allowed": "Verlenging toegestaan",
- "Extension period": "Verlengingstermijn",
- "Extension period is required when extension is allowed": "Verlengingstermijn is vereist wanneer verlenging is toegestaan",
- "Extension: allowed (+{period})": "Verlenging: toegestaan (+{period})",
- "Extension: already extended": "Verlenging: reeds verlengd",
- "Extension: not allowed": "Verlenging: niet toegestaan",
- "External": "Extern",
- "Failed to add participant": "Deelnemer toevoegen mislukt",
- "Failed to add status type": "Statustype toevoegen mislukt",
- "Failed to delete case type": "Zaaktype verwijderen mislukt",
- "Failed to delete status type": "Statustype verwijderen mislukt",
- "Failed to delete status type \"{name}\"": "Statustype \"{name}\" verwijderen mislukt",
- "Failed to load dashboard data": "Dashboard gegevens laden mislukt",
- "Failed to save": "Opslaan mislukt",
- "Failed to save case type": "Zaaktype opslaan mislukt",
- "Final": "Definitief",
- "Final status": "Eindstatus",
- "General": "Algemeen",
- "Handler": "Behandelaar",
- "Handler action": "Behandelaar actie",
- "High": "Hoog",
- "Highly confidential": "Zeer vertrouwelijk",
- "Identifier": "Identificatie",
- "Initial status": "Beginstatus",
- "Initiator action": "Initiator actie",
- "Install OpenRegister": "Installeer OpenRegister",
- "Internal": "Intern",
- "Invalid JSON in one of the mapping fields: {error}": "Ongeldige JSON in een van de mappingvelden: {error}",
- "Keywords": "Trefwoorden",
- "Link to a case (optional)": "Koppel aan een zaak (optioneel)",
- "Linked Case": "Gekoppelde zaak",
- "Low": "Laag",
- "Manage case types and their configurations": "Beheer zaaktypen en hun configuraties",
- "Manage cases and workflows": "Beheer zaken en workflows",
- "Mapping saved successfully": "Mapping succesvol opgeslagen",
- "Missing required fields: {fields}": "Verplichte velden ontbreken: {fields}",
- "Must be a valid ISO 8601 duration (e.g., P28D)": "Moet een geldige ISO 8601 duur zijn (bijv. P28D)",
- "Must be a valid ISO 8601 duration (e.g., P42D)": "Moet een geldige ISO 8601 duur zijn (bijv. P42D)",
- "Must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks, P2M for 2 months)": "Moet een geldige ISO 8601 duur zijn (bijv. P56D voor 56 dagen, P8W voor 8 weken, P2M voor 2 maanden)",
- "Must be a valid ISO 8601 duration (e.g., P56D)": "Moet een geldige ISO 8601 duur zijn (bijv. P56D)",
- "My Tasks": "Mijn taken",
- "My Work": "Mijn werk",
- "Name": "Naam",
- "Name *": "Naam *",
- "New Case": "Nieuwe zaak",
- "New Case Type": "Nieuw zaaktype",
- "New Task": "Nieuwe taak",
- "New task": "Nieuwe taak",
- "No Procest register configured": "Geen Procest register geconfigureerd",
- "No activity yet": "Nog geen activiteit",
- "No cases found": "Geen zaken gevonden",
- "No deadline": "Geen deadline",
- "No items assigned to you": "Geen items aan u toegewezen",
- "No mapping configured for %s": "Geen mapping geconfigureerd voor %s",
- "No open cases": "Geen openstaande zaken",
- "No overdue cases": "Geen openstaande zaken",
- "No participants assigned": "Geen deelnemers toegewezen",
- "No reason provided": "Geen reden opgegeven",
- "No recent activity": "Geen recente activiteit",
- "No result recorded yet": "Nog geen resultaat geregistreerd",
- "No settings available yet": "Nog geen instellingen beschikbaar",
- "No status types defined. Add at least one to publish this case type.": "Geen statustypen gedefinieerd. Voeg er ten minste één toe om dit zaaktype te publiceren.",
- "No tasks found": "Geen taken gevonden",
- "No tasks yet": "Nog geen taken",
- "No widgets configured": "Geen widgets geconfigureerd",
- "Normal": "Normaal",
- "Not configured": "Niet geconfigureerd",
- "Not set": "Niet ingesteld",
- "Notification text": "Notificatietekst",
- "Notify": "Notificeren",
- "Notify initiator": "Initiator notificeren",
- "Only published case types can be set as default": "Alleen gepubliceerde zaaktypen kunnen als standaard worden ingesteld",
- "Open Cases": "Open zaken",
- "OpenRegister is required": "OpenRegister is vereist",
- "Optional description...": "Optionele omschrijving...",
- "Order": "Volgorde",
- "Order *": "Volgorde *",
- "Order is required": "Volgorde is verplicht",
- "Origin": "Oorsprong",
- "Overdue": "Te laat",
- "Overdue Cases": "Openstaande zaken",
- "Participant": "Deelnemer",
- "Participants": "Deelnemers",
- "Please fix the validation errors": "Corrigeer de validatiefouten",
- "Please select a result type": "Selecteer een resultaattype",
- "Priority": "Prioriteit",
- "Processing deadline": "Verwerkingsdeadline",
- "Processing time": "Verwerkingstijd",
- "Procest": "Procest",
- "Procest needs the OpenRegister app to store and manage data. Please install OpenRegister from the app store to get started.": "Procest heeft de OpenRegister app nodig om gegevens op te slaan en te beheren. Installeer OpenRegister uit de app store om te beginnen.",
- "Procest settings": "Procest instellingen",
- "Property Mapping (outbound: English → Dutch)": "Eigenschap mapping (uitgaand: Engels → Nederlands)",
- "Public": "Openbaar",
- "Publication required": "Publicatie vereist",
- "Publication text": "Publicatietekst",
- "Publish": "Publiceren",
- "Published": "Gepubliceerd",
- "Purpose": "Doel",
- "Query Parameter Mapping": "Query parameter mapping",
- "Reason": "Reden",
- "Reassign": "Hertoewijzen",
- "Reassign handler to:": "Behandelaar hertoewijzen aan:",
- "Recent Activity": "Recente activiteit",
- "Reference process": "Referentieproces",
- "Refresh dashboard": "Dashboard vernieuwen",
- "Register": "Register",
- "Register ID": "Register ID",
- "Register and schema settings": "Register en schema instellingen",
- "Remove this participant?": "Deze deelnemer verwijderen?",
- "Request Extension": "Verlenging aanvragen",
- "Reset": "Herstellen",
- "Responsible unit": "Verantwoordelijke eenheid",
- "Restricted": "Beperkt",
- "Result": "Resultaat",
- "Result (required)": "Resultaat (verplicht)",
- "Result is required when closing a case": "Resultaat is verplicht bij het sluiten van een zaak",
- "Result schema": "Resultaat schema",
- "Retry": "Opnieuw proberen",
- "Reverse Mapping (inbound: Dutch → English)": "Reverse mapping (inkomend: Nederlands → Engels)",
- "Role schema": "Rol schema",
- "Role type": "Roltype",
- "Save": "Opslaan",
- "Save the case type first before adding status types.": "Sla het zaaktype eerst op voordat u statustypen toevoegt.",
- "Saved successfully": "Succesvol opgeslagen",
- "Schema ID": "Schema ID",
- "Secret": "Geheim",
- "Select a case type...": "Selecteer een zaaktype...",
- "Select due date": "Selecteer deadline",
- "Select priority": "Selecteer prioriteit",
- "Select result type...": "Selecteer resultaattype...",
- "Select role type...": "Selecteer roltype...",
- "Select user...": "Selecteer gebruiker...",
- "Service target": "Servicenorm",
- "Set as default": "Als standaard instellen",
- "Show completed": "Toon afgerond",
- "Source Register": "Bronregister",
- "Source Schema": "Bronschema",
- "Start": "Start",
- "Start date": "Startdatum",
- "Started": "Gestart",
- "Status": "Status",
- "Status Timeline": "Status tijdlijn",
- "Status changed from \\": "Status gewijzigd van \\",
- "Status changed to \\": "Status gewijzigd naar \\",
- "Status schema": "Status schema",
- "Status type name is required": "Statustype naam is verplicht",
- "Status type schema": "Statustype schema",
- "Statuses": "Statussen",
- "Subject": "Onderwerp",
- "TASK": "TAAK",
- "Task": "Taak",
- "Task Information": "Taak informatie",
- "Task schema": "Taak schema",
- "Tasks": "Taken",
- "Terminate": "Beëindigen",
- "Terminated": "Beëindigd",
- "This case has {count} linked tasks. Are you sure you want to delete it?": "Deze zaak heeft {count} gekoppelde taken. Weet u zeker dat u deze wilt verwijderen?",
- "This will delete the case type and all {count} status types. Continue?": "Dit verwijdert het zaaktype en alle {count} statustypen. Doorgaan?",
- "This will extend the deadline by {period}.": "Dit verlengt de deadline met {period}.",
- "Title": "Titel",
- "Title is required": "Titel is verplicht",
- "Top secret": "Zeer geheim",
- "Track and manage tasks": "Taken bijhouden en beheren",
- "Trigger": "Trigger",
- "Type: {type}": "Type: {type}",
- "Unknown": "Onbekend",
- "Unnamed case": "Naamloze zaak",
- "Unnamed task": "Naamloze taak",
- "Unpublish": "Depubliceren",
- "Unpublishing this case type will prevent new cases from being created. Existing cases will continue to function. Continue?": "Het depubliceren van dit zaaktype voorkomt dat er nieuwe zaken worden aangemaakt. Bestaande zaken blijven functioneren. Doorgaan?",
- "Upcoming": "Aankomend",
- "Updated: {fields}": "Bijgewerkt: {fields}",
- "Urgent": "Urgent",
- "User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update.",
- "Username": "Gebruikersnaam",
- "Username (optional)": "Gebruikersnaam (optioneel)",
- "Valid from": "Geldig vanaf",
- "Valid until": "Geldig tot",
- "Value Mappings (enum translations)": "Waarde mappings (enum vertalingen)",
- "View all activity": "Alle activiteit bekijken",
- "View all my work": "Al mijn werk bekijken",
- "View all overdue": "Alle openstaande bekijken",
- "View case": "Bekijk zaak",
- "View task": "Bekijk taak",
- "Welcome to Procest! Get started by creating your first case or task using the buttons above.": "Welkom bij Procest! Begin door uw eerste zaak of taak aan te maken met de knoppen hierboven.",
- "Welcome to Procest! Get started by creating your first case type in Settings.": "Welkom bij Procest! Begin door uw eerste zaaktype aan te maken in Instellingen.",
- "Why is an extension needed?": "Waarom is een verlenging nodig?",
- "Widget not available": "Widget niet beschikbaar",
- "ZGW API Mapping": "ZGW API Mapping",
- "ZGW Resource": "ZGW Bron",
- "action needed": "actie vereist",
- "all on track": "alles op schema",
- "avg {days} days": "gem. {days} dagen",
- "by {user}": "door {user}",
- "completed": "afgerond",
- "e.g., P28D (28 days)": "bijv. P28D (28 dagen)",
- "e.g., P42D (42 days)": "bijv. P42D (42 dagen)",
- "e.g., P56D (56 days)": "bijv. P56D (56 dagen)",
- "just now": "zojuist",
- "no data": "geen gegevens",
- "none due today": "geen deadlines vandaag",
- "open": "open",
- "overdue": "te laat",
- "tasks": "taken",
- "yesterday": "gisteren",
- "{days} days": "{days} dagen",
- "{days} days ago": "{days} dagen geleden",
- "{days} days overdue": "{days} dagen te laat",
- "{days} days remaining": "{days} dagen resterend",
- "{field} is required": "{field} is verplicht",
- "{from} \\u2014 (no end)": "{from} \\u2014 (no end)",
- "{hours} hours ago": "{hours} uur geleden",
- "{min} min ago": "{min} min geleden",
- "{n} days": "{n} dagen",
- "{n} due today": "{n} vandaag verlopen",
- "{n} months": "{n} maanden",
- "{n} weeks": "{n} weken",
- "{n} years": "{n} jaar",
- "File not found.": "Bestand niet gevonden.",
- "Document is already locked.": "Document is al vergrendeld.",
- "Document is not locked.": "Document is niet vergrendeld.",
- "Not found.": "Niet gevonden.",
- "This document has no pending chunked upload.": "Dit document heeft geen openstaande chunked upload.",
- "Invalid chunk configuration.": "Ongeldige chunk configuratie.",
- "Invalid sequence number. Expected 1-%s.": "Ongeldig volgnummer. Verwacht 1-%s.",
- "No file content received.": "Geen bestandsinhoud ontvangen.",
- "You do not have the correct permissions for this action.": "U heeft niet de juiste rechten voor deze actie.",
- "The document cannot be deleted: there are related ObjectInformatieObjecten.": "Het informatieobject kan niet verwijderd worden: er zijn gerelateerde ObjectInformatieObjecten.",
- "The document cannot be deleted.": "Het informatieobject kan niet verwijderd worden.",
- "Only locked documents may be edited.": "Alleen vergrendelde documenten mogen bewerkt worden.",
- "The document is not locked. Lock the document first.": "Het document is niet vergrendeld. Vergrendel het document eerst.",
- "Lock ID is required for editing a locked document.": "Lock ID is vereist voor het bewerken van een vergrendeld document.",
- "Lock ID is missing from the request.": "Lock ID ontbreekt in het verzoek.",
- "Lock ID does not match.": "Lock ID komt niet overeen.",
- "Lock ID does not match the stored lock.": "Lock ID komt niet overeen met de opgeslagen vergrendeling.",
- "When heeftAlleAutorisaties is true, autorisaties must not be specified. When heeftAlleAutorisaties is false, autorisaties must be specified.": "Wanneer heeftAlleAutorisaties op true staat, mag autorisaties niet opgegeven worden. Indien heeftAlleAutorisaties false is, dan moet autorisaties opgegeven worden.",
- "When heeftAlleAutorisaties is false, autorisaties must be specified.": "Wanneer heeftAlleAutorisaties false is, dan moet autorisaties opgegeven worden.",
- "zaaktype is required when a scope related to zaken is specified.": "zaaktype is verplicht wanneer een scope m.b.t. zaken is opgegeven.",
- "maxVertrouwelijkheidaanduiding is required when a scope related to zaken is specified.": "maxVertrouwelijkheidaanduiding is verplicht wanneer een scope m.b.t. zaken is opgegeven.",
- "informatieobjecttype is required when a scope related to documenten is specified.": "informatieobjecttype is verplicht wanneer een scope m.b.t. documenten is opgegeven.",
- "maxVertrouwelijkheidaanduiding is required when a scope related to documenten is specified.": "maxVertrouwelijkheidaanduiding is verplicht wanneer een scope m.b.t. documenten is opgegeven.",
- "besluittype is required when a scope related to besluiten is specified.": "besluittype is verplicht wanneer een scope m.b.t. besluiten is opgegeven.",
- "Forced unlocking is not allowed without the correct scope.": "Geforceerd unlocken is niet toegestaan zonder juiste scope.",
- "Lock ID does not match and forced unlocking is not allowed.": "Lock ID komt niet overeen en geforceerd unlocken is niet toegestaan.",
- "productenOfDiensten contains a value not present in the zaaktype.": "productenOfDiensten bevat een waarde die niet in het zaaktype voorkomt.",
- "Product '%s' is not allowed for this zaaktype.": "Product '%s' is niet toegestaan voor dit zaaktype."
- }
+ "translations": {
+ "'Valid from' date must be set": "'Geldig vanaf'-datum moet worden ingesteld",
+ "'Valid until' must be after 'Valid from'": "'Geldig tot' moet na 'Geldig vanaf' liggen",
+ "+{n} today": "+{n} vandaag",
+ "0 today": "0 vandaag",
+ "1 day": "1 dag",
+ "1 day overdue": "1 dag te laat",
+ "1 month": "1 maand",
+ "1 week": "1 week",
+ "1 year": "1 jaar",
+ "A status type with this order already exists": "Er bestaat al een statustype met deze volgorde",
+ "Actions": "Acties",
+ "Active": "Actief",
+ "Activity": "Activiteit",
+ "Add": "Toevoegen",
+ "Add Participant": "Deelnemer toevoegen",
+ "Add Status Type": "Statustype toevoegen",
+ "Add a note...": "Notitie toevoegen...",
+ "Add note": "Notitie toevoegen",
+ "All": "Alle",
+ "All caught up!": "Alles bijgewerkt!",
+ "All your items are completed": "Al uw items zijn afgerond",
+ "Are you sure you want to delete this case?": "Weet u zeker dat u deze zaak wilt verwijderen?",
+ "Are you sure you want to delete this task?": "Weet u zeker dat u deze taak wilt verwijderen?",
+ "Are you sure you want to delete this?": "Weet u zeker dat u dit wilt verwijderen?",
+ "Assign Handler": "Behandelaar toewijzen",
+ "Assign handler...": "Behandelaar toewijzen...",
+ "Assignee": "Toegewezen aan",
+ "At least one status type must be defined": "Er moet ten minste één statustype worden gedefinieerd",
+ "At least one status type must be marked as final": "Ten minste één statustype moet als definitief worden gemarkeerd",
+ "Available": "Beschikbaar",
+ "Back to list": "Terug naar lijst",
+ "CASE": "ZAAK",
+ "Calculated deadline": "Berekende deadline",
+ "Cancel": "Annuleren",
+ "Cannot delete: active cases are using this type": "Kan niet verwijderen: actieve zaken gebruiken dit type",
+ "Cannot publish:": "Kan niet publiceren:",
+ "Case": "Zaak",
+ "Case Information": "Zaak informatie",
+ "Case Type": "Zaaktype",
+ "Case Type Management": "Zaaktype beheer",
+ "Case Types": "Zaaktypen",
+ "Case created successfully": "Zaak succesvol aangemaakt",
+ "Case created with type '": "Zaak aangemaakt met type '",
+ "Case created with type '{type}'": "Zaak aangemaakt met type '{type}'",
+ "Case created with type \\": "Zaak aangemaakt met type \\",
+ "Case deleted successfully": "Zaak succesvol verwijderd",
+ "Case schema": "Zaak schema",
+ "Case sensitive": "Hoofdlettergevoelig",
+ "Case type": "Zaaktype",
+ "Case type '": "Zaaktype '",
+ "Case type '{name}' is a draft and cannot be used to create cases": "Zaaktype '{name}' is een concept en kan niet worden gebruikt om zaken aan te maken",
+ "Case type \\": "Zaaktype \\",
+ "Case type has expired (valid until {date})": "Zaaktype is verlopen (geldig tot {date})",
+ "Case type is not yet valid (valid from {date})": "Zaaktype is nog niet geldig (geldig vanaf {date})",
+ "Case type is required": "Zaaktype is verplicht",
+ "Case type schema": "Zaaktype schema",
+ "Case updated successfully": "Zaak succesvol bijgewerkt",
+ "Case: {id}": "Zaak: {id}",
+ "Cases": "Zaken",
+ "Cases and tasks assigned to you will appear here": "Zaken en taken die aan u zijn toegewezen verschijnen hier",
+ "Cases by Status": "Zaken per status",
+ "Cases overview": "Zaken overzicht",
+ "Change status": "Status wijzigen",
+ "Change status...": "Status wijzigen...",
+ "Closed": "Gesloten",
+ "Closed on {date}": "Gesloten op {date}",
+ "Comma-separated keywords": "Kommagescheiden trefwoorden",
+ "Complete": "Voltooien",
+ "Completed": "Afgerond",
+ "Completed This Month": "Deze maand afgerond",
+ "Completed on {date}": "Afgerond op {date}",
+ "Confidential": "Vertrouwelijk",
+ "Confidentiality": "Vertrouwelijkheid",
+ "Configuration": "Configuratie",
+ "Configuration saved": "Configuratie opgeslagen",
+ "Configure case types": "Zaaktypen configureren",
+ "Configure property mappings between English OpenRegister fields and Dutch ZGW API fields": "Configureer eigenschap-mappings tussen Engelse OpenRegister velden en Nederlandse ZGW API velden",
+ "Confirm": "Bevestigen",
+ "Create case": "Zaak aanmaken",
+ "Create task": "Taak aanmaken",
+ "Created": "Aangemaakt",
+ "Dashboard": "Dashboard",
+ "Days elapsed": "Dagen verstreken",
+ "Deadline": "Deadline",
+ "Deadline & Timing": "Deadline & Timing",
+ "Deadline extended from {old} to {new}. Reason: {reason}": "Deadline verlengd van {old} naar {new}. Reden: {reason}",
+ "Deadline: {date}": "Deadline: {date}",
+ "Decision schema": "Besluit schema",
+ "Delete": "Verwijderen",
+ "Delete case type \"{title}\"?": "Zaaktype \"{title}\" verwijderen?",
+ "Delete status type \"{name}\"?": "Statustype \"{name}\" verwijderen?",
+ "Description": "Omschrijving",
+ "Disable": "Uitschakelen",
+ "Disabled": "Uitgeschakeld",
+ "Document is already locked.": "Document is al vergrendeld.",
+ "Document is not locked.": "Document is niet vergrendeld.",
+ "Documentation": "Documentatie",
+ "Draft": "Concept",
+ "Drag to reorder": "Sleep om te herordenen",
+ "Due date": "Deadline",
+ "Due this week": "Deze week verlopen",
+ "Due today": "Vandaag verlopen",
+ "Due tomorrow": "Morgen verlopen",
+ "Edit": "Bewerken",
+ "Edit ZGW Mapping: {key}": "ZGW Mapping bewerken: {key}",
+ "Enable this mapping": "Deze mapping inschakelen",
+ "Enabled": "Ingeschakeld",
+ "Enter case title...": "Voer zaaktitel in...",
+ "Enter task title...": "Voer taaktitel in...",
+ "Extend Deadline": "Deadline verlengen",
+ "Extend deadline": "Deadline verlengen",
+ "Extension allowed": "Verlenging toegestaan",
+ "Extension period": "Verlengingstermijn",
+ "Extension period is required when extension is allowed": "Verlengingstermijn is vereist wanneer verlenging is toegestaan",
+ "Extension: allowed (+{period})": "Verlenging: toegestaan (+{period})",
+ "Extension: already extended": "Verlenging: reeds verlengd",
+ "Extension: not allowed": "Verlenging: niet toegestaan",
+ "External": "Extern",
+ "Failed to add participant": "Deelnemer toevoegen mislukt",
+ "Failed to add status type": "Statustype toevoegen mislukt",
+ "Failed to delete case type": "Zaaktype verwijderen mislukt",
+ "Failed to delete status type": "Statustype verwijderen mislukt",
+ "Failed to delete status type \"{name}\"": "Statustype \"{name}\" verwijderen mislukt",
+ "Failed to load dashboard data": "Dashboard gegevens laden mislukt",
+ "Failed to save": "Opslaan mislukt",
+ "Failed to save case type": "Zaaktype opslaan mislukt",
+ "File not found.": "Bestand niet gevonden.",
+ "Final": "Definitief",
+ "Final status": "Eindstatus",
+ "Forced unlocking is not allowed without the correct scope.": "Geforceerd unlocken is niet toegestaan zonder juiste scope.",
+ "General": "Algemeen",
+ "Handler": "Behandelaar",
+ "Handler action": "Behandelaar actie",
+ "High": "Hoog",
+ "Highly confidential": "Zeer vertrouwelijk",
+ "Identifier": "Identificatie",
+ "Initial status": "Beginstatus",
+ "Initiator action": "Initiator actie",
+ "Install OpenRegister": "Installeer OpenRegister",
+ "Internal": "Intern",
+ "Invalid JSON in one of the mapping fields: {error}": "Ongeldige JSON in een van de mappingvelden: {error}",
+ "Invalid chunk configuration.": "Ongeldige chunk configuratie.",
+ "Invalid sequence number. Expected 1-%s.": "Ongeldig volgnummer. Verwacht 1-%s.",
+ "Keywords": "Trefwoorden",
+ "Link to a case (optional)": "Koppel aan een zaak (optioneel)",
+ "Linked Case": "Gekoppelde zaak",
+ "Loading dashboard…": "Dashboard laden…",
+ "Loading...": "Laden...",
+ "Lock ID does not match and forced unlocking is not allowed.": "Lock ID komt niet overeen en geforceerd unlocken is niet toegestaan.",
+ "Lock ID does not match the stored lock.": "Lock ID komt niet overeen met de opgeslagen vergrendeling.",
+ "Lock ID does not match.": "Lock ID komt niet overeen.",
+ "Lock ID is missing from the request.": "Lock ID ontbreekt in het verzoek.",
+ "Lock ID is required for editing a locked document.": "Lock ID is vereist voor het bewerken van een vergrendeld document.",
+ "Low": "Laag",
+ "Manage case types and their configurations": "Beheer zaaktypen en hun configuraties",
+ "Manage cases and workflows": "Beheer zaken en workflows",
+ "Manage your cases and tasks": "Beheer uw zaken en taken",
+ "Mapping saved successfully": "Mapping succesvol opgeslagen",
+ "Missing required fields: {fields}": "Verplichte velden ontbreken: {fields}",
+ "Must be a valid ISO 8601 duration (e.g., P28D)": "Moet een geldige ISO 8601 duur zijn (bijv. P28D)",
+ "Must be a valid ISO 8601 duration (e.g., P42D)": "Moet een geldige ISO 8601 duur zijn (bijv. P42D)",
+ "Must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks, P2M for 2 months)": "Moet een geldige ISO 8601 duur zijn (bijv. P56D voor 56 dagen, P8W voor 8 weken, P2M voor 2 maanden)",
+ "Must be a valid ISO 8601 duration (e.g., P56D)": "Moet een geldige ISO 8601 duur zijn (bijv. P56D)",
+ "My Tasks": "Mijn taken",
+ "My Work": "Mijn werk",
+ "Name": "Naam",
+ "Name *": "Naam *",
+ "New Case": "Nieuwe zaak",
+ "New Case Type": "Nieuw zaaktype",
+ "New Task": "Nieuwe taak",
+ "New case": "Nieuwe zaak",
+ "New task": "Nieuwe taak",
+ "Next": "Volgende",
+ "No Procest register configured": "Geen Procest register geconfigureerd",
+ "No activity yet": "Nog geen activiteit",
+ "No cases found": "Geen zaken gevonden",
+ "No deadline": "Geen deadline",
+ "No file content received.": "Geen bestandsinhoud ontvangen.",
+ "No items assigned to you": "Geen items aan u toegewezen",
+ "No mapping configured for %s": "Geen mapping geconfigureerd voor %s",
+ "No open cases": "Geen openstaande zaken",
+ "No overdue cases": "Geen openstaande zaken",
+ "No participants assigned": "Geen deelnemers toegewezen",
+ "No reason provided": "Geen reden opgegeven",
+ "No recent activity": "Geen recente activiteit",
+ "No result recorded yet": "Nog geen resultaat geregistreerd",
+ "No settings available yet": "Nog geen instellingen beschikbaar",
+ "No status types defined. Add at least one to publish this case type.": "Geen statustypen gedefinieerd. Voeg er ten minste één toe om dit zaaktype te publiceren.",
+ "No tasks found": "Geen taken gevonden",
+ "No tasks yet": "Nog geen taken",
+ "No widgets configured": "Geen widgets geconfigureerd",
+ "Normal": "Normaal",
+ "Not configured": "Niet geconfigureerd",
+ "Not found.": "Niet gevonden.",
+ "Not set": "Niet ingesteld",
+ "Notification text": "Notificatietekst",
+ "Notify": "Notificeren",
+ "Notify initiator": "Initiator notificeren",
+ "Only locked documents may be edited.": "Alleen vergrendelde documenten mogen bewerkt worden.",
+ "Only published case types can be set as default": "Alleen gepubliceerde zaaktypen kunnen als standaard worden ingesteld",
+ "Open Cases": "Open zaken",
+ "OpenRegister is required": "OpenRegister is vereist",
+ "Optional description...": "Optionele omschrijving...",
+ "Order": "Volgorde",
+ "Order *": "Volgorde *",
+ "Order is required": "Volgorde is verplicht",
+ "Origin": "Oorsprong",
+ "Overdue": "Te laat",
+ "Overdue Cases": "Openstaande zaken",
+ "Participant": "Deelnemer",
+ "Participants": "Deelnemers",
+ "Please fix the validation errors": "Corrigeer de validatiefouten",
+ "Please select a result type": "Selecteer een resultaattype",
+ "Previous": "Vorige",
+ "Priority": "Prioriteit",
+ "Processing deadline": "Verwerkingsdeadline",
+ "Processing time": "Verwerkingstijd",
+ "Procest": "Procest",
+ "Procest needs the OpenRegister app to store and manage data. Please install OpenRegister from the app store to get started.": "Procest heeft de OpenRegister app nodig om gegevens op te slaan en te beheren. Installeer OpenRegister uit de app store om te beginnen.",
+ "Procest settings": "Procest instellingen",
+ "Product '%s' is not allowed for this zaaktype.": "Product '%s' is niet toegestaan voor dit zaaktype.",
+ "Property Mapping (outbound: English → Dutch)": "Eigenschap mapping (uitgaand: Engels → Nederlands)",
+ "Public": "Openbaar",
+ "Publication required": "Publicatie vereist",
+ "Publication text": "Publicatietekst",
+ "Publish": "Publiceren",
+ "Published": "Gepubliceerd",
+ "Purpose": "Doel",
+ "Query Parameter Mapping": "Query parameter mapping",
+ "Reason": "Reden",
+ "Reassign": "Hertoewijzen",
+ "Reassign handler to:": "Behandelaar hertoewijzen aan:",
+ "Recent Activity": "Recente activiteit",
+ "Reference process": "Referentieproces",
+ "Refresh dashboard": "Dashboard vernieuwen",
+ "Register": "Register",
+ "Register ID": "Register ID",
+ "Register and schema settings": "Register en schema instellingen",
+ "Remove this participant?": "Deze deelnemer verwijderen?",
+ "Request Extension": "Verlenging aanvragen",
+ "Reset": "Herstellen",
+ "Responsible unit": "Verantwoordelijke eenheid",
+ "Restricted": "Beperkt",
+ "Result": "Resultaat",
+ "Result (required)": "Resultaat (verplicht)",
+ "Result is required when closing a case": "Resultaat is verplicht bij het sluiten van een zaak",
+ "Result schema": "Resultaat schema",
+ "Retry": "Opnieuw proberen",
+ "Reverse Mapping (inbound: Dutch → English)": "Reverse mapping (inkomend: Nederlands → Engels)",
+ "Role schema": "Rol schema",
+ "Role type": "Roltype",
+ "Save": "Opslaan",
+ "Save the case type first before adding status types.": "Sla het zaaktype eerst op voordat u statustypen toevoegt.",
+ "Saved successfully": "Succesvol opgeslagen",
+ "Schema ID": "Schema ID",
+ "Search": "Zoeken",
+ "Secret": "Geheim",
+ "Select a case type...": "Selecteer een zaaktype...",
+ "Select due date": "Selecteer deadline",
+ "Select priority": "Selecteer prioriteit",
+ "Select result type...": "Selecteer resultaattype...",
+ "Select role type...": "Selecteer roltype...",
+ "Select user...": "Selecteer gebruiker...",
+ "Service target": "Servicenorm",
+ "Set as default": "Als standaard instellen",
+ "Settings": "Instellingen",
+ "Show completed": "Toon afgerond",
+ "Source Register": "Bronregister",
+ "Source Schema": "Bronschema",
+ "Start": "Start",
+ "Start date": "Startdatum",
+ "Started": "Gestart",
+ "Status": "Status",
+ "Status Timeline": "Status tijdlijn",
+ "Status changed from '": "Status gewijzigd van '",
+ "Status changed from '{from}' to '{to}'": "Status gewijzigd van '{from}' naar '{to}'",
+ "Status changed from \\": "Status gewijzigd van \\",
+ "Status changed to '": "Status gewijzigd naar '",
+ "Status changed to '{status}'": "Status gewijzigd naar '{status}'",
+ "Status changed to \\": "Status gewijzigd naar \\",
+ "Status schema": "Status schema",
+ "Status type name is required": "Statustype naam is verplicht",
+ "Status type schema": "Statustype schema",
+ "Statuses": "Statussen",
+ "Subject": "Onderwerp",
+ "TASK": "TAAK",
+ "Task": "Taak",
+ "Task Information": "Taak informatie",
+ "Task created successfully": "Taak succesvol aangemaakt",
+ "Task schema": "Taak schema",
+ "Task updated successfully": "Taak succesvol bijgewerkt",
+ "Tasks": "Taken",
+ "Terminate": "Beëindigen",
+ "Terminated": "Beëindigd",
+ "The document cannot be deleted.": "Het informatieobject kan niet verwijderd worden.",
+ "The document cannot be deleted: there are related ObjectInformatieObjecten.": "Het informatieobject kan niet verwijderd worden: er zijn gerelateerde ObjectInformatieObjecten.",
+ "The document is not locked. Lock the document first.": "Het document is niet vergrendeld. Vergrendel het document eerst.",
+ "This case has {count} linked tasks. Are you sure you want to delete it?": "Deze zaak heeft {count} gekoppelde taken. Weet u zeker dat u deze wilt verwijderen?",
+ "This document has no pending chunked upload.": "Dit document heeft geen openstaande chunked upload.",
+ "This will delete the case type and all {count} status types. Continue?": "Dit verwijdert het zaaktype en alle {count} statustypen. Doorgaan?",
+ "This will extend the deadline by {period}.": "Dit verlengt de deadline met {period}.",
+ "Title": "Titel",
+ "Title is required": "Titel is verplicht",
+ "Top secret": "Zeer geheim",
+ "Track and manage tasks": "Taken bijhouden en beheren",
+ "Trigger": "Trigger",
+ "Type: {type}": "Type: {type}",
+ "Unknown": "Onbekend",
+ "Unnamed case": "Naamloze zaak",
+ "Unnamed task": "Naamloze taak",
+ "Unpublish": "Depubliceren",
+ "Unpublishing this case type will prevent new cases from being created. Existing cases will continue to function. Continue?": "Het depubliceren van dit zaaktype voorkomt dat er nieuwe zaken worden aangemaakt. Bestaande zaken blijven functioneren. Doorgaan?",
+ "Upcoming": "Aankomend",
+ "Updated": "Bijgewerkt",
+ "Updated: {fields}": "Bijgewerkt: {fields}",
+ "Urgent": "Urgent",
+ "User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update.",
+ "Username": "Gebruikersnaam",
+ "Username (optional)": "Gebruikersnaam (optioneel)",
+ "Valid from": "Geldig vanaf",
+ "Valid until": "Geldig tot",
+ "Value Mappings (enum translations)": "Waarde mappings (enum vertalingen)",
+ "View all activity": "Alle activiteit bekijken",
+ "View all my work": "Al mijn werk bekijken",
+ "View all overdue": "Alle openstaande bekijken",
+ "View case": "Bekijk zaak",
+ "View task": "Bekijk taak",
+ "Welcome to Procest": "Welkom bij Procest",
+ "Welcome to Procest! Get started by creating your first case or task using the buttons above.": "Welkom bij Procest! Begin door uw eerste zaak of taak aan te maken met de knoppen hierboven.",
+ "Welcome to Procest! Get started by creating your first case type in Settings.": "Welkom bij Procest! Begin door uw eerste zaaktype aan te maken in Instellingen.",
+ "When heeftAlleAutorisaties is false, autorisaties must be specified.": "Wanneer heeftAlleAutorisaties false is, dan moet autorisaties opgegeven worden.",
+ "When heeftAlleAutorisaties is true, autorisaties must not be specified. When heeftAlleAutorisaties is false, autorisaties must be specified.": "Wanneer heeftAlleAutorisaties op true staat, mag autorisaties niet opgegeven worden. Indien heeftAlleAutorisaties false is, dan moet autorisaties opgegeven worden.",
+ "Why is an extension needed?": "Waarom is een verlenging nodig?",
+ "Widget not available": "Widget niet beschikbaar",
+ "You do not have the correct permissions for this action.": "U heeft niet de juiste rechten voor deze actie.",
+ "ZGW API Mapping": "ZGW API Mapping",
+ "ZGW Resource": "ZGW Bron",
+ "action needed": "actie vereist",
+ "all on track": "alles op schema",
+ "avg {days} days": "gem. {days} dagen",
+ "besluittype is required when a scope related to besluiten is specified.": "besluittype is verplicht wanneer een scope m.b.t. besluiten is opgegeven.",
+ "by {user}": "door {user}",
+ "closed": "gesloten",
+ "completed": "afgerond",
+ "e.g., P28D (28 days)": "bijv. P28D (28 dagen)",
+ "e.g., P42D (42 days)": "bijv. P42D (42 dagen)",
+ "e.g., P56D (56 days)": "bijv. P56D (56 dagen)",
+ "high": "hoog",
+ "in_progress": "in behandeling",
+ "informatieobjecttype is required when a scope related to documenten is specified.": "informatieobjecttype is verplicht wanneer een scope m.b.t. documenten is opgegeven.",
+ "just now": "zojuist",
+ "low": "laag",
+ "maxVertrouwelijkheidaanduiding is required when a scope related to documenten is specified.": "maxVertrouwelijkheidaanduiding is verplicht wanneer een scope m.b.t. documenten is opgegeven.",
+ "maxVertrouwelijkheidaanduiding is required when a scope related to zaken is specified.": "maxVertrouwelijkheidaanduiding is verplicht wanneer een scope m.b.t. zaken is opgegeven.",
+ "no data": "geen gegevens",
+ "none due today": "geen deadlines vandaag",
+ "normal": "normaal",
+ "open": "open",
+ "overdue": "te laat",
+ "productenOfDiensten contains a value not present in the zaaktype.": "productenOfDiensten bevat een waarde die niet in het zaaktype voorkomt.",
+ "tasks": "taken",
+ "urgent": "urgent",
+ "yesterday": "gisteren",
+ "zaaktype is required when a scope related to zaken is specified.": "zaaktype is verplicht wanneer een scope m.b.t. zaken is opgegeven.",
+ "{days} days": "{days} dagen",
+ "{days} days ago": "{days} dagen geleden",
+ "{days} days overdue": "{days} dagen te laat",
+ "{days} days remaining": "{days} dagen resterend",
+ "{field} is required": "{field} is verplicht",
+ "{from} \\u2014 (no end)": "{from} \\u2014 (no end)",
+ "{from} — (no end)": "{from} — (geen eind)",
+ "{hours} hours ago": "{hours} uur geleden",
+ "{min} min ago": "{min} min geleden",
+ "{n} days": "{n} dagen",
+ "{n} due today": "{n} vandaag verlopen",
+ "{n} months": "{n} maanden",
+ "{n} weeks": "{n} weken",
+ "{n} years": "{n} jaar"
+ }
}
diff --git a/lib/Controller/BrcController.php b/lib/Controller/BrcController.php
index ba7267c9..a28375fb 100644
--- a/lib/Controller/BrcController.php
+++ b/lib/Controller/BrcController.php
@@ -492,7 +492,11 @@ private function indexBesluitInformatieObjecten(): JSONResponse
$outboundMapping = $this->zgwService->createOutboundMapping(mappingConfig: $mappingConfig);
$mapped = [];
foreach ($objects as $object) {
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$mapped[] = $this->zgwService->applyOutboundMapping(
objectData: $objectData,
@@ -569,7 +573,7 @@ private function createBesluitInformatieObject(): JSONResponse
mappingConfig: $mappingConfig
);
- /** @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change */
+ // @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change
if (is_array($englishData) === false) {
return new JSONResponse(
data: ['detail' => 'Invalid mapping result'],
@@ -582,7 +586,11 @@ private function createBesluitInformatieObject(): JSONResponse
schema: $mappingConfig['sourceSchema'],
object: $englishData
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$objectUuid = $objectData['id'] ?? ($objectData['@self']['id'] ?? '');
@@ -689,7 +697,11 @@ private function deleteOiosForBesluit(string $besluitUrl): void
$result = $objectService->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $oio) {
- $oioData = is_array($oio) === true ? $oio : $oio->jsonSerialize();
+ if (is_array($oio) === true) {
+ $oioData = $oio;
+ } else {
+ $oioData = $oio->jsonSerialize();
+ }
$oioUuid = $oioData['id'] ?? ($oioData['@self']['id'] ?? '');
if ($oioUuid !== '') {
@@ -734,7 +746,11 @@ private function destroyBesluitInformatieObject(string $uuid): JSONResponse
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $bioData = is_array($bioObj) === true ? $bioObj : $bioObj->jsonSerialize();
+ if (is_array($bioObj) === true) {
+ $bioData = $bioObj;
+ } else {
+ $bioData = $bioObj->jsonSerialize();
+ }
// Build the besluit URL from the stored decision UUID.
$decisionUuid = $bioData['decision'] ?? '';
@@ -810,7 +826,11 @@ private function deleteOioByBesluitAndIo(string $besluitUrl, string $ioUrl): voi
$result = $objectService->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $oio) {
- $oioData = is_array($oio) === true ? $oio : $oio->jsonSerialize();
+ if (is_array($oio) === true) {
+ $oioData = $oio;
+ } else {
+ $oioData = $oio->jsonSerialize();
+ }
$oioUuid = $oioData['id'] ?? ($oioData['@self']['id'] ?? '');
if ($oioUuid !== '') {
@@ -857,7 +877,11 @@ private function destroyBesluit(string $uuid): JSONResponse
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existingObj) === true ? $existingObj : $existingObj->jsonSerialize();
+ if (is_array($existingObj) === true) {
+ $existingData = $existingObj;
+ } else {
+ $existingData = $existingObj->jsonSerialize();
+ }
// Run destroy business rules.
$ruleResult = $this->zgwService->getBusinessRulesService()->validate(
diff --git a/lib/Controller/DrcController.php b/lib/Controller/DrcController.php
index 27a6f608..3641d70e 100644
--- a/lib/Controller/DrcController.php
+++ b/lib/Controller/DrcController.php
@@ -158,7 +158,11 @@ private function indexFlatArray(string $resource): JSONResponse
$outboundMapping = $this->zgwService->createOutboundMapping(mappingConfig: $mappingConfig);
$mapped = [];
foreach ($objects as $object) {
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$mapped[] = $this->zgwService->applyOutboundMapping(
objectData: $objectData,
@@ -261,7 +265,7 @@ public function create(string $resource): JSONResponse
unset($englishData['content']);
}
- /** @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change */
+ // @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change
if (is_array($englishData) === false) {
return new JSONResponse(
data: ['detail' => 'Invalid mapping result'],
@@ -290,7 +294,11 @@ public function create(string $resource): JSONResponse
schema: $mappingConfig['sourceSchema'],
object: $englishData
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$objectUuid = $objectData['id'] ?? ($objectData['@self']['id'] ?? '');
@@ -328,7 +336,7 @@ public function create(string $resource): JSONResponse
);
// Add bestandsdelen for chunked upload responses.
- $chunkInfo = $this->parseFileParts(objectData: $objectData);
+ $chunkInfo = $this->parseFileParts(objectData: $objectData);
$mapped['bestandsdelen'] = [];
if ($chunkInfo !== null && ($chunkInfo['pending'] ?? false) === true) {
$mapped['bestandsdelen'] = $this->buildBestandsdelenArray(
@@ -492,7 +500,11 @@ public function destroy(string $resource, string $uuid): JSONResponse
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $existingData = $existing;
+ } else {
+ $existingData = $existing->jsonSerialize();
+ }
$fileName = $existingData['fileName'] ?? 'document';
if ($fileName === '') {
@@ -586,7 +598,11 @@ public function download(string $uuid): DataDownloadResponse|JSONResponse
schema: $mappingConfig['sourceSchema'],
id: $uuid
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$fileName = $objectData['fileName'] ?? 'document';
if ($fileName === '') {
@@ -716,7 +732,11 @@ private function lockFallback(object $objectService, string $uuid, \Throwable $o
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $existingData = $existing;
+ } else {
+ $existingData = $existing->jsonSerialize();
+ }
$lockId = bin2hex(random_bytes(16));
@@ -801,9 +821,11 @@ public function unlock(string $uuid): JSONResponse
'geforceerd-bijwerken'
);
if ($hasForceScope === false) {
- $detail = ($lockId === '')
- ? $this->l10n->t('Forced unlocking is not allowed without the correct scope.')
- : $this->l10n->t('Lock ID does not match and forced unlocking is not allowed.');
+ if ($lockId === '') {
+ $detail = $this->l10n->t('Forced unlocking is not allowed without the correct scope.');
+ } else {
+ $detail = $this->l10n->t('Lock ID does not match and forced unlocking is not allowed.');
+ }
return new JSONResponse(
data: [
@@ -863,7 +885,11 @@ private function unlockFallback(object $objectService, string $uuid, \Throwable
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $existingData = $existing;
+ } else {
+ $existingData = $existing->jsonSerialize();
+ }
unset($existingData['@self'], $existingData['id'], $existingData['organisation']);
$existingData['locked'] = false;
@@ -1090,7 +1116,11 @@ private function extractIdsFromResults(array $result): array
{
$ids = [];
foreach (($result['results'] ?? []) as $obj) {
- $data = is_array($obj) === true ? $obj : $obj->jsonSerialize();
+ if (is_array($obj) === true) {
+ $data = $obj;
+ } else {
+ $data = $obj->jsonSerialize();
+ }
$id = $data['id'] ?? ($data['@self']['id'] ?? null);
if ($id !== null) {
@@ -1129,7 +1159,11 @@ private function cascadeDeleteGebruiksrechten(string $eioUuid): void
$result = $objectService->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $gr) {
- $grData = is_array($gr) === true ? $gr : $gr->jsonSerialize();
+ if (is_array($gr) === true) {
+ $grData = $gr;
+ } else {
+ $grData = $gr->jsonSerialize();
+ }
$grUuid = $grData['id'] ?? ($grData['@self']['id'] ?? '');
if ($grUuid !== '') {
@@ -1192,7 +1226,11 @@ private function getGebruiksrechtData(string $uuid): ?array
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $data = is_array($obj) === true ? $obj : $obj->jsonSerialize();
+ if (is_array($obj) === true) {
+ $data = $obj;
+ } else {
+ $data = $obj->jsonSerialize();
+ }
$ioRef = $data['document'] ?? ($data['informatieobject'] ?? '');
$uuidPattern = '/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i';
@@ -1246,7 +1284,11 @@ private function checkAndClearIndicatieGebruiksrecht(string $eioUuid): void
register: $eioConfig['sourceRegister'],
schema: $eioConfig['sourceSchema']
);
- $eioData = is_array($eioObj) === true ? $eioObj : $eioObj->jsonSerialize();
+ if (is_array($eioObj) === true) {
+ $eioData = $eioObj;
+ } else {
+ $eioData = $eioObj->jsonSerialize();
+ }
$eioData['usageRightsIndication'] = null;
@@ -1302,7 +1344,11 @@ private function setIndicatieGebruiksrecht(string $ioUrl, ?bool $value): void
register: $eioConfig['sourceRegister'],
schema: $eioConfig['sourceSchema']
);
- $eioData = is_array($eioObj) === true ? $eioObj : $eioObj->jsonSerialize();
+ if (is_array($eioObj) === true) {
+ $eioData = $eioObj;
+ } else {
+ $eioData = $eioObj->jsonSerialize();
+ }
$eioData['usageRightsIndication'] = $value;
@@ -1362,7 +1408,11 @@ public function uploadChunk(string $uuid): JSONResponse
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $objectData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $objectData = $existing;
+ } else {
+ $objectData = $existing->jsonSerialize();
+ }
// Verify this document has a pending chunked upload.
$chunkInfo = $this->parseFileParts(objectData: $objectData);
@@ -1496,14 +1546,18 @@ private function enrichWithBestandsdelen(JSONResponse $response, string $uuid):
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $objectData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $objectData = $existing;
+ } else {
+ $objectData = $existing->jsonSerialize();
+ }
$data = $response->getData();
if (is_array($data) === false) {
return;
}
- $chunkInfo = $this->parseFileParts(objectData: $objectData);
+ $chunkInfo = $this->parseFileParts(objectData: $objectData);
$data['bestandsdelen'] = [];
if ($chunkInfo !== null && ($chunkInfo['pending'] ?? false) === true) {
$data['bestandsdelen'] = $this->buildBestandsdelenArray(
@@ -1618,7 +1672,11 @@ private function handleEioUpdate(string $resource, string $uuid, bool $partial):
return $lockError;
}
- $action = ($partial === true) ? 'partial_update' : 'update';
+ if ($partial === true) {
+ $action = 'partial_update';
+ } else {
+ $action = 'update';
+ }
$ruleResult = $this->zgwService->getBusinessRulesService()->validate(
zgwApi: self::ZGW_API,
@@ -1645,7 +1703,11 @@ private function handleEioUpdate(string $resource, string $uuid, bool $partial):
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $existingData = $existing;
+ } else {
+ $existingData = $existing->jsonSerialize();
+ }
$inboundMapping = $this->zgwService->createInboundMapping(mappingConfig: $mappingConfig);
$englishData = $this->zgwService->applyInboundMapping(
@@ -1658,7 +1720,7 @@ private function handleEioUpdate(string $resource, string $uuid, bool $partial):
unset($englishData['content']);
}
- /** @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change */
+ // @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change
if (is_array($englishData) === false) {
return new JSONResponse(
data: ['detail' => 'Invalid mapping result'],
@@ -1676,7 +1738,11 @@ private function handleEioUpdate(string $resource, string $uuid, bool $partial):
object: $englishData,
uuid: $uuid
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$objectUuid = $objectData['id'] ?? ($objectData['@self']['id'] ?? $uuid);
@@ -1749,7 +1815,7 @@ private function handleEioUpdate(string $resource, string $uuid, bool $partial):
*
* @return JSONResponse|null Error response if lock check fails, null if OK.
*
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag) — $partial distinguishes PUT vs PATCH lock semantics
+ * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
*/
private function checkDocumentLock(
array $mappingConfig,
@@ -1790,8 +1856,13 @@ private function checkDocumentLock(
if ($providedLockId === '') {
// PUT (full update): lock is a required field (drc-009d).
// PATCH (partial): lock is missing for lock enforcement (drc-009e).
- $errorName = ($partial === false) ? 'lock' : 'nonFieldErrors';
- $errorCode = ($partial === false) ? 'required' : 'missing-lock-id';
+ if ($partial === false) {
+ $errorName = 'lock';
+ $errorCode = 'required';
+ } else {
+ $errorName = 'nonFieldErrors';
+ $errorCode = 'missing-lock-id';
+ }
return new JSONResponse(
data: [
@@ -1865,7 +1936,11 @@ private function resolveStoredLockId(
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $existingData = $existing;
+ } else {
+ $existingData = $existing->jsonSerialize();
+ }
// Check for stored lockId first.
$lockId = $existingData['lockId'] ?? null;
@@ -1909,7 +1984,11 @@ private function storeLockIdInData(
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $existingData = $existing;
+ } else {
+ $existingData = $existing->jsonSerialize();
+ }
unset($existingData['@self'], $existingData['id'], $existingData['organisation']);
$existingData['locked'] = true;
@@ -1948,7 +2027,11 @@ private function clearLockIdInData(
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existing) === true ? $existing : $existing->jsonSerialize();
+ if (is_array($existing) === true) {
+ $existingData = $existing;
+ } else {
+ $existingData = $existing->jsonSerialize();
+ }
unset($existingData['@self'], $existingData['id'], $existingData['organisation']);
$existingData['locked'] = false;
diff --git a/lib/Controller/HealthController.php b/lib/Controller/HealthController.php
index 2af149a6..4dd21d58 100644
--- a/lib/Controller/HealthController.php
+++ b/lib/Controller/HealthController.php
@@ -78,7 +78,11 @@ public function index(): JSONResponse
$status = 'degraded';
}
- $httpStatus = ($status === 'ok') ? Http::STATUS_OK : Http::STATUS_SERVICE_UNAVAILABLE;
+ if ($status === 'ok') {
+ $httpStatus = Http::STATUS_OK;
+ } else {
+ $httpStatus = Http::STATUS_SERVICE_UNAVAILABLE;
+ }
return new JSONResponse(
[
diff --git a/lib/Controller/ZrcController.php b/lib/Controller/ZrcController.php
index 20c891a9..20fa14e8 100644
--- a/lib/Controller/ZrcController.php
+++ b/lib/Controller/ZrcController.php
@@ -170,7 +170,7 @@ public function create(string $resource): JSONResponse
$originalBody = $body;
// ZRC-specific: resolve zaak closed from body before validation.
- $zaakClosed = $this->zgwService->resolveZaakClosedFromBody($resource, $body);
+ $zaakClosed = $this->zgwService->resolveZaakClosedFromBody($resource, $body);
$hasGeforceerd = true;
if ($zaakClosed === true) {
$hasGeforceerd = $this->zgwService->consumerHasScope(
@@ -206,7 +206,7 @@ public function create(string $resource): JSONResponse
mappingConfig: $mappingConfig
);
- /** @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change */
+ // @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change
if (is_array($englishData) === false) {
return new JSONResponse(
data: ['detail' => 'Invalid mapping result'],
@@ -235,7 +235,11 @@ public function create(string $resource): JSONResponse
schema: $mappingConfig['sourceSchema'],
object: $englishData
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$objectUuid = $objectData['id'] ?? ($objectData['@self']['id'] ?? '');
@@ -644,7 +648,11 @@ public function zaakbesluitenIndex(string $zaakUuid): JSONResponse
$outboundMapping = $this->zgwService->createOutboundMapping(mappingConfig: $mappingConfig);
$mapped = [];
foreach (($result['results'] ?? []) as $object) {
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$mapped[] = $this->zgwService->applyOutboundMapping(
objectData: $objectData,
@@ -684,7 +692,7 @@ public function zoek(): JSONResponse
$response = $this->index(resource: 'zaken');
$response->setStatus(Http::STATUS_CREATED);
- /** @var JSONResponse $response */
+ // @var JSONResponse $response
return $response;
}//end zoek()
@@ -766,7 +774,11 @@ private function checkZaakReadAccess(string $uuid): ?JSONResponse
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $zaakData = is_array($zaakObj) === true ? $zaakObj : $zaakObj->jsonSerialize();
+ if (is_array($zaakObj) === true) {
+ $zaakData = $zaakObj;
+ } else {
+ $zaakData = $zaakObj->jsonSerialize();
+ }
$zaakVa = $zaakData['confidentiality'] ?? ($zaakData['vertrouwelijkheidaanduiding'] ?? 'openbaar');
$zaakLevel = self::VERTROUWELIJKHEID_LEVELS[$zaakVa] ?? 1;
@@ -779,7 +791,11 @@ private function checkZaakReadAccess(string $uuid): ?JSONResponse
}
$maxVa = $auth['maxVertrouwelijkheidaanduiding'] ?? ($auth['max_vertrouwelijkheidaanduiding'] ?? null);
- $maxLevel = ($maxVa !== null) ? (self::VERTROUWELIJKHEID_LEVELS[$maxVa] ?? 99) : 99;
+ if ($maxVa !== null) {
+ $maxLevel = self::VERTROUWELIJKHEID_LEVELS[$maxVa] ?? 99;
+ } else {
+ $maxLevel = 99;
+ }
if ($zaakLevel <= $maxLevel) {
return null;
@@ -844,7 +860,11 @@ private function filterZakenByAuthorisation(JSONResponse $response): JSONRespons
foreach ($lezenAuths as $auth) {
$maxVa = $auth['maxVertrouwelijkheidaanduiding'] ?? ($auth['max_vertrouwelijkheidaanduiding'] ?? null);
- $maxLevel = ($maxVa !== null) ? (self::VERTROUWELIJKHEID_LEVELS[$maxVa] ?? 99) : 99;
+ if ($maxVa !== null) {
+ $maxLevel = self::VERTROUWELIJKHEID_LEVELS[$maxVa] ?? 99;
+ } else {
+ $maxLevel = 99;
+ }
if ($zaakLevel <= $maxLevel) {
$filtered[] = $zaak;
@@ -929,7 +949,11 @@ private function preValidateZaakBody(bool $isPatch): ?JSONResponse
$segments = array_filter(explode('/', trim($path, '/')));
$last = end($segments);
$looksLikeUuid = preg_match('/[0-9a-f]{4,}-/i', (string) $last) === 1;
- $code = ($looksLikeUuid === true) ? 'bad-url' : 'invalid-resource';
+ if ($looksLikeUuid === true) {
+ $code = 'bad-url';
+ } else {
+ $code = 'invalid-resource';
+ }
return new JSONResponse(
data: [
@@ -1002,7 +1026,11 @@ private function preValidateProductenOfDiensten(
register: $ztConfig['sourceRegister'],
schema: $ztConfig['sourceSchema']
);
- $ztData = is_array($ztObj) === true ? $ztObj : $ztObj->jsonSerialize();
+ if (is_array($ztObj) === true) {
+ $ztData = $ztObj;
+ } else {
+ $ztData = $ztObj->jsonSerialize();
+ }
} catch (\Throwable $e) {
return null;
}
@@ -1090,7 +1118,11 @@ private function destroyZaak(string $uuid): JSONResponse
$result = $objectService->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $obj) {
- $data = is_array($obj) === true ? $obj : $obj->jsonSerialize();
+ if (is_array($obj) === true) {
+ $data = $obj;
+ } else {
+ $data = $obj->jsonSerialize();
+ }
$subUuid = $data['id'] ?? ($data['@self']['id'] ?? '');
if ($subUuid === '') {
@@ -1159,7 +1191,11 @@ private function resolveZaakClosedForExisting(string $resource, string $uuid): a
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existingObj) === true ? $existingObj : $existingObj->jsonSerialize();
+ if (is_array($existingObj) === true) {
+ $existingData = $existingObj;
+ } else {
+ $existingData = $existingObj->jsonSerialize();
+ }
$zaakClosed = $this->zgwService->resolveZaakClosed($resource, $existingData);
$hasGeforceerd = true;
@@ -1217,7 +1253,11 @@ private function checkReopenScope(array $body): ?JSONResponse
register: $zaakConfig['sourceRegister'],
schema: $zaakConfig['sourceSchema']
);
- $zaakData = is_array($zaak) === true ? $zaak : $zaak->jsonSerialize();
+ if (is_array($zaak) === true) {
+ $zaakData = $zaak;
+ } else {
+ $zaakData = $zaak->jsonSerialize();
+ }
$endDate = $zaakData['endDate'] ?? null;
@@ -1241,7 +1281,11 @@ private function checkReopenScope(array $body): ?JSONResponse
register: $stConfig['sourceRegister'],
schema: $stConfig['sourceSchema']
);
- $stData = is_array($statustype) === true ? $statustype : $statustype->jsonSerialize();
+ if (is_array($statustype) === true) {
+ $stData = $statustype;
+ } else {
+ $stData = $statustype->jsonSerialize();
+ }
$isEindstatus = $stData['isFinal'] ?? ($stData['isFinalStatus'] ?? ($stData['isEindstatus'] ?? false));
@@ -1309,7 +1353,11 @@ private function checkIndicatieGebruiksrechtBeforeClose(array $body): ?JSONRespo
return null;
}
- $stData = is_array($statustype) === true ? $statustype : $statustype->jsonSerialize();
+ if (is_array($statustype) === true) {
+ $stData = $statustype;
+ } else {
+ $stData = $statustype->jsonSerialize();
+ }
$isEindstatus = $stData['isFinal'] ?? ($stData['isFinalStatus'] ?? ($stData['isEindstatus'] ?? false));
@@ -1348,7 +1396,11 @@ private function checkIndicatieGebruiksrechtBeforeClose(array $body): ?JSONRespo
schema: $zaakConfig['sourceSchema']
);
if ($zaakObj !== null) {
- $zaakData = is_array($zaakObj) === true ? $zaakObj : $zaakObj->jsonSerialize();
+ if (is_array($zaakObj) === true) {
+ $zaakData = $zaakObj;
+ } else {
+ $zaakData = $zaakObj->jsonSerialize();
+ }
$endDate = $zaakData['endDate'] ?? ($zaakData['einddatum'] ?? null);
$zaakAlreadyClosed = ($endDate !== null && $endDate !== '');
@@ -1375,7 +1427,11 @@ private function checkIndicatieGebruiksrechtBeforeClose(array $body): ?JSONRespo
$zioResult = $this->zgwService->getObjectService()->searchObjectsPaginated(query: $query);
foreach (($zioResult['results'] ?? []) as $zioObj) {
- $zioData = is_array($zioObj) === true ? $zioObj : $zioObj->jsonSerialize();
+ if (is_array($zioObj) === true) {
+ $zioData = $zioObj;
+ } else {
+ $zioData = $zioObj->jsonSerialize();
+ }
$docUuid = $zioData['document'] ?? ($zioData['informatieobject'] ?? '');
@@ -1388,7 +1444,11 @@ private function checkIndicatieGebruiksrechtBeforeClose(array $body): ?JSONRespo
register: $docConfig['sourceRegister'],
schema: $docConfig['sourceSchema']
);
- $docData = is_array($docObj) === true ? $docObj : $docObj->jsonSerialize();
+ if (is_array($docObj) === true) {
+ $docData = $docObj;
+ } else {
+ $docData = $docObj->jsonSerialize();
+ }
$indGr = $docData['usageRightsIndication'] ?? ($docData['usageRightsIndicator'] ?? ($docData['indicatieGebruiksrecht'] ?? null));
@@ -1461,7 +1521,11 @@ private function isEindstatusByVolgnummer(array $stData, array $stConfig, string
$maxOrder = 0;
foreach (($result['results'] ?? []) as $st) {
- $stObj = is_array($st) === true ? $st : $st->jsonSerialize();
+ if (is_array($st) === true) {
+ $stObj = $st;
+ } else {
+ $stObj = $st->jsonSerialize();
+ }
$order = (int) ($stObj['order'] ?? ($stObj['volgnummer'] ?? 0));
if ($order > $maxOrder) {
@@ -1515,7 +1579,11 @@ private function handleEindstatusEffect(array $body, array $objectData): void
return;
}
- $stData = is_array($statustype) === true ? $statustype : $statustype->jsonSerialize();
+ if (is_array($statustype) === true) {
+ $stData = $statustype;
+ } else {
+ $stData = $statustype->jsonSerialize();
+ }
$isEindstatus = $stData['isFinal'] ?? ($stData['isFinalStatus'] ?? ($stData['isEindstatus'] ?? false));
@@ -1558,7 +1626,11 @@ private function handleEindstatusEffect(array $body, array $objectData): void
$maxOrder = 0;
foreach (($result['results'] ?? []) as $st) {
- $stObj = is_array($st) === true ? $st : $st->jsonSerialize();
+ if (is_array($st) === true) {
+ $stObj = $st;
+ } else {
+ $stObj = $st->jsonSerialize();
+ }
$order = (int) ($stObj['order'] ?? ($stObj['volgnummer'] ?? 0));
if ($order > $maxOrder) {
@@ -1595,7 +1667,11 @@ private function handleEindstatusEffect(array $body, array $objectData): void
return;
}
- $zaakData = is_array($zaak) === true ? $zaak : $zaak->jsonSerialize();
+ if (is_array($zaak) === true) {
+ $zaakData = $zaak;
+ } else {
+ $zaakData = $zaak->jsonSerialize();
+ }
// Strip metadata that confuses saveObject on re-save.
unset($zaakData['@self'], $zaakData['organisation']);
@@ -1640,7 +1716,7 @@ private function handleEindstatusEffect(array $body, array $objectData): void
// Zrc-007b: Set indicatieGebruiksrecht on all related informatieobjecten.
$this->setIndicatieGebruiksrechtOnClose(zaakUuid: $zaakMatches[1]);
- }
+ }//end if
if ($isEindstatus === false) {
// Zrc-008: Heropenen zaak — when a non-eindstatus is created on
@@ -1700,7 +1776,11 @@ private function setIndicatieGebruiksrechtOnClose(string $zaakUuid): void
$result = $this->zgwService->getObjectService()->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $zioObj) {
- $zioData = is_array($zioObj) === true ? $zioObj : $zioObj->jsonSerialize();
+ if (is_array($zioObj) === true) {
+ $zioData = $zioObj;
+ } else {
+ $zioData = $zioObj->jsonSerialize();
+ }
$docUuid = $zioData['document'] ?? ($zioData['informatieobject'] ?? '');
@@ -1715,7 +1795,11 @@ private function setIndicatieGebruiksrechtOnClose(string $zaakUuid): void
register: $docConfig['sourceRegister'],
schema: $docConfig['sourceSchema']
);
- $docData = is_array($docObj) === true ? $docObj : $docObj->jsonSerialize();
+ if (is_array($docObj) === true) {
+ $docData = $docObj;
+ } else {
+ $docData = $docObj->jsonSerialize();
+ }
// Check if indicatieGebruiksrecht is already set.
$indGr = $docData['usageRightsIndication'] ?? ($docData['usageRightsIndicator'] ?? ($docData['indicatieGebruiksrecht'] ?? null));
@@ -1799,7 +1883,11 @@ private function handleResultaatCreated(array $body, array $objectData): void
register: $zaakConfig['sourceRegister'],
schema: $zaakConfig['sourceSchema']
);
- $zaakData = is_array($zaakObj) === true ? $zaakObj : $zaakObj->jsonSerialize();
+ if (is_array($zaakObj) === true) {
+ $zaakData = $zaakObj;
+ } else {
+ $zaakData = $zaakObj->jsonSerialize();
+ }
// Use the zaak endDate as einddatum (may be null if zaak isn't closed yet).
$einddatum = $zaakData['endDate'] ?? date('Y-m-d');
@@ -1879,7 +1967,11 @@ private function deriveArchiefactiedatum(array $zaakData, array $zaakConfig, str
}
$resultaat = $results[0];
- $resultaatData = is_array($resultaat) === true ? $resultaat : $resultaat->jsonSerialize();
+ if (is_array($resultaat) === true) {
+ $resultaatData = $resultaat;
+ } else {
+ $resultaatData = $resultaat->jsonSerialize();
+ }
// Get the resultaattype to find brondatumArchiefprocedure.
$resultaattypeId = $resultaatData['resultType'] ?? ($resultaatData['resultaattype'] ?? '');
@@ -1906,7 +1998,11 @@ private function deriveArchiefactiedatum(array $zaakData, array $zaakConfig, str
return $zaakData;
}
- $rtData = is_array($rtObj) === true ? $rtObj : $rtObj->jsonSerialize();
+ if (is_array($rtObj) === true) {
+ $rtData = $rtObj;
+ } else {
+ $rtData = $rtObj->jsonSerialize();
+ }
// Get brondatumArchiefprocedure.
$brondatum = $rtData['sourceDateArchiveProcedure'] ?? ($rtData['brondatumArchiefprocedure'] ?? null);
@@ -2021,7 +2117,11 @@ private function resolveArchiveBaseDate(
register: $zaakConfig['sourceRegister'],
schema: $zaakConfig['sourceSchema']
);
- $mainData = is_array($mainZaak) === true ? $mainZaak : $mainZaak->jsonSerialize();
+ if (is_array($mainZaak) === true) {
+ $mainData = $mainZaak;
+ } else {
+ $mainData = $mainZaak->jsonSerialize();
+ }
$mainEnd = $mainData['endDate'] ?? null;
if ($mainEnd !== null && $mainEnd !== '') {
@@ -2094,7 +2194,11 @@ private function resolveEigenschapDate(array $zaakData, string $datumkenmerk): ?
$results = $result['results'] ?? [];
if (empty($results) === false) {
$propObj = $results[0];
- $propData = is_array($propObj) === true ? $propObj : $propObj->jsonSerialize();
+ if (is_array($propObj) === true) {
+ $propData = $propObj;
+ } else {
+ $propData = $propObj->jsonSerialize();
+ }
$value = $propData['value'] ?? ($propData['waarde'] ?? '');
if ($value !== '' && strtotime($value) !== false) {
@@ -2145,7 +2249,11 @@ private function resolveBesluitDate(array $zaakData, string $englishField, strin
// Find the latest (maximum) date among all besluiten for this zaak.
$latestDate = null;
foreach ($results as $besluitObj) {
- $besluitData = is_array($besluitObj) === true ? $besluitObj : $besluitObj->jsonSerialize();
+ if (is_array($besluitObj) === true) {
+ $besluitData = $besluitObj;
+ } else {
+ $besluitData = $besluitObj->jsonSerialize();
+ }
$dateVal = $besluitData[$englishField] ?? ($besluitData[$dutchField] ?? '');
if ($dateVal !== '' && strtotime($dateVal) !== false) {
@@ -2243,7 +2351,7 @@ private function syncCreateObjectInformatieObject(string $zaakUrl, string $ioUrl
mappingConfig: $oioConfig
);
- /** @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change */
+ // @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change
if (is_array($englishData) === false) {
$englishData = $oioData;
}
@@ -2284,7 +2392,11 @@ private function getZioDataForOioSync(string $uuid): ?array
register: $zioConfig['sourceRegister'],
schema: $zioConfig['sourceSchema']
);
- $zioData = is_array($zioObj) === true ? $zioObj : $zioObj->jsonSerialize();
+ if (is_array($zioObj) === true) {
+ $zioData = $zioObj;
+ } else {
+ $zioData = $zioObj->jsonSerialize();
+ }
// The ZIO stores 'case' as a UUID (format: uuid with $ref) and
// 'document' as a full URL (format: uri). Build the zaak URL from
@@ -2341,7 +2453,11 @@ private function syncDeleteObjectInformatieObject(string $zaakUrl, string $ioUrl
$result = $this->zgwService->getObjectService()->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $oioObj) {
- $oioData = is_array($oioObj) === true ? $oioObj : $oioObj->jsonSerialize();
+ if (is_array($oioObj) === true) {
+ $oioData = $oioObj;
+ } else {
+ $oioData = $oioObj->jsonSerialize();
+ }
$oioUuid = $oioData['id'] ?? ($oioData['@self']['id'] ?? '');
if ($oioUuid !== '') {
diff --git a/lib/Controller/ZtcController.php b/lib/Controller/ZtcController.php
index 2c10ba6c..a3c881c7 100644
--- a/lib/Controller/ZtcController.php
+++ b/lib/Controller/ZtcController.php
@@ -265,7 +265,11 @@ private function resolveParentDraft(string $resource, string $uuid): ?bool
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existingObj) === true ? $existingObj : $existingObj->jsonSerialize();
+ if (is_array($existingObj) === true) {
+ $existingData = $existingObj;
+ } else {
+ $existingData = $existingObj->jsonSerialize();
+ }
return $this->zgwService->resolveParentZaaktypeDraft($resource, $existingData);
} catch (\Throwable $e) {
@@ -457,7 +461,11 @@ private function handlePublish(string $resource, string $uuid): JSONResponse
object: $existingData,
uuid: $uuid
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$baseUrl = $this->zgwService->buildBaseUrl($this->request, self::ZGW_API, $resource);
$outboundMapping = $this->zgwService->createOutboundMapping(mappingConfig: $mappingConfig);
@@ -624,14 +632,18 @@ private function enrichBesluittype(
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
// Expand documentTypes UUIDs to informatieobjecttypen URLs.
- $docTypes = $objectData['documentTypes'] ?? '';
+ $docTypes = $objectData['documentTypes'] ?? '';
$docTypeIds = [];
if (is_string($docTypes) === true && $docTypes !== '') {
$docTypeIds = json_decode($docTypes, true);
- } elseif (is_array($docTypes) === true) {
+ } else if (is_array($docTypes) === true) {
$docTypeIds = $docTypes;
}
@@ -647,11 +659,11 @@ private function enrichBesluittype(
}
// Expand caseTypes to zaaktypen URLs.
- $caseTypes = $objectData['caseTypes'] ?? '';
+ $caseTypes = $objectData['caseTypes'] ?? '';
$caseTypeIds = [];
if (is_string($caseTypes) === true && $caseTypes !== '') {
$caseTypeIds = json_decode($caseTypes, true);
- } elseif (is_array($caseTypes) === true) {
+ } else if (is_array($caseTypes) === true) {
$caseTypeIds = $caseTypes;
}
@@ -704,7 +716,11 @@ private function enrichZaaktype(
register: $ztMapping['sourceRegister'],
schema: $ztMapping['sourceSchema']
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$subCases = $objectData['subCaseTypes'] ?? [];
if (is_array($subCases) === true && empty($subCases) === false) {
@@ -721,7 +737,11 @@ private function enrichZaaktype(
register: $ztMapping['sourceRegister'],
schema: $ztMapping['sourceSchema']
);
- $refData = is_array($refObj) === true ? $refObj : $refObj->jsonSerialize();
+ if (is_array($refObj) === true) {
+ $refData = $refObj;
+ } else {
+ $refData = $refObj->jsonSerialize();
+ }
$ident = $refData['identifier'] ?? '';
@@ -733,7 +753,11 @@ private function enrichZaaktype(
);
$result = $objectService->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $match) {
- $mData = is_array($match) === true ? $match : $match->jsonSerialize();
+ if (is_array($match) === true) {
+ $mData = $match;
+ } else {
+ $mData = $match->jsonSerialize();
+ }
$mId = $mData['id'] ?? ($mData['@self']['id'] ?? '');
if ($mId !== '') {
@@ -807,7 +831,11 @@ private function enrichZaaktype(
register: $ztMapping['sourceRegister'],
schema: $ztMapping['sourceSchema']
);
- $refData = is_array($refObj) === true ? $refObj : $refObj->jsonSerialize();
+ if (is_array($refObj) === true) {
+ $refData = $refObj;
+ } else {
+ $refData = $refObj->jsonSerialize();
+ }
$ident = $refData['identifier'] ?? '';
@@ -819,7 +847,11 @@ private function enrichZaaktype(
);
$result = $objectService->searchObjectsPaginated(query: $query);
foreach (($result['results'] ?? []) as $match) {
- $mData = is_array($match) === true ? $match : $match->jsonSerialize();
+ if (is_array($match) === true) {
+ $mData = $match;
+ } else {
+ $mData = $match->jsonSerialize();
+ }
$mId = $mData['id'] ?? ($mData['@self']['id'] ?? '');
if ($mId !== '') {
@@ -865,7 +897,11 @@ private function enrichZaaktype(
$iotUrls = [];
foreach (($result['results'] ?? []) as $ziot) {
- $ziotData = is_array($ziot) === true ? $ziot : $ziot->jsonSerialize();
+ if (is_array($ziot) === true) {
+ $ziotData = $ziot;
+ } else {
+ $ziotData = $ziot->jsonSerialize();
+ }
$iotRef = $ziotData['informatieobjecttype'] ?? '';
if ($iotRef === '') {
@@ -879,7 +915,11 @@ private function enrichZaaktype(
register: $iotMapping['sourceRegister'],
schema: $iotMapping['sourceSchema']
);
- $iotData = is_array($iotObj) === true ? $iotObj : $iotObj->jsonSerialize();
+ if (is_array($iotObj) === true) {
+ $iotData = $iotObj;
+ } else {
+ $iotData = $iotObj->jsonSerialize();
+ }
$iotName = $iotData['name'] ?? '';
@@ -892,7 +932,11 @@ private function enrichZaaktype(
);
$iotResult = $objectService->searchObjectsPaginated(query: $iotQuery);
foreach (($iotResult['results'] ?? []) as $matchingIot) {
- $mData = is_array($matchingIot) === true ? $matchingIot : $matchingIot->jsonSerialize();
+ if (is_array($matchingIot) === true) {
+ $mData = $matchingIot;
+ } else {
+ $mData = $matchingIot->jsonSerialize();
+ }
$mId = $mData['id'] ?? ($mData['@self']['id'] ?? '');
if ($mId !== '') {
@@ -932,7 +976,11 @@ private function enrichZaaktype(
$btUrls = [];
foreach (($result['results'] ?? []) as $bt) {
- $btData = is_array($bt) === true ? $bt : $bt->jsonSerialize();
+ if (is_array($bt) === true) {
+ $btData = $bt;
+ } else {
+ $btData = $bt->jsonSerialize();
+ }
$btUuid = $btData['id'] ?? ($btData['@self']['id'] ?? '');
if ($btUuid !== '') {
@@ -972,7 +1020,11 @@ private function enrichZaaktype(
$urls = [];
foreach (($result['results'] ?? []) as $sub) {
- $subData = is_array($sub) === true ? $sub : $sub->jsonSerialize();
+ if (is_array($sub) === true) {
+ $subData = $sub;
+ } else {
+ $subData = $sub->jsonSerialize();
+ }
$subUuid = $subData['id'] ?? ($subData['@self']['id'] ?? '');
if ($subUuid !== '') {
@@ -1135,7 +1187,11 @@ private function isUrlValid(string $url, string $schemaKey, string $today): bool
schema: $mappingConfig['sourceSchema']
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
// Must be published (isDraft=false / concept=false).
$isDraft = $objectData['isDraft'] ?? ($objectData['concept'] ?? true);
@@ -1280,7 +1336,11 @@ private function resolveIotByOmschrijving(array $body): void
if (($result['total'] ?? 0) > 0) {
$iot = $result['results'][0];
- $iotData = is_array($iot) === true ? $iot : $iot->jsonSerialize();
+ if (is_array($iot) === true) {
+ $iotData = $iot;
+ } else {
+ $iotData = $iot->jsonSerialize();
+ }
$iotUuid = $iotData['id'] ?? ($iotData['@self']['id'] ?? '');
if ($iotUuid !== '') {
diff --git a/lib/Dashboard/CasesOverviewWidget.php b/lib/Dashboard/CasesOverviewWidget.php
index f2876128..f37621aa 100644
--- a/lib/Dashboard/CasesOverviewWidget.php
+++ b/lib/Dashboard/CasesOverviewWidget.php
@@ -110,7 +110,7 @@ public function getUrl(): ?string
* @inheritDoc
* @return void
*
- * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design
+ * @SuppressWarnings(PHPMD.StaticAccess)
*/
public function load(): void
{
diff --git a/lib/Dashboard/MyTasksWidget.php b/lib/Dashboard/MyTasksWidget.php
index cc345954..47f9d966 100644
--- a/lib/Dashboard/MyTasksWidget.php
+++ b/lib/Dashboard/MyTasksWidget.php
@@ -110,7 +110,7 @@ public function getUrl(): ?string
* @inheritDoc
* @return void
*
- * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design
+ * @SuppressWarnings(PHPMD.StaticAccess)
*/
public function load(): void
{
diff --git a/lib/Dashboard/OverdueCasesWidget.php b/lib/Dashboard/OverdueCasesWidget.php
index 737a353b..da50e152 100644
--- a/lib/Dashboard/OverdueCasesWidget.php
+++ b/lib/Dashboard/OverdueCasesWidget.php
@@ -110,7 +110,7 @@ public function getUrl(): ?string
* @inheritDoc
* @return void
*
- * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design
+ * @SuppressWarnings(PHPMD.StaticAccess)
*/
public function load(): void
{
diff --git a/lib/Middleware/ZgwAuthMiddleware.php b/lib/Middleware/ZgwAuthMiddleware.php
index c7686183..afce8fbd 100644
--- a/lib/Middleware/ZgwAuthMiddleware.php
+++ b/lib/Middleware/ZgwAuthMiddleware.php
@@ -144,7 +144,7 @@ private function loadOpenRegisterServices(): void
*
* @throws \OCA\Procest\Middleware\ZgwAuthException If authorization fails.
*
- * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $methodName required by Middleware interface
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function beforeController($controller, $methodName): void
{
@@ -213,7 +213,7 @@ public function beforeController($controller, $methodName): void
*
* @return JSONResponse|null
*
- * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $controller/$methodName required by Middleware interface
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterException($controller, $methodName, \Exception $exception): ?JSONResponse
{
diff --git a/lib/Service/NotificatieService.php b/lib/Service/NotificatieService.php
index ac866fec..f3d0c599 100644
--- a/lib/Service/NotificatieService.php
+++ b/lib/Service/NotificatieService.php
@@ -155,7 +155,11 @@ private function deliver(array $notification): void
$client = new Client(['timeout' => 10]);
foreach ($subscriptions as $subscription) {
- $subData = is_array($subscription) === true ? $subscription : $subscription->jsonSerialize();
+ if (is_array($subscription) === true) {
+ $subData = $subscription;
+ } else {
+ $subData = $subscription->jsonSerialize();
+ }
$this->deliverToSubscription(
client: $client,
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
index f3cae4e7..098eeb82 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -134,7 +134,7 @@ public function isOpenRegisterAvailable(): bool
*
* @return array Import result
*
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag) — $force is a simple re-import toggle
+ * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
*/
public function loadConfiguration(bool $force=false): array
{
diff --git a/lib/Service/ZgwBrcRulesService.php b/lib/Service/ZgwBrcRulesService.php
index 51816245..3c63c34e 100644
--- a/lib/Service/ZgwBrcRulesService.php
+++ b/lib/Service/ZgwBrcRulesService.php
@@ -85,7 +85,7 @@ class ZgwBrcRulesService extends ZgwRulesBase
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/besluiten/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW business rules validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function rulesBesluitenCreate(array $body): array
{
@@ -530,7 +530,7 @@ private function validateZaakBesluittypeRelation(string $zaakUrl, string $beslui
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) — ZGW cross-register validation
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
private function validateBioInformatieobjecttype(string $besluitUrl, string $ioUrl): ?array
{
diff --git a/lib/Service/ZgwBusinessRulesService.php b/lib/Service/ZgwBusinessRulesService.php
index cd271517..81d774a4 100644
--- a/lib/Service/ZgwBusinessRulesService.php
+++ b/lib/Service/ZgwBusinessRulesService.php
@@ -76,7 +76,7 @@ public function __construct(
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag) — ZGW scope flag from middleware
+ * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
*/
public function validate(
string $zgwApi,
@@ -271,7 +271,7 @@ private function dispatchZrc(string $resource, string $action, array $body, ?arr
*
* @psalm-suppress UnusedParam — $existingObject reserved for update validation rules
*
- * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $existingObject reserved for update rules
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
private function dispatchZtc(string $resource, string $action, array $body, ?array $existingObject): array
{
diff --git a/lib/Service/ZgwDrcRulesService.php b/lib/Service/ZgwDrcRulesService.php
index 98e6f43d..b67dfc3c 100644
--- a/lib/Service/ZgwDrcRulesService.php
+++ b/lib/Service/ZgwDrcRulesService.php
@@ -82,8 +82,8 @@ class ZgwDrcRulesService extends ZgwRulesBase
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/documenten/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW business rules validation
- * @SuppressWarnings(PHPMD.NPathComplexity) — ZGW business rules validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function rulesEnkelvoudiginformatieobjectenCreate(array $body): array
{
@@ -258,8 +258,8 @@ public function rulesEnkelvoudiginformatieobjectenDestroy(
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/documenten/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW business rules validation
- * @SuppressWarnings(PHPMD.NPathComplexity) — ZGW business rules validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function rulesObjectinformatieobjectenCreate(array $body): array
{
@@ -350,7 +350,11 @@ private function findOioRelationsForDocument(
$ids = [];
foreach (($result['results'] ?? []) as $obj) {
- $data = is_array($obj) === true ? $obj : $obj->jsonSerialize();
+ if (is_array($obj) === true) {
+ $data = $obj;
+ } else {
+ $data = $obj->jsonSerialize();
+ }
$id = $data['id'] ?? ($data['@self']['id'] ?? null);
if ($id !== null) {
@@ -376,7 +380,7 @@ private function findOioRelationsForDocument(
*
* @psalm-suppress UnusedParam — $body reserved for future gebruiksrechten lookup
*
- * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $body reserved for future gebruiksrechten lookup
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
private function validateIndicatieGebruiksrechtTrue(array $body): array
{
@@ -472,7 +476,7 @@ private function validateObjectUrl(string $objectUrl, string $objectType): ?arra
*
* @return array|null Validation error if relation doesn't exist, null if valid
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW cross-register validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function validateOioCrossRegister(string $ioUrl, string $objectUrl, string $objectType): ?array
{
@@ -517,9 +521,11 @@ private function validateOioCrossRegister(string $ioUrl, string $objectUrl, stri
$total = $result['total'] ?? count($result['results'] ?? []);
if ($total === 0) {
- $detail = ($objectType === 'zaak')
- ? 'Er bestaat geen ZaakInformatieObject in de Zaken API voor deze combinatie.'
- : 'Er bestaat geen BesluitInformatieObject in de Besluiten API voor deze combinatie.';
+ if ($objectType === 'zaak') {
+ $detail = 'Er bestaat geen ZaakInformatieObject in de Zaken API voor deze combinatie.';
+ } else {
+ $detail = 'Er bestaat geen BesluitInformatieObject in de Besluiten API voor deze combinatie.';
+ }
return $this->error(
status: 400,
diff --git a/lib/Service/ZgwRulesBase.php b/lib/Service/ZgwRulesBase.php
index 66ba8220..a8d5cea7 100644
--- a/lib/Service/ZgwRulesBase.php
+++ b/lib/Service/ZgwRulesBase.php
@@ -278,7 +278,11 @@ protected function validateTypeUrl(string $typeUrl, string $fieldName, string $s
);
}
- $typeData = is_array($typeObject) === true ? $typeObject : $typeObject->jsonSerialize();
+ if (is_array($typeObject) === true) {
+ $typeData = $typeObject;
+ } else {
+ $typeData = $typeObject->jsonSerialize();
+ }
$isDraft = $typeData['isDraft'] ?? true;
if ($isDraft === true) {
@@ -485,8 +489,12 @@ protected function findObjectByField(
return null;
}
- $obj = $results[0];
- $data = is_array($obj) === true ? $obj : $obj->jsonSerialize();
+ $obj = $results[0];
+ if (is_array($obj) === true) {
+ $data = $obj;
+ } else {
+ $data = $obj->jsonSerialize();
+ }
return $data['id'] ?? ($data['@self']['id'] ?? null);
} catch (\Throwable $e) {
@@ -524,7 +532,11 @@ protected function findAllObjectsByField(
$ids = [];
foreach (($result['results'] ?? []) as $obj) {
- $data = is_array($obj) === true ? $obj : $obj->jsonSerialize();
+ if (is_array($obj) === true) {
+ $data = $obj;
+ } else {
+ $data = $obj->jsonSerialize();
+ }
$id = $data['id'] ?? ($data['@self']['id'] ?? null);
if ($id !== null) {
@@ -637,7 +649,11 @@ protected function checkFieldUniqueness(
// count it as a match (conservative: assume coercion happened).
$matchCount = 0;
foreach (($result['results'] ?? []) as $obj) {
- $data = is_array($obj) === true ? $obj : $obj->jsonSerialize();
+ if (is_array($obj) === true) {
+ $data = $obj;
+ } else {
+ $data = $obj->jsonSerialize();
+ }
$storedVal = $data[$field2Search] ?? null;
$storedStr = (string) $storedVal;
diff --git a/lib/Service/ZgwService.php b/lib/Service/ZgwService.php
index 5e8a20d1..07015fa9 100644
--- a/lib/Service/ZgwService.php
+++ b/lib/Service/ZgwService.php
@@ -313,7 +313,12 @@ public function translateQueryParams(array $params, array $mappingConfig): array
$value = end($parts);
}
- $filterKey = ($operator !== null) ? $field.'.'.$operator : $field;
+ if ($operator !== null) {
+ $filterKey = $field.'.'.$operator;
+ } else {
+ $filterKey = $field;
+ }
+
$filters[$filterKey] = $value;
}
}//end foreach
@@ -616,8 +621,8 @@ public function validateJwtAuth(IRequest $request): ?JSONResponse
*
* @return bool True if the consumer has the scope or heeftAlleAutorisaties
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — multiple JWT validation paths
- * @SuppressWarnings(PHPMD.NPathComplexity) — multiple JWT validation paths
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function consumerHasScope(IRequest $request, string $component, string $scope): bool
{
@@ -687,7 +692,7 @@ public function consumerHasScope(IRequest $request, string $component, string $s
*
* @return array|null Array of autorisatie entries, or null if unrestricted
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — multiple JWT validation paths
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function getConsumerAuthorisaties(IRequest $request, string $component): ?array
{
@@ -880,7 +885,11 @@ public function handleIndex(IRequest $request, string $zgwApi, string $resource)
$outboundMapping = $this->createOutboundMapping(mappingConfig: $mappingConfig);
$mapped = [];
foreach ($objects as $object) {
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$mapped[] = $this->applyOutboundMapping(
objectData: $objectData,
@@ -924,8 +933,8 @@ public function handleIndex(IRequest $request, string $zgwApi, string $resource)
*
* @return JSONResponse
*
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag) — ZGW scope flags from middleware
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength) — orchestration method with validation + mapping
+ * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function handleCreate(
IRequest $request,
@@ -979,7 +988,7 @@ public function handleCreate(
mappingConfig: $mappingConfig
);
- /** @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change */
+ // @phpstan-ignore-next-line — defensive guard: applyInboundMapping may change
if (is_array($englishData) === false) {
return new JSONResponse(
data: ['detail' => 'Invalid mapping result'],
@@ -998,7 +1007,11 @@ public function handleCreate(
object: $englishData
);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$objectUuid = $objectData['id'] ?? ($objectData['@self']['id'] ?? '');
@@ -1067,7 +1080,11 @@ public function handleShow(
$baseUrl = $this->buildBaseUrl(request: $request, zgwApi: $zgwApi, resource: $resource);
$outboundMapping = $this->createOutboundMapping(mappingConfig: $mappingConfig);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$mapped = $this->applyOutboundMapping(
objectData: $objectData,
@@ -1106,7 +1123,7 @@ public function handleShow(
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag) — ZGW scope flags from middleware
+ * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
*/
public function handleUpdate(
IRequest $request,
@@ -1133,14 +1150,22 @@ public function handleUpdate(
try {
$body = $this->getRequestBody(request: $request);
- $action = ($partial === true) ? 'patch' : 'update';
+ if ($partial === true) {
+ $action = 'patch';
+ } else {
+ $action = 'update';
+ }
$existingObj = $this->objectService->find(
$uuid,
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existingObj) === true ? $existingObj : $existingObj->jsonSerialize();
+ if (is_array($existingObj) === true) {
+ $existingData = $existingObj;
+ } else {
+ $existingData = $existingObj->jsonSerialize();
+ }
$ruleResult = $this->businessRulesService->validate(
zgwApi: $zgwApi,
@@ -1270,7 +1295,11 @@ public function handleUpdate(
$baseUrl = $this->buildBaseUrl(request: $request, zgwApi: $zgwApi, resource: $resource);
$outboundMapping = $this->createOutboundMapping(mappingConfig: $mappingConfig);
- $objectData = is_array($object) === true ? $object : $object->jsonSerialize();
+ if (is_array($object) === true) {
+ $objectData = $object;
+ } else {
+ $objectData = $object->jsonSerialize();
+ }
$mapped = $this->applyOutboundMapping(
objectData: $objectData,
@@ -1312,7 +1341,7 @@ public function handleUpdate(
*
* @return JSONResponse
*
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag) — ZGW scope flags from middleware
+ * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
*/
public function handleDestroy(
IRequest $request,
@@ -1338,7 +1367,11 @@ public function handleDestroy(
register: $mappingConfig['sourceRegister'],
schema: $mappingConfig['sourceSchema']
);
- $existingData = is_array($existingObj) === true ? $existingObj : $existingObj->jsonSerialize();
+ if (is_array($existingObj) === true) {
+ $existingData = $existingObj;
+ } else {
+ $existingData = $existingObj->jsonSerialize();
+ }
$ruleResult = $this->businessRulesService->validate(
zgwApi: $zgwApi,
@@ -1473,7 +1506,11 @@ public function handleAudittrailShow(
try {
$logs = $this->objectService->getLogs($uuid, [], false, false);
foreach ($logs as $log) {
- $logData = is_array($log) === true ? $log : $log->jsonSerialize();
+ if (is_array($log) === true) {
+ $logData = $log;
+ } else {
+ $logData = $log->jsonSerialize();
+ }
if (($logData['uuid'] ?? '') === $auditUuid) {
return new JSONResponse(
@@ -1521,7 +1558,11 @@ private function mapAuditTrailToZgw(
string $resourceUrl,
string $resource
): array {
- $logData = is_array($log) === true ? $log : $log->jsonSerialize();
+ if (is_array($log) === true) {
+ $logData = $log;
+ } else {
+ $logData = $log->jsonSerialize();
+ }
// Map OpenRegister action names to ZGW actie names.
$actionMap = [
@@ -1574,8 +1615,8 @@ private function mapAuditTrailToZgw(
*
* @return bool|null True if closed, false if open, null if N/A
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — sub-resource lookup with multiple guard clauses
- * @SuppressWarnings(PHPMD.NPathComplexity) — sub-resource lookup with multiple guard clauses
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function resolveZaakClosed(string $resource, array $existingData): ?bool
{
@@ -1626,7 +1667,11 @@ public function resolveZaakClosed(string $resource, array $existingData): ?bool
return null;
}
- $zaakData = is_array($zaak) === true ? $zaak : $zaak->jsonSerialize();
+ if (is_array($zaak) === true) {
+ $zaakData = $zaak;
+ } else {
+ $zaakData = $zaak->jsonSerialize();
+ }
$endDate = $zaakData['endDate'] ?? ($zaakData['einddatum'] ?? null);
@@ -1647,8 +1692,8 @@ public function resolveZaakClosed(string $resource, array $existingData): ?bool
*
* @return bool|null True if closed, false if open, null if N/A
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — sub-resource lookup with multiple guard clauses
- * @SuppressWarnings(PHPMD.NPathComplexity) — sub-resource lookup with multiple guard clauses
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function resolveZaakClosedFromBody(string $resource, array $body): ?bool
{
@@ -1700,7 +1745,11 @@ public function resolveZaakClosedFromBody(string $resource, array $body): ?bool
return null;
}
- $zaakData = is_array($zaak) === true ? $zaak : $zaak->jsonSerialize();
+ if (is_array($zaak) === true) {
+ $zaakData = $zaak;
+ } else {
+ $zaakData = $zaak->jsonSerialize();
+ }
$endDate = $zaakData['endDate'] ?? ($zaakData['einddatum'] ?? null);
@@ -1718,8 +1767,8 @@ public function resolveZaakClosedFromBody(string $resource, array $body): ?bool
*
* @return bool|null True if draft, false if published, null if N/A
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — sub-resource lookup with multiple guard clauses
- * @SuppressWarnings(PHPMD.NPathComplexity) — sub-resource lookup with multiple guard clauses
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function resolveParentZaaktypeDraft(string $resource, array $existingData): ?bool
{
@@ -1764,7 +1813,11 @@ public function resolveParentZaaktypeDraft(string $resource, array $existingData
return null;
}
- $ztData = is_array($zaaktype) === true ? $zaaktype : $zaaktype->jsonSerialize();
+ if (is_array($zaaktype) === true) {
+ $ztData = $zaaktype;
+ } else {
+ $ztData = $zaaktype->jsonSerialize();
+ }
$isDraft = $ztData['isDraft'] ?? ($ztData['concept'] ?? true);
@@ -1792,8 +1845,8 @@ public function resolveParentZaaktypeDraft(string $resource, array $existingData
*
* @return bool|null True if draft, false if published, null if N/A
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — sub-resource lookup with multiple guard clauses
- * @SuppressWarnings(PHPMD.NPathComplexity) — sub-resource lookup with multiple guard clauses
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function resolveParentZaaktypeDraftFromBody(string $resource, array $body): ?bool
{
@@ -1841,7 +1894,11 @@ public function resolveParentZaaktypeDraftFromBody(string $resource, array $body
return null;
}
- $ztData = is_array($zaaktype) === true ? $zaaktype : $zaaktype->jsonSerialize();
+ if (is_array($zaaktype) === true) {
+ $ztData = $zaaktype;
+ } else {
+ $ztData = $zaaktype->jsonSerialize();
+ }
$isDraft = $ztData['isDraft'] ?? ($ztData['concept'] ?? true);
diff --git a/lib/Service/ZgwZrcRulesService.php b/lib/Service/ZgwZrcRulesService.php
index a42225db..5eeababd 100644
--- a/lib/Service/ZgwZrcRulesService.php
+++ b/lib/Service/ZgwZrcRulesService.php
@@ -80,7 +80,7 @@ class ZgwZrcRulesService extends ZgwRulesBase
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/zaken/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW business rules validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function rulesZakenCreate(array $body): array
{
@@ -472,8 +472,8 @@ private function deriveVertrouwelijkheidaanduiding(array $body, string $zaaktype
*
* @psalm-suppress UnusedParam — $zaaktypeField reserved for future filtering
*
- * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $zaaktypeField reserved for future filtering
- * @SuppressWarnings(PHPMD.NPathComplexity) — cross-register validation with multiple lookups
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
private function validateSubResourceType(
string $zaakUrl,
@@ -552,8 +552,8 @@ private function validateSubResourceType(
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/zaken/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW cross-register validation
- * @SuppressWarnings(PHPMD.NPathComplexity) — ZGW cross-register validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
private function validateZioInformatieobjecttype(string $zaakUrl, string $ioUrl): ?array
{
@@ -659,7 +659,7 @@ private function validateZioInformatieobjecttype(string $zaakUrl, string $ioUrl)
*
* @psalm-suppress UnusedParam — $isPatch reserved for partial-update field validation
*
- * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $isPatch reserved for partial-update validation
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
private function validateZaakFields(array $result, ?array $existingObject, bool $isPatch): array
{
@@ -698,7 +698,11 @@ private function validateZaakFields(array $result, ?array $existingObject, bool
$segments = array_filter(explode('/', trim($path, '/')));
$last = end($segments);
$looksLikeUuid = preg_match('/[0-9a-f]{4,}-/i', $last) === 1;
- $code = ($looksLikeUuid === true) ? 'bad-url' : 'invalid-resource';
+ if ($looksLikeUuid === true) {
+ $code = 'bad-url';
+ } else {
+ $code = 'invalid-resource';
+ }
return $this->error(
status: 400,
@@ -972,8 +976,8 @@ private function validateHoofdzaakNesting(string $hoofdzaakUrl): ?array
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/zaken/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW business rules validation
- * @SuppressWarnings(PHPMD.NPathComplexity) — ZGW business rules validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
private function validateProductenOfDiensten(array $body): ?array
{
@@ -1058,7 +1062,7 @@ private function validateProductenOfDiensten(array $body): ?array
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/zaken/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — immutability check on multiple fields
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function checkZioImmutability(array $result, ?array $existingObject): array
{
@@ -1072,9 +1076,11 @@ private function checkZioImmutability(array $result, ?array $existingObject): ar
if (isset($body['zaak']) === true) {
$existingZaak = $existingObject['case'] ?? ($existingObject['zaak'] ?? '');
$newZaakUuid = $this->extractUuid(url: $body['zaak']);
- $existZaakId = is_string($existingZaak) === true
- ? $this->extractUuid(url: $existingZaak)
- : $existingZaak;
+ if (is_string($existingZaak) === true) {
+ $existZaakId = $this->extractUuid(url: $existingZaak);
+ } else {
+ $existZaakId = $existingZaak;
+ }
if ($existZaakId !== null && $newZaakUuid !== null && $newZaakUuid !== $existZaakId) {
return $this->fieldImmutableError(fieldName: 'zaak');
@@ -1085,9 +1091,11 @@ private function checkZioImmutability(array $result, ?array $existingObject): ar
if (isset($body['informatieobject']) === true) {
$existingIo = $existingObject['document'] ?? ($existingObject['informatieobject'] ?? '');
$newIoUuid = $this->extractUuid(url: $body['informatieobject']);
- $existIoId = is_string($existingIo) === true
- ? $this->extractUuid(url: $existingIo)
- : $existingIo;
+ if (is_string($existingIo) === true) {
+ $existIoId = $this->extractUuid(url: $existingIo);
+ } else {
+ $existIoId = $existingIo;
+ }
if ($existIoId !== null && $newIoUuid !== null && $newIoUuid !== $existIoId) {
return $this->fieldImmutableError(fieldName: 'informatieobject');
diff --git a/lib/Service/ZgwZtcRulesService.php b/lib/Service/ZgwZtcRulesService.php
index 2e8c7ead..4a4c0a8a 100644
--- a/lib/Service/ZgwZtcRulesService.php
+++ b/lib/Service/ZgwZtcRulesService.php
@@ -247,8 +247,8 @@ public function preserveConcept(array $body, string $resource, ?array $existingO
*
* @link https://vng-realisatie.github.io/gemma-zaken/standaard/catalogi/
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW business rules validation
- * @SuppressWarnings(PHPMD.NPathComplexity) — ZGW business rules validation
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function rulesZaaktypenCreate(array $body): array
{
@@ -368,7 +368,7 @@ public function rulesBesluittypenCreate(array $body): array
*
* @return array The validation result
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — ZGW resolution of omschrijving/UUID/URL
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function rulesZaaktypeinformatieobjecttypenCreate(array $body): array
{
@@ -388,9 +388,9 @@ public function rulesZaaktypeinformatieobjecttypenCreate(array $body): array
$needsNameLookup = false;
if ($isUrl === true) {
// URL — let reverse mapping handle UUID extraction.
- } elseif ($uuid !== null) {
+ } else if ($uuid !== null) {
// Bare UUID — verify it exists; if not, treat as omschrijving.
- $existing = $this->findBySchemaKey(uuid: $uuid, schemaKey: 'document_type_schema');
+ $existing = $this->findBySchemaKey(uuid: $uuid, schemaKey: 'document_type_schema');
$needsNameLookup = ($existing === null);
}
@@ -625,7 +625,11 @@ private function validateBrondatumArchief(array $archief, ?array $selectielijstD
// Ztc-008: procestermijn required only for termijn.
$procestermijn = $archief['procestermijn'] ?? null;
- $ptValue = is_string($procestermijn) === true ? $procestermijn : '';
+ if (is_string($procestermijn) === true) {
+ $ptValue = $procestermijn;
+ } else {
+ $ptValue = '';
+ }
$errors = array_merge(
$errors,
@@ -907,8 +911,8 @@ private function resolveTypeReferences(
*
* @return array The body with resolved zaaktype references
*
- * @SuppressWarnings(PHPMD.CyclomaticComplexity) — nested object resolution
- * @SuppressWarnings(PHPMD.NPathComplexity) — nested object resolution
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
private function resolveGerelateerdeZaaktypen(array $body): array
{
diff --git a/openspec/README.md b/openspec/README.md
new file mode 100644
index 00000000..3828089c
--- /dev/null
+++ b/openspec/README.md
@@ -0,0 +1,78 @@
+# OpenSpec Setup for Procest
+
+This project uses [OpenSpec](https://github.com/Fission-AI/OpenSpec) for change management (proposal → specs → design → tasks → review). The workflow schema is **shared** across all Nextcloud apps in the `apps-extra` folder.
+
+## How the shared schema works
+
+The OpenSpec schema files live in **one place**:
+
+```
+apps-extra/openspec/schemas/conduction/
+├── schema.yaml
+└── templates/
+ ├── proposal.md, spec.md, design.md, review.md, tasks.md
+```
+
+Procest does **not** have its own copy. Instead, `procest/openspec/schemas` is a **junction** (Windows directory link) that points to the shared folder. Both paths access the **same files**:
+
+- `apps-extra/openspec/schemas/conduction/schema.yaml`
+- `procest/openspec/schemas/conduction/schema.yaml` ← same file via junction
+
+Editing either path updates the same file. Changes apply to all apps in `apps-extra` that use this schema.
+
+## After cloning: one-time setup
+
+The `openspec/schemas` folder is in `.gitignore` (it's a link to shared files), so it is not committed. After cloning, you must create the junction.
+
+### Prerequisites
+
+- Procest must be inside the `nextcloud-docker-dev` workspace structure:
+ ```
+ nextcloud-docker-dev/
+ └── workspace/server/apps-extra/
+ ├── openspec/schemas/ ← shared schema (source of truth)
+ └── procest/ ← this app
+ ```
+
+### Windows (PowerShell)
+
+From the procest root:
+
+```powershell
+.\openspec\setup-schemas.ps1
+```
+
+Or manually:
+
+```powershell
+cd openspec
+cmd /c mklink /J schemas "..\..\openspec\schemas"
+```
+
+### Linux / macOS
+
+```bash
+cd openspec
+ln -s ../../openspec/schemas schemas
+```
+
+### Verify
+
+After setup, both paths should show the same content:
+
+```powershell
+# Should show identical content
+Get-Content openspec\schemas\conduction\schema.yaml | Select-Object -First 3
+Get-Content ..\..\openspec\schemas\conduction\schema.yaml | Select-Object -First 3
+```
+
+## Using OpenSpec
+
+With the schema linked, use the slash commands in your AI assistant:
+
+- `/opsx:propose "add feature X"` — create a new change
+- `/opsx:apply` — implement tasks
+- `/opsx:verify` — run verification
+- `/opsx:archive` — archive completed change
+
+See [OpenSpec docs](https://github.com/Fission-AI/OpenSpec) for the full workflow.
diff --git a/openspec/ROADMAP.md b/openspec/ROADMAP.md
index 3778c1d0..6557935b 100644
--- a/openspec/ROADMAP.md
+++ b/openspec/ROADMAP.md
@@ -1,55 +1,55 @@
-# Procest Roadmap
-
-## Implemented (MVP Complete)
-
-All MVP specs have been implemented and archived.
-
-| Spec | Archived | Summary |
-|------|----------|---------|
-| openregister-integration | 2026-02-26 | Register config, repair step, 12 schemas, Pinia store |
-| case-types | 2026-02-26 | Case type system with status types, deadlines, extensions |
-| case-management | 2026-02-26 | Full case CRUD, status lifecycle, timeline, activity |
-| task-management | 2026-02-26 | CMMN task lifecycle, assignment, priority, due dates |
-| dashboard | 2026-02-26 | KPI cards, overdue alerts, workload preview, activity feed |
-| my-work | 2026-02-26 | Personal workload view with grouped urgency, filters |
-| roles-decisions (MVP) | 2026-02-26 | Role assignment, handler reassign, result recording |
-| admin-settings | — | Already implemented (AdminSettings.php, Settings.vue, CaseTypeAdmin.vue, status types) |
-
-## V1 Features (Roadmap)
-
-### roles-decisions V1
-
-The MVP implemented roles (assignment, handler shortcut, display, validation) and results (type selector, object creation). V1 adds the remaining 8 requirements:
-
-| Requirement | Description | Complexity |
-|-------------|-------------|------------|
-| REQ-ROLE-002 | Role type enforcement from case type (filter available role types by case type) | Medium |
-| REQ-ROLE-004 | Role-based case access / RBAC (restrict case visibility by role) | High |
-| REQ-RESULT-002 | Result type admin configuration (CRUD in admin settings Results tab) | Medium |
-| REQ-DECISION-001 | Decision CRUD (create, read, update, delete formal decisions on cases) | High |
-| REQ-DECISION-002 | Decision validity periods (start/end dates, legal effect tracking) | Medium |
-| REQ-DECISION-003 | Decision types from case type (admin config for allowed decision types) | Medium |
-| REQ-DECISION-004 | Decision validation (required fields, type enforcement) | Low |
-| REQ-DECISION-005 | Decisions section on case detail (display decisions with timeline) | Medium |
-
-### admin-settings V1
-
-The admin settings MVP is implemented (panel registration, case type CRUD, general tab, status types with reorder, publish, default). V1 adds type management tabs:
-
-| Requirement | Description | Complexity |
-|-------------|-------------|------------|
-| REQ-ADMIN-009 | Result type management tab (CRUD with archival rules, retention periods) | Medium |
-| REQ-ADMIN-010 | Role type management tab (CRUD with generic role mapping) | Medium |
-| REQ-ADMIN-011 | Property definition management tab (custom fields per case type) | Medium |
-| REQ-ADMIN-012 | Document type management tab (document requirements per case type) | Medium |
-
-### Potential Future Features
-
-These are not yet specified but may be needed:
-
-- **Document management**: Upload/attach documents to cases, enforce document type requirements
-- **Notifications**: Notify participants on status changes, task assignments, deadlines
-- **Bulk operations**: Bulk status change, bulk assignment across cases
-- **Reporting/export**: Case statistics, processing time reports, CSV/PDF export
-- **External contacts**: Support non-Nextcloud participants (citizens, organizations)
-- **Case templates**: Pre-fill case data from templates
+# Procest Roadmap
+
+## Implemented (MVP Complete)
+
+All MVP specs have been implemented and archived.
+
+| Spec | Archived | Summary |
+|------|----------|---------|
+| openregister-integration | 2026-02-26 | Register config, repair step, 12 schemas, Pinia store |
+| case-types | 2026-02-26 | Case type system with status types, deadlines, extensions |
+| case-management | 2026-02-26 | Full case CRUD, status lifecycle, timeline, activity |
+| task-management | 2026-02-26 | CMMN task lifecycle, assignment, priority, due dates |
+| dashboard | 2026-02-26 | KPI cards, overdue alerts, workload preview, activity feed |
+| my-work | 2026-02-26 | Personal workload view with grouped urgency, filters |
+| roles-decisions (MVP) | 2026-02-26 | Role assignment, handler reassign, result recording |
+| admin-settings | — | Already implemented (AdminSettings.php, Settings.vue, CaseTypeAdmin.vue, status types) |
+
+## V1 Features (Roadmap)
+
+### roles-decisions V1
+
+The MVP implemented roles (assignment, handler shortcut, display, validation) and results (type selector, object creation). V1 adds the remaining 8 requirements:
+
+| Requirement | Description | Complexity |
+|-------------|-------------|------------|
+| REQ-ROLE-002 | Role type enforcement from case type (filter available role types by case type) | Medium |
+| REQ-ROLE-004 | Role-based case access / RBAC (restrict case visibility by role) | High |
+| REQ-RESULT-002 | Result type admin configuration (CRUD in admin settings Results tab) | Medium |
+| REQ-DECISION-001 | Decision CRUD (create, read, update, delete formal decisions on cases) | High |
+| REQ-DECISION-002 | Decision validity periods (start/end dates, legal effect tracking) | Medium |
+| REQ-DECISION-003 | Decision types from case type (admin config for allowed decision types) | Medium |
+| REQ-DECISION-004 | Decision validation (required fields, type enforcement) | Low |
+| REQ-DECISION-005 | Decisions section on case detail (display decisions with timeline) | Medium |
+
+### admin-settings V1
+
+The admin settings MVP is implemented (panel registration, case type CRUD, general tab, status types with reorder, publish, default). V1 adds type management tabs:
+
+| Requirement | Description | Complexity |
+|-------------|-------------|------------|
+| REQ-ADMIN-009 | Result type management tab (CRUD with archival rules, retention periods) | Medium |
+| REQ-ADMIN-010 | Role type management tab (CRUD with generic role mapping) | Medium |
+| REQ-ADMIN-011 | Property definition management tab (custom fields per case type) | Medium |
+| REQ-ADMIN-012 | Document type management tab (document requirements per case type) | Medium |
+
+### Potential Future Features
+
+These are not yet specified but may be needed:
+
+- **Document management**: Upload/attach documents to cases, enforce document type requirements
+- **Notifications**: Notify participants on status changes, task assignments, deadlines
+- **Bulk operations**: Bulk status change, bulk assignment across cases
+- **Reporting/export**: Case statistics, processing time reports, CSV/PDF export
+- **External contacts**: Support non-Nextcloud participants (citizens, organizations)
+- **Case templates**: Pre-fill case data from templates
diff --git a/openspec/changes/archive/2026-02-26-case-management/design.md b/openspec/changes/archive/2026-02-26-case-management/design.md
index fcc83f84..c28ce39b 100644
--- a/openspec/changes/archive/2026-02-26-case-management/design.md
+++ b/openspec/changes/archive/2026-02-26-case-management/design.md
@@ -1,156 +1,156 @@
-# Design: Case Management — MVP
-
-## Architecture Overview
-
-This change enhances the existing CaseList.vue and CaseDetail.vue with case-type-aware behavior, and adds new sub-components for status timeline, deadline panel, and activity timeline. All data flows through the existing `useObjectStore` Pinia store and OpenRegister API.
-
-## File Map
-
-### New Files
-
-| File | Purpose |
-|------|---------|
-| `src/utils/caseHelpers.js` | Deadline calculation, countdown text, identifier generation, overdue/due-today logic for cases |
-| `src/utils/caseValidation.js` | Case form validation: required fields, case type validity checks |
-| `src/views/cases/CaseCreateDialog.vue` | Modal dialog for creating a new case with case type selection |
-| `src/views/cases/components/StatusTimeline.vue` | Horizontal status timeline visualization |
-| `src/views/cases/components/DeadlinePanel.vue` | Deadline info panel with countdown and extension button |
-| `src/views/cases/components/ActivityTimeline.vue` | Chronological activity log with "Add note" input |
-| `src/views/cases/components/QuickStatusDropdown.vue` | Inline status dropdown for the case list |
-
-### Modified Files
-
-| File | Changes |
-|------|---------|
-| `src/views/cases/CaseList.vue` | Replace hardcoded statuses with case-type-aware filters; add columns (identifier, type, deadline countdown); add quick status change; add overdue filter; add case type filter; add "New case" opens CaseCreateDialog |
-| `src/views/cases/CaseDetail.vue` | Integrate case type data; replace hardcoded status/priority dropdowns with case-type-aware ones; add StatusTimeline, DeadlinePanel, ActivityTimeline components; add result input on final status; add read-only mode for closed cases; enhance info panel |
-| `src/App.vue` | No routing changes needed — existing `#/cases` and `#/cases/{id}` routes are sufficient |
-| `src/store/store.js` | No changes — `case`, `caseType`, `statusType` object types already registered |
-
-### Unchanged Files
-
-| File | Reason |
-|------|--------|
-| `lib/Service/SettingsService.php` | All needed config keys already exist |
-| `src/store/modules/object.js` | Generic CRUD store already supports all needed operations |
-| `src/navigation/MainMenu.vue` | Cases nav item already exists |
-| `src/utils/taskLifecycle.js` | Task lifecycle unchanged |
-| `src/utils/taskHelpers.js` | May import some helpers (isOverdue pattern) but file not modified |
-
-## Design Decisions
-
-### DD-01: Case Type Data Loading Strategy
-
-**Decision**: Load case types and their status types eagerly on CaseDetail mount; cache in object store.
-
-**Rationale**: Case detail needs case type data for status timeline, deadline panel, status dropdown, and validation. Loading on mount avoids repeated fetches. The object store's `getObject('caseType', id)` provides caching.
-
-**Implementation**: In CaseDetail.mounted(), after fetching the case, fetch its case type. Then fetch status types filtered by `_filters[caseType]={caseTypeId}` and sort by `_order[order]=asc`.
-
-### DD-02: Status History Tracking
-
-**Decision**: Store status history as an array property on the case object: `statusHistory: [{ status, date, changedBy }]`.
-
-**Rationale**: MVP avoids backend complexity. OpenRegister stores JSON objects, so an embedded array is natural. The status timeline component reads this array to show dates under passed statuses.
-
-**Trade-off**: Status history is denormalized. Acceptable for MVP; V1 can migrate to separate status change entities if needed.
-
-### DD-03: Activity Log Storage
-
-**Decision**: Store activity as an array property on the case object: `activity: [{ date, type, description, user }]`.
-
-**Rationale**: Avoids Nextcloud Activity system integration (V1 scope). Frontend appends entries on create/update/status-change/extension. Activity array grows with the case but is bounded in practice (tens of entries, not thousands).
-
-**Trade-off**: No server-side activity filtering or cross-case activity feeds. Acceptable for MVP.
-
-### DD-04: Identifier Generation
-
-**Decision**: Generate identifiers client-side as `YYYY-{timestamp_suffix}` where suffix is `Date.now() % 10000`.
-
-**Rationale**: True sequential numbering (YYYY-NNN) requires backend coordination (counter in app config). For MVP, a timestamp-based approach avoids collisions while being simple. Format: `2026-4281`.
-
-**Trade-off**: Not strictly sequential. Acceptable for MVP; V1 can add a backend endpoint for sequential IDs.
-
-### DD-05: Deadline Calculation
-
-**Decision**: Calculate deadline client-side: `startDate + parseDuration(caseType.processingDeadline)`.
-
-**Rationale**: Duration parsing already exists in `durationHelpers.js`. The `parseDuration()` function returns `{ years, months, weeks, days }`. Apply these to the start date using JavaScript Date arithmetic.
-
-**Implementation**: New function `calculateDeadline(startDate, durationString)` in `caseHelpers.js`. Handles P{n}D (add days), P{n}W (add weeks*7 days), P{n}M (add months), P{n}Y (add years).
-
-### DD-06: Case Create Flow
-
-**Decision**: Use a modal dialog (CaseCreateDialog) instead of navigating to `#/cases/new`.
-
-**Rationale**: The create form needs case type selection with live preview of defaults (deadline, confidentiality, initial status). A dialog keeps the user in context (case list or dashboard). After creation, navigate to the new case's detail view.
-
-**Implementation**: CaseCreateDialog receives `@created` event with the new case ID. Parent navigates to `#/cases/{newId}`.
-
-### DD-07: Quick Status Change in List
-
-**Decision**: Clicking the status badge in a case list row opens a QuickStatusDropdown inline.
-
-**Rationale**: Matches CM-05a/b requirement. The dropdown shows only statuses from the case's case type. Selection triggers an immediate save and re-renders the row.
-
-**Implementation**: QuickStatusDropdown is a lightweight component that fetches status types for the case's case type. Uses `@click.stop` to prevent row navigation. Emits `@changed` for the parent to refresh.
-
-### DD-08: Read-Only Mode for Closed Cases
-
-**Decision**: Cases at a final status (`isFinal = true` on current status type) become fully read-only.
-
-**Rationale**: Matches CM-14f. Once a case reaches its final status, `endDate` is set and all form fields are disabled. The status dropdown is hidden (no further transitions).
-
-**Implementation**: Computed `isReadOnly` checks if the current status type's `isFinal` flag is true.
-
-### DD-09: Extension Flow
-
-**Decision**: Extension is a single-click action with a confirmation dialog capturing reason.
-
-**Rationale**: Matches CM-16a/b/c. The deadline panel shows an "Extend" button when the case type allows it and the case hasn't been extended yet. Clicking opens a small dialog for the reason. On confirm: `deadline += extensionPeriod`, `extensionCount++`, activity entry added.
-
-### DD-10: Reuse Overdue/Countdown Pattern
-
-**Decision**: Create case-specific overdue helpers in `caseHelpers.js` mirroring the pattern from `taskHelpers.js`.
-
-**Rationale**: Cases use `deadline` (not `dueDate`) and don't have CMMN terminal statuses — they use `isFinal` from their status type. The logic is similar but the field names and final-status check differ, so separate functions are cleaner than parameterizing the task helpers.
-
-## Component Hierarchy
-
-```
-CaseList.vue
-├── CaseCreateDialog.vue (modal, shown on "New case" click)
-└── QuickStatusDropdown.vue (inline per row)
-
-CaseDetail.vue
-├── StatusTimeline.vue
-├── DeadlinePanel.vue
-├── [existing task section]
-└── ActivityTimeline.vue
-```
-
-## Data Flow
-
-### Case Creation
-1. User clicks "New case" → CaseCreateDialog opens
-2. Dialog fetches published + valid case types via `fetchCollection('caseType', { '_filters[isDraft]': false })`
-3. User selects type → dialog shows defaults preview (deadline, confidentiality, initial status)
-4. User enters title → submits
-5. Dialog validates (caseValidation.js), constructs case object with auto-fields
-6. `saveObject('case', caseData)` → on success, emit `@created` with new ID
-7. Parent navigates to `#/cases/{newId}`
-
-### Status Change
-1. User selects new status from dropdown (detail or list)
-2. Frontend validates: status must be in case type's status types
-3. If target is final status: prompt for result text
-4. Update case: `status = newStatusTypeId`, append to `statusHistory`, append to `activity`
-5. If final: set `endDate = today`, set `result` text
-6. `saveObject('case', updatedCaseData)`
-
-### Deadline Extension
-1. User clicks "Extend" on deadline panel
-2. Confirmation dialog: enter reason text
-3. Calculate new deadline: `currentDeadline + parseDuration(caseType.extensionPeriod)`
-4. Update case: `deadline = newDeadline`, `extensionCount++`, append to `activity`
-5. `saveObject('case', updatedCaseData)`
+# Design: Case Management — MVP
+
+## Architecture Overview
+
+This change enhances the existing CaseList.vue and CaseDetail.vue with case-type-aware behavior, and adds new sub-components for status timeline, deadline panel, and activity timeline. All data flows through the existing `useObjectStore` Pinia store and OpenRegister API.
+
+## File Map
+
+### New Files
+
+| File | Purpose |
+|------|---------|
+| `src/utils/caseHelpers.js` | Deadline calculation, countdown text, identifier generation, overdue/due-today logic for cases |
+| `src/utils/caseValidation.js` | Case form validation: required fields, case type validity checks |
+| `src/views/cases/CaseCreateDialog.vue` | Modal dialog for creating a new case with case type selection |
+| `src/views/cases/components/StatusTimeline.vue` | Horizontal status timeline visualization |
+| `src/views/cases/components/DeadlinePanel.vue` | Deadline info panel with countdown and extension button |
+| `src/views/cases/components/ActivityTimeline.vue` | Chronological activity log with "Add note" input |
+| `src/views/cases/components/QuickStatusDropdown.vue` | Inline status dropdown for the case list |
+
+### Modified Files
+
+| File | Changes |
+|------|---------|
+| `src/views/cases/CaseList.vue` | Replace hardcoded statuses with case-type-aware filters; add columns (identifier, type, deadline countdown); add quick status change; add overdue filter; add case type filter; add "New case" opens CaseCreateDialog |
+| `src/views/cases/CaseDetail.vue` | Integrate case type data; replace hardcoded status/priority dropdowns with case-type-aware ones; add StatusTimeline, DeadlinePanel, ActivityTimeline components; add result input on final status; add read-only mode for closed cases; enhance info panel |
+| `src/App.vue` | No routing changes needed — existing `#/cases` and `#/cases/{id}` routes are sufficient |
+| `src/store/store.js` | No changes — `case`, `caseType`, `statusType` object types already registered |
+
+### Unchanged Files
+
+| File | Reason |
+|------|--------|
+| `lib/Service/SettingsService.php` | All needed config keys already exist |
+| `src/store/modules/object.js` | Generic CRUD store already supports all needed operations |
+| `src/navigation/MainMenu.vue` | Cases nav item already exists |
+| `src/utils/taskLifecycle.js` | Task lifecycle unchanged |
+| `src/utils/taskHelpers.js` | May import some helpers (isOverdue pattern) but file not modified |
+
+## Design Decisions
+
+### DD-01: Case Type Data Loading Strategy
+
+**Decision**: Load case types and their status types eagerly on CaseDetail mount; cache in object store.
+
+**Rationale**: Case detail needs case type data for status timeline, deadline panel, status dropdown, and validation. Loading on mount avoids repeated fetches. The object store's `getObject('caseType', id)` provides caching.
+
+**Implementation**: In CaseDetail.mounted(), after fetching the case, fetch its case type. Then fetch status types filtered by `_filters[caseType]={caseTypeId}` and sort by `_order[order]=asc`.
+
+### DD-02: Status History Tracking
+
+**Decision**: Store status history as an array property on the case object: `statusHistory: [{ status, date, changedBy }]`.
+
+**Rationale**: MVP avoids backend complexity. OpenRegister stores JSON objects, so an embedded array is natural. The status timeline component reads this array to show dates under passed statuses.
+
+**Trade-off**: Status history is denormalized. Acceptable for MVP; V1 can migrate to separate status change entities if needed.
+
+### DD-03: Activity Log Storage
+
+**Decision**: Store activity as an array property on the case object: `activity: [{ date, type, description, user }]`.
+
+**Rationale**: Avoids Nextcloud Activity system integration (V1 scope). Frontend appends entries on create/update/status-change/extension. Activity array grows with the case but is bounded in practice (tens of entries, not thousands).
+
+**Trade-off**: No server-side activity filtering or cross-case activity feeds. Acceptable for MVP.
+
+### DD-04: Identifier Generation
+
+**Decision**: Generate identifiers client-side as `YYYY-{timestamp_suffix}` where suffix is `Date.now() % 10000`.
+
+**Rationale**: True sequential numbering (YYYY-NNN) requires backend coordination (counter in app config). For MVP, a timestamp-based approach avoids collisions while being simple. Format: `2026-4281`.
+
+**Trade-off**: Not strictly sequential. Acceptable for MVP; V1 can add a backend endpoint for sequential IDs.
+
+### DD-05: Deadline Calculation
+
+**Decision**: Calculate deadline client-side: `startDate + parseDuration(caseType.processingDeadline)`.
+
+**Rationale**: Duration parsing already exists in `durationHelpers.js`. The `parseDuration()` function returns `{ years, months, weeks, days }`. Apply these to the start date using JavaScript Date arithmetic.
+
+**Implementation**: New function `calculateDeadline(startDate, durationString)` in `caseHelpers.js`. Handles P{n}D (add days), P{n}W (add weeks*7 days), P{n}M (add months), P{n}Y (add years).
+
+### DD-06: Case Create Flow
+
+**Decision**: Use a modal dialog (CaseCreateDialog) instead of navigating to `#/cases/new`.
+
+**Rationale**: The create form needs case type selection with live preview of defaults (deadline, confidentiality, initial status). A dialog keeps the user in context (case list or dashboard). After creation, navigate to the new case's detail view.
+
+**Implementation**: CaseCreateDialog receives `@created` event with the new case ID. Parent navigates to `#/cases/{newId}`.
+
+### DD-07: Quick Status Change in List
+
+**Decision**: Clicking the status badge in a case list row opens a QuickStatusDropdown inline.
+
+**Rationale**: Matches CM-05a/b requirement. The dropdown shows only statuses from the case's case type. Selection triggers an immediate save and re-renders the row.
+
+**Implementation**: QuickStatusDropdown is a lightweight component that fetches status types for the case's case type. Uses `@click.stop` to prevent row navigation. Emits `@changed` for the parent to refresh.
+
+### DD-08: Read-Only Mode for Closed Cases
+
+**Decision**: Cases at a final status (`isFinal = true` on current status type) become fully read-only.
+
+**Rationale**: Matches CM-14f. Once a case reaches its final status, `endDate` is set and all form fields are disabled. The status dropdown is hidden (no further transitions).
+
+**Implementation**: Computed `isReadOnly` checks if the current status type's `isFinal` flag is true.
+
+### DD-09: Extension Flow
+
+**Decision**: Extension is a single-click action with a confirmation dialog capturing reason.
+
+**Rationale**: Matches CM-16a/b/c. The deadline panel shows an "Extend" button when the case type allows it and the case hasn't been extended yet. Clicking opens a small dialog for the reason. On confirm: `deadline += extensionPeriod`, `extensionCount++`, activity entry added.
+
+### DD-10: Reuse Overdue/Countdown Pattern
+
+**Decision**: Create case-specific overdue helpers in `caseHelpers.js` mirroring the pattern from `taskHelpers.js`.
+
+**Rationale**: Cases use `deadline` (not `dueDate`) and don't have CMMN terminal statuses — they use `isFinal` from their status type. The logic is similar but the field names and final-status check differ, so separate functions are cleaner than parameterizing the task helpers.
+
+## Component Hierarchy
+
+```
+CaseList.vue
+├── CaseCreateDialog.vue (modal, shown on "New case" click)
+└── QuickStatusDropdown.vue (inline per row)
+
+CaseDetail.vue
+├── StatusTimeline.vue
+├── DeadlinePanel.vue
+├── [existing task section]
+└── ActivityTimeline.vue
+```
+
+## Data Flow
+
+### Case Creation
+1. User clicks "New case" → CaseCreateDialog opens
+2. Dialog fetches published + valid case types via `fetchCollection('caseType', { '_filters[isDraft]': false })`
+3. User selects type → dialog shows defaults preview (deadline, confidentiality, initial status)
+4. User enters title → submits
+5. Dialog validates (caseValidation.js), constructs case object with auto-fields
+6. `saveObject('case', caseData)` → on success, emit `@created` with new ID
+7. Parent navigates to `#/cases/{newId}`
+
+### Status Change
+1. User selects new status from dropdown (detail or list)
+2. Frontend validates: status must be in case type's status types
+3. If target is final status: prompt for result text
+4. Update case: `status = newStatusTypeId`, append to `statusHistory`, append to `activity`
+5. If final: set `endDate = today`, set `result` text
+6. `saveObject('case', updatedCaseData)`
+
+### Deadline Extension
+1. User clicks "Extend" on deadline panel
+2. Confirmation dialog: enter reason text
+3. Calculate new deadline: `currentDeadline + parseDuration(caseType.extensionPeriod)`
+4. Update case: `deadline = newDeadline`, `extensionCount++`, append to `activity`
+5. `saveObject('case', updatedCaseData)`
diff --git a/openspec/changes/archive/2026-02-26-case-management/proposal.md b/openspec/changes/archive/2026-02-26-case-management/proposal.md
index 7bb13355..6d43d5b4 100644
--- a/openspec/changes/archive/2026-02-26-case-management/proposal.md
+++ b/openspec/changes/archive/2026-02-26-case-management/proposal.md
@@ -1,52 +1,52 @@
-# Proposal: Case Management
-
-## Summary
-
-Enhance the existing basic case CRUD (CaseList + CaseDetail) with full case management capabilities: case type integration, status timeline, deadline tracking, extension support, case-scoped participants, and activity timeline. This builds on the completed case-types and task-management foundations.
-
-## Problem
-
-The current CaseList and CaseDetail views are minimal — hardcoded status options (open, in_progress, closed), no case type integration, no deadline calculation, no status timeline, and no activity log. Cases cannot leverage the rich case type configuration already built in the admin settings.
-
-## Scope — MVP
-
-**In scope (MVP tier):**
-- Case creation with case type selection, validation (published + valid), auto-defaults (identifier, startDate, deadline, confidentiality, initial status)
-- Case update with title, description, assignee, priority fields
-- Case deletion with confirmation (warn about linked tasks)
-- Case list with filters (type, status, handler, priority, overdue), search, sort, pagination
-- Quick status change from list via dropdown (case-type-aware statuses)
-- Case detail: info panel, status change dropdown, deadline panel with countdown
-- Status timeline visualization (passed/current/future dots with dates)
-- Participants panel (MVP: handler assignment via assignee field + display initiator)
-- Tasks section (already exists from task-management, minor enhancements)
-- Activity timeline (frontend-only event log stored as case property, not full Nextcloud Activity integration)
-- Case result recording (basic: text field on final status)
-- Deadline extension (when case type allows it, single extension)
-- Deadline countdown display across list and detail views
-- Validation rules (title required, case type required + published + valid)
-
-**Out of scope (V1):**
-- Full participant/role type management (CM-08b)
-- Custom properties panel (CM-09)
-- Document checklist (CM-10)
-- Decisions section (CM-12)
-- Status change blocked by missing properties/documents (CM-14c, CM-14d)
-- Notification on status change (CM-14e)
-- Case suspension (CM-17)
-- Sub-cases (CM-18)
-- Confidentiality level override UI (CM-19)
-- Nextcloud Activity system integration for audit trail (CM-22 — use frontend event array instead)
-
-## Approach
-
-- Enhance existing CaseList.vue and CaseDetail.vue rather than replacing them
-- New utility modules: `caseHelpers.js` (deadline calculations, countdown, identifier generation), `caseValidation.js` (form validation with case type awareness)
-- New components: StatusTimeline.vue, DeadlinePanel.vue, ActivityTimeline.vue, CaseCreateDialog.vue
-- Reuse existing patterns: useObjectStore for all CRUD, duration helpers for deadline display, task helpers for overdue logic
-
-## Dependencies
-
-- **case-types** (archived) — Case type and status type data in OpenRegister
-- **task-management** (archived) — Task lifecycle, task section in CaseDetail
-- **OpenRegister** — All data persistence
+# Proposal: Case Management
+
+## Summary
+
+Enhance the existing basic case CRUD (CaseList + CaseDetail) with full case management capabilities: case type integration, status timeline, deadline tracking, extension support, case-scoped participants, and activity timeline. This builds on the completed case-types and task-management foundations.
+
+## Problem
+
+The current CaseList and CaseDetail views are minimal — hardcoded status options (open, in_progress, closed), no case type integration, no deadline calculation, no status timeline, and no activity log. Cases cannot leverage the rich case type configuration already built in the admin settings.
+
+## Scope — MVP
+
+**In scope (MVP tier):**
+- Case creation with case type selection, validation (published + valid), auto-defaults (identifier, startDate, deadline, confidentiality, initial status)
+- Case update with title, description, assignee, priority fields
+- Case deletion with confirmation (warn about linked tasks)
+- Case list with filters (type, status, handler, priority, overdue), search, sort, pagination
+- Quick status change from list via dropdown (case-type-aware statuses)
+- Case detail: info panel, status change dropdown, deadline panel with countdown
+- Status timeline visualization (passed/current/future dots with dates)
+- Participants panel (MVP: handler assignment via assignee field + display initiator)
+- Tasks section (already exists from task-management, minor enhancements)
+- Activity timeline (frontend-only event log stored as case property, not full Nextcloud Activity integration)
+- Case result recording (basic: text field on final status)
+- Deadline extension (when case type allows it, single extension)
+- Deadline countdown display across list and detail views
+- Validation rules (title required, case type required + published + valid)
+
+**Out of scope (V1):**
+- Full participant/role type management (CM-08b)
+- Custom properties panel (CM-09)
+- Document checklist (CM-10)
+- Decisions section (CM-12)
+- Status change blocked by missing properties/documents (CM-14c, CM-14d)
+- Notification on status change (CM-14e)
+- Case suspension (CM-17)
+- Sub-cases (CM-18)
+- Confidentiality level override UI (CM-19)
+- Nextcloud Activity system integration for audit trail (CM-22 — use frontend event array instead)
+
+## Approach
+
+- Enhance existing CaseList.vue and CaseDetail.vue rather than replacing them
+- New utility modules: `caseHelpers.js` (deadline calculations, countdown, identifier generation), `caseValidation.js` (form validation with case type awareness)
+- New components: StatusTimeline.vue, DeadlinePanel.vue, ActivityTimeline.vue, CaseCreateDialog.vue
+- Reuse existing patterns: useObjectStore for all CRUD, duration helpers for deadline display, task helpers for overdue logic
+
+## Dependencies
+
+- **case-types** (archived) — Case type and status type data in OpenRegister
+- **task-management** (archived) — Task lifecycle, task section in CaseDetail
+- **OpenRegister** — All data persistence
diff --git a/openspec/changes/archive/2026-02-26-case-management/review.md b/openspec/changes/archive/2026-02-26-case-management/review.md
index 4ba114b6..4f1463d0 100644
--- a/openspec/changes/archive/2026-02-26-case-management/review.md
+++ b/openspec/changes/archive/2026-02-26-case-management/review.md
@@ -1,87 +1,87 @@
-# Review: case-management
-
-## Summary
-- Tasks completed: 15/15
-- GitHub issues closed: N/A (used `/opsx:ff`, no `plan.json` or GitHub issues created)
-- Spec compliance: **PASS** (with warnings)
-
-## Verification
-
-### Task Completion
-All 15 implementation tasks (T01-T15) are marked complete in `tasks.md`. Verification tasks V01 and V10 are marked complete (file existence + task checklist). V02-V09 (manual browser testing) remain unchecked — these require live testing.
-
-### Files Created/Modified
-All 7 new files exist and are syntactically valid:
-- `src/utils/caseHelpers.js`
-- `src/utils/caseValidation.js`
-- `src/views/cases/CaseCreateDialog.vue`
-- `src/views/cases/components/StatusTimeline.vue`
-- `src/views/cases/components/DeadlinePanel.vue`
-- `src/views/cases/components/ActivityTimeline.vue`
-- `src/views/cases/components/QuickStatusDropdown.vue`
-
-Both modified files have been rewritten:
-- `src/views/cases/CaseList.vue`
-- `src/views/cases/CaseDetail.vue`
-
----
-
-## Requirement-by-Requirement Verification
-
-| Req | Status | Notes |
-|-----|--------|-------|
-| CM-01 (Case Creation) | PASS (with warning) | All auto-fields, validation, preview panel implemented. Default case type pre-selection from admin settings not implemented. |
-| CM-02 (Case Update) | PASS | All 4 editable fields + activity tracking for changes |
-| CM-03 (Case Deletion) | PASS | Confirmation dialog with linked task count warning |
-| CM-04 (Case List View) | PASS (with warning) | 6 columns, 5 filters, search, sort, pagination. Overdue filter checkbox is non-functional (see W02). |
-| CM-05 (Quick Status Change) | PASS | QuickStatusDropdown inline with @click.stop, updates + refreshes list |
-| CM-06 (Case Detail View) | PASS | Full info panel, status change dropdown, deadline panel |
-| CM-07 (Status Timeline) | PASS | Horizontal dots with passed/current/future states and date labels |
-| CM-11 (Tasks Section) | PASS | Task list with completion counter, "New task" button, status/priority badges |
-| CM-13 (Activity Timeline) | PASS | All 4 auto-recorded types + manual notes |
-| CM-14 (Status Change) | PASS | Statuses from case type only, final status sets endDate, read-only mode |
-| CM-15 (Result Recording) | PASS | Result text prompt on final status, required before confirm |
-| CM-16 (Deadline Extension) | PASS | Extension dialog with reason, deadline calculation, extensionCount tracking |
-| CM-20 (Case Validation) | PASS | Title required, case type required + published + valid window |
-| CM-21 (Deadline Countdown) | PASS | 4 states (remaining/today/tomorrow/overdue) with correct styles in both list and detail |
-
----
-
-## Findings
-
-### CRITICAL
-None.
-
-### WARNING
-- [ ] **W01**: CM-01 — Default case type pre-selection from admin settings is not implemented. The delta spec lists "Default case type pre-selected if configured in admin settings" as a key implementation point. `CaseCreateDialog.vue` does not read `default_case_type` from settings. (spec_ref: CM-01, "Default case type pre-selected if configured in admin settings")
-
-- [ ] **W02**: CM-04 — Overdue filter checkbox is non-functional. `CaseList.vue` has a `filters.overdue` checkbox (line 43-49) that triggers `onFilterChange()` → `fetchCases()`, but `fetchCases()` never sends the overdue value to the API. The checkbox appears functional but has no effect on results. Either implement client-side filtering post-fetch, add a backend `_filters[overdue]` param, or remove the checkbox for MVP. (spec_ref: CM-04, filter list includes "overdue toggle")
-
-- [ ] **W03**: Accessibility — `CaseCreateDialog.vue` and extension dialog overlays lack focus traps, `aria-modal` attributes, and ESC key handling. These are custom overlay implementations. The NL Design System shared spec recommends WCAG AA compliance which includes proper modal dialog accessibility. (spec_ref: shared nl-design spec, accessibility)
-
-### SUGGESTION
-- The `confirm()` native dialog for deletion (`CaseDetail.vue:546`) could be replaced with a proper NcDialog component for better UX and consistency with the custom dialogs used elsewhere.
-- `generateIdentifier()` uses `Date.now() % 10000` which could produce collisions in rapid creation. The delta spec acknowledges this as acceptable for MVP; V1 should add backend sequential numbering.
-- `CaseCreateDialog.vue` fetches all case types and filters client-side via `isCaseTypeUsable()`. For large case type counts, a server-side `_filters[isDraft]=false` filter would be more efficient. Fine for MVP.
-- `QuickStatusDropdown.vue:86` saves the entire case object spread (`{ ...this.caseObj, status, statusHistory, activity }`). If `caseObj` is stale (another user changed it), this could overwrite their changes. Consider a PATCH-style update in V1.
-
----
-
-## Cross-Reference with Shared Specs
-
-| Shared Spec | Status | Notes |
-|-------------|--------|-------|
-| nextcloud-app | PASS | No PHP/routing changes needed; existing routes sufficient |
-| api-patterns | N/A | No new API endpoints created; uses existing objectStore |
-| nl-design | WARNING | Colors use CSS variables (good), but custom modals lack accessibility features (W03) |
-| docker | PASS | No Docker changes needed; all frontend-only |
-
----
-
-## Recommendation
-**APPROVE** — All 13 MVP requirements are implemented with correct behavior. The 3 warnings are minor:
-- W01 (default case type) is a SHOULD-level feature, not blocking
-- W02 (overdue filter) is a UI bug that should be fixed before merge
-- W03 (accessibility) is a cross-cutting concern applicable to the entire app, not specific to this change
-
-**Suggested pre-merge fix**: Address W02 by either implementing the overdue filter or removing the non-functional checkbox.
+# Review: case-management
+
+## Summary
+- Tasks completed: 15/15
+- GitHub issues closed: N/A (used `/opsx:ff`, no `plan.json` or GitHub issues created)
+- Spec compliance: **PASS** (with warnings)
+
+## Verification
+
+### Task Completion
+All 15 implementation tasks (T01-T15) are marked complete in `tasks.md`. Verification tasks V01 and V10 are marked complete (file existence + task checklist). V02-V09 (manual browser testing) remain unchecked — these require live testing.
+
+### Files Created/Modified
+All 7 new files exist and are syntactically valid:
+- `src/utils/caseHelpers.js`
+- `src/utils/caseValidation.js`
+- `src/views/cases/CaseCreateDialog.vue`
+- `src/views/cases/components/StatusTimeline.vue`
+- `src/views/cases/components/DeadlinePanel.vue`
+- `src/views/cases/components/ActivityTimeline.vue`
+- `src/views/cases/components/QuickStatusDropdown.vue`
+
+Both modified files have been rewritten:
+- `src/views/cases/CaseList.vue`
+- `src/views/cases/CaseDetail.vue`
+
+---
+
+## Requirement-by-Requirement Verification
+
+| Req | Status | Notes |
+|-----|--------|-------|
+| CM-01 (Case Creation) | PASS (with warning) | All auto-fields, validation, preview panel implemented. Default case type pre-selection from admin settings not implemented. |
+| CM-02 (Case Update) | PASS | All 4 editable fields + activity tracking for changes |
+| CM-03 (Case Deletion) | PASS | Confirmation dialog with linked task count warning |
+| CM-04 (Case List View) | PASS (with warning) | 6 columns, 5 filters, search, sort, pagination. Overdue filter checkbox is non-functional (see W02). |
+| CM-05 (Quick Status Change) | PASS | QuickStatusDropdown inline with @click.stop, updates + refreshes list |
+| CM-06 (Case Detail View) | PASS | Full info panel, status change dropdown, deadline panel |
+| CM-07 (Status Timeline) | PASS | Horizontal dots with passed/current/future states and date labels |
+| CM-11 (Tasks Section) | PASS | Task list with completion counter, "New task" button, status/priority badges |
+| CM-13 (Activity Timeline) | PASS | All 4 auto-recorded types + manual notes |
+| CM-14 (Status Change) | PASS | Statuses from case type only, final status sets endDate, read-only mode |
+| CM-15 (Result Recording) | PASS | Result text prompt on final status, required before confirm |
+| CM-16 (Deadline Extension) | PASS | Extension dialog with reason, deadline calculation, extensionCount tracking |
+| CM-20 (Case Validation) | PASS | Title required, case type required + published + valid window |
+| CM-21 (Deadline Countdown) | PASS | 4 states (remaining/today/tomorrow/overdue) with correct styles in both list and detail |
+
+---
+
+## Findings
+
+### CRITICAL
+None.
+
+### WARNING
+- [ ] **W01**: CM-01 — Default case type pre-selection from admin settings is not implemented. The delta spec lists "Default case type pre-selected if configured in admin settings" as a key implementation point. `CaseCreateDialog.vue` does not read `default_case_type` from settings. (spec_ref: CM-01, "Default case type pre-selected if configured in admin settings")
+
+- [ ] **W02**: CM-04 — Overdue filter checkbox is non-functional. `CaseList.vue` has a `filters.overdue` checkbox (line 43-49) that triggers `onFilterChange()` → `fetchCases()`, but `fetchCases()` never sends the overdue value to the API. The checkbox appears functional but has no effect on results. Either implement client-side filtering post-fetch, add a backend `_filters[overdue]` param, or remove the checkbox for MVP. (spec_ref: CM-04, filter list includes "overdue toggle")
+
+- [ ] **W03**: Accessibility — `CaseCreateDialog.vue` and extension dialog overlays lack focus traps, `aria-modal` attributes, and ESC key handling. These are custom overlay implementations. The NL Design System shared spec recommends WCAG AA compliance which includes proper modal dialog accessibility. (spec_ref: shared nl-design spec, accessibility)
+
+### SUGGESTION
+- The `confirm()` native dialog for deletion (`CaseDetail.vue:546`) could be replaced with a proper NcDialog component for better UX and consistency with the custom dialogs used elsewhere.
+- `generateIdentifier()` uses `Date.now() % 10000` which could produce collisions in rapid creation. The delta spec acknowledges this as acceptable for MVP; V1 should add backend sequential numbering.
+- `CaseCreateDialog.vue` fetches all case types and filters client-side via `isCaseTypeUsable()`. For large case type counts, a server-side `_filters[isDraft]=false` filter would be more efficient. Fine for MVP.
+- `QuickStatusDropdown.vue:86` saves the entire case object spread (`{ ...this.caseObj, status, statusHistory, activity }`). If `caseObj` is stale (another user changed it), this could overwrite their changes. Consider a PATCH-style update in V1.
+
+---
+
+## Cross-Reference with Shared Specs
+
+| Shared Spec | Status | Notes |
+|-------------|--------|-------|
+| nextcloud-app | PASS | No PHP/routing changes needed; existing routes sufficient |
+| api-patterns | N/A | No new API endpoints created; uses existing objectStore |
+| nl-design | WARNING | Colors use CSS variables (good), but custom modals lack accessibility features (W03) |
+| docker | PASS | No Docker changes needed; all frontend-only |
+
+---
+
+## Recommendation
+**APPROVE** — All 13 MVP requirements are implemented with correct behavior. The 3 warnings are minor:
+- W01 (default case type) is a SHOULD-level feature, not blocking
+- W02 (overdue filter) is a UI bug that should be fixed before merge
+- W03 (accessibility) is a cross-cutting concern applicable to the entire app, not specific to this change
+
+**Suggested pre-merge fix**: Address W02 by either implementing the overdue filter or removing the non-functional checkbox.
diff --git a/openspec/changes/archive/2026-02-26-case-management/specs/case-management/spec.md b/openspec/changes/archive/2026-02-26-case-management/specs/case-management/spec.md
index 2fd942d4..8520a156 100644
--- a/openspec/changes/archive/2026-02-26-case-management/specs/case-management/spec.md
+++ b/openspec/changes/archive/2026-02-26-case-management/specs/case-management/spec.md
@@ -1,202 +1,202 @@
-# Delta Spec: Case Management — MVP
-
-**Source**: `openspec/specs/case-management/spec.md`
-**Scope**: MVP tier requirements only. V1 items (CM-08b, CM-09, CM-10, CM-12, CM-14c/d/e, CM-17, CM-18, CM-19) are deferred.
-
----
-
-## MODIFIED — REQ-CM-01: Case Creation (MVP scope)
-
-Scenarios included: CM-01a, CM-01b, CM-01c, CM-01d, CM-01e, CM-01f, CM-01g
-
-All creation scenarios are in MVP scope. Key implementation points:
-
-- Case type selection dropdown showing only published + currently valid case types
-- Auto-generate `identifier` in format `YYYY-NNN` (year + sequential number)
-- Auto-set `startDate` to current date
-- Auto-calculate `deadline` from `startDate + caseType.processingDeadline`
-- Inherit `confidentiality` from case type
-- Set initial `status` to first status type (lowest `order`) of the case type
-- Default case type pre-selected if configured in admin settings
-- Validation: title required, case type required, case type must be published, case type must be within validity window
-
-**MVP simplification**: Identifier generation uses `YYYY-{Date.now() % 10000}` format (frontend-generated, not sequential from backend). This is acceptable for MVP since true sequential numbering requires backend coordination.
-
----
-
-## INCLUDED — REQ-CM-02: Case Update (MVP scope)
-
-Scenarios included: CM-02a, CM-02b, CM-02c
-
-All update scenarios are in MVP scope. Editable fields: title, description, assignee, priority.
-
-**MVP simplification**: Audit trail entries are stored as an `activity` array property on the case object itself (not via Nextcloud Activity system). Each entry: `{ date, type, description, user }`.
-
----
-
-## INCLUDED — REQ-CM-03: Case Deletion (MVP scope)
-
-Scenarios included: CM-03a, CM-03b
-
-Both deletion scenarios in scope. Show confirmation dialog; if case has linked tasks, show count in warning message.
-
-**MVP simplification**: Linked tasks are NOT cascade-deleted. The warning informs the user, but deletion only removes the case object. Orphaned tasks remain accessible from the global task list.
-
----
-
-## INCLUDED — REQ-CM-04: Case List View (MVP scope)
-
-Scenarios included: CM-04a, CM-04b, CM-04c, CM-04d, CM-04e, CM-04f, CM-04g, CM-04h, CM-04i
-
-All list scenarios in scope. Enhance existing CaseList.vue:
-
-- Columns: Identifier, Title, Type, Status (badge), Deadline (countdown), Handler
-- Filters: case type, status, handler (text), priority, overdue toggle
-- Search against title/description (debounced 300ms)
-- Sort by any column (server-side via `_order` param)
-- Pagination at 20 items per page
-- Overdue cases highlighted with red left border + red deadline text
-
----
-
-## INCLUDED — REQ-CM-05: Quick Status Change from List (MVP scope, partial)
-
-Scenarios included: CM-05a, CM-05b
-Scenarios EXCLUDED: CM-05c (blocked by properties — V1)
-
-Quick status change via clickable status cell in the case list. Opens a dropdown of statuses from the case's case type. Selecting a new status updates inline without page reload.
-
-**MVP simplification**: No property/document validation on status change from list. That is V1 (CM-05c).
-
----
-
-## INCLUDED — REQ-CM-06: Case Detail View (MVP scope, partial)
-
-Scenarios included: CM-06a, CM-06b, CM-06c, CM-06d, CM-06e, CM-06f
-
-Case detail view with:
-- **Info panel**: title (editable), description (editable), type (read-only), identifier (read-only), priority (dropdown), confidentiality (read-only, inherited), assignee (editable), creation date
-- **Status change dropdown**: shows statuses from case type, current status highlighted
-- **Deadline panel**: start date, deadline, days remaining/overdue, processing deadline duration, days elapsed, extension button (if allowed)
-
----
-
-## INCLUDED — REQ-CM-07: Status Timeline Visualization (MVP scope)
-
-Scenarios included: CM-07a, CM-07b, CM-07c
-
-Horizontal timeline of status dots:
-- Each dot = a status type from the case type (ordered by `order`)
-- Passed statuses: filled dot + date label below
-- Current status: highlighted/active dot + date label
-- Future statuses: greyed/empty dot, no date
-
-Status dates are tracked via `statusHistory` array on the case object: `[{ status: "uuid", date: "ISO", changedBy: "user" }]`.
-
----
-
-## INCLUDED — REQ-CM-11: Tasks Section (MVP scope)
-
-Scenarios included: CM-11a, CM-11b
-
-Already partially implemented in CaseDetail.vue from task-management change. Enhancements:
-- Keep existing task list with completion counter
-- Ensure "Add Task" button navigates to `#/tasks/new/{caseId}`
-- Status icons for each task state
-
----
-
-## INCLUDED — REQ-CM-13: Activity Timeline (MVP scope, simplified)
-
-Scenarios included: CM-13a, CM-13b
-
-**MVP simplification**: Activity is stored as an `activity` array on the case object. NOT integrated with Nextcloud Activity system (that is V1/CM-22).
-
-Activity entries automatically recorded for:
-- Case creation
-- Status changes
-- Field updates (title, description, assignee, priority changes)
-- Deadline extension
-
-Manual notes: user can add a text note via the activity panel. Stored in same `activity` array.
-
-Each entry: `{ date: ISO, type: "created"|"status_change"|"update"|"extension"|"note", description: string, user: string }`.
-
----
-
-## INCLUDED — REQ-CM-14: Status Change (MVP scope, partial)
-
-Scenarios included: CM-14a, CM-14b, CM-14f
-Scenarios EXCLUDED: CM-14c (properties block — V1), CM-14d (documents block — V1), CM-14e (notification — V1)
-
-- Only statuses from the case type are allowed
-- Invalid status rejected with error message
-- Final status (`isFinal = true`) sets `endDate` to current date and marks case as closed
-- Closed cases become read-only (no further edits without reopening)
-
----
-
-## INCLUDED — REQ-CM-15: Case Result Recording (MVP scope, simplified)
-
-Scenarios included: CM-15b (simplified)
-Scenarios EXCLUDED: CM-15a (result types from case type — V1), CM-15c (archival rules — V1)
-
-**MVP simplification**: When transitioning to a final status, a text input for "Result" is shown. The result is stored as a `result` string property on the case object. No result type entities, no archival rules.
-
----
-
-## INCLUDED — REQ-CM-16: Case Deadline Extension (MVP scope)
-
-Scenarios included: CM-16a, CM-16b, CM-16c
-
-- Extension button visible when case type `extensionAllowed = true`
-- Clicking extends deadline by `caseType.extensionPeriod`
-- Track `extensionCount` on case object; reject if already extended once
-- Reason captured as text input in extension dialog
-- Activity entry recorded for extension
-
----
-
-## INCLUDED — REQ-CM-20: Case Validation Rules (MVP scope)
-
-Scenarios included: CM-20a, CM-20b, CM-20c, CM-20d
-Scenario CM-20e: noted but not enforced (future date warning only)
-
-Client-side validation:
-- Title required
-- Case type required + must be published + must be within validity window
-- Status must be from case type's status types
-
----
-
-## INCLUDED — REQ-CM-21: Case Deadline Countdown Display (MVP scope)
-
-Scenarios included: CM-21a, CM-21b, CM-21c, CM-21d
-
-Deadline countdown logic (reuse overdue pattern from taskHelpers):
-- `X days remaining` — neutral style (green)
-- `Due tomorrow` — warning style (amber)
-- `Due today` — warning style (amber)
-- `X days overdue` — error style (red)
-
-Applied in both CaseList (deadline column) and CaseDetail (deadline panel).
-
----
-
-## EXCLUDED — V1 Requirements
-
-The following are explicitly excluded from this MVP change:
-
-| Req | Description | Reason |
-|-----|-------------|--------|
-| CM-08b | Full participant/role management | Needs role types infrastructure |
-| CM-09 | Custom properties panel | Needs property definition types |
-| CM-10 | Document checklist | Needs document type infrastructure |
-| CM-12 | Decisions section | Needs decision entity types |
-| CM-14c | Status blocked by properties | Depends on CM-09 |
-| CM-14d | Status blocked by documents | Depends on CM-10 |
-| CM-14e | Notification on status change | Backend notification system |
-| CM-17 | Case suspension | Complex deadline recalculation |
-| CM-18 | Sub-cases | Parent/child hierarchy |
-| CM-19 | Confidentiality override | Read-only from case type for MVP |
-| CM-22 | Nextcloud Activity audit trail | Backend Activity provider needed |
+# Delta Spec: Case Management — MVP
+
+**Source**: `openspec/specs/case-management/spec.md`
+**Scope**: MVP tier requirements only. V1 items (CM-08b, CM-09, CM-10, CM-12, CM-14c/d/e, CM-17, CM-18, CM-19) are deferred.
+
+---
+
+## MODIFIED — REQ-CM-01: Case Creation (MVP scope)
+
+Scenarios included: CM-01a, CM-01b, CM-01c, CM-01d, CM-01e, CM-01f, CM-01g
+
+All creation scenarios are in MVP scope. Key implementation points:
+
+- Case type selection dropdown showing only published + currently valid case types
+- Auto-generate `identifier` in format `YYYY-NNN` (year + sequential number)
+- Auto-set `startDate` to current date
+- Auto-calculate `deadline` from `startDate + caseType.processingDeadline`
+- Inherit `confidentiality` from case type
+- Set initial `status` to first status type (lowest `order`) of the case type
+- Default case type pre-selected if configured in admin settings
+- Validation: title required, case type required, case type must be published, case type must be within validity window
+
+**MVP simplification**: Identifier generation uses `YYYY-{Date.now() % 10000}` format (frontend-generated, not sequential from backend). This is acceptable for MVP since true sequential numbering requires backend coordination.
+
+---
+
+## INCLUDED — REQ-CM-02: Case Update (MVP scope)
+
+Scenarios included: CM-02a, CM-02b, CM-02c
+
+All update scenarios are in MVP scope. Editable fields: title, description, assignee, priority.
+
+**MVP simplification**: Audit trail entries are stored as an `activity` array property on the case object itself (not via Nextcloud Activity system). Each entry: `{ date, type, description, user }`.
+
+---
+
+## INCLUDED — REQ-CM-03: Case Deletion (MVP scope)
+
+Scenarios included: CM-03a, CM-03b
+
+Both deletion scenarios in scope. Show confirmation dialog; if case has linked tasks, show count in warning message.
+
+**MVP simplification**: Linked tasks are NOT cascade-deleted. The warning informs the user, but deletion only removes the case object. Orphaned tasks remain accessible from the global task list.
+
+---
+
+## INCLUDED — REQ-CM-04: Case List View (MVP scope)
+
+Scenarios included: CM-04a, CM-04b, CM-04c, CM-04d, CM-04e, CM-04f, CM-04g, CM-04h, CM-04i
+
+All list scenarios in scope. Enhance existing CaseList.vue:
+
+- Columns: Identifier, Title, Type, Status (badge), Deadline (countdown), Handler
+- Filters: case type, status, handler (text), priority, overdue toggle
+- Search against title/description (debounced 300ms)
+- Sort by any column (server-side via `_order` param)
+- Pagination at 20 items per page
+- Overdue cases highlighted with red left border + red deadline text
+
+---
+
+## INCLUDED — REQ-CM-05: Quick Status Change from List (MVP scope, partial)
+
+Scenarios included: CM-05a, CM-05b
+Scenarios EXCLUDED: CM-05c (blocked by properties — V1)
+
+Quick status change via clickable status cell in the case list. Opens a dropdown of statuses from the case's case type. Selecting a new status updates inline without page reload.
+
+**MVP simplification**: No property/document validation on status change from list. That is V1 (CM-05c).
+
+---
+
+## INCLUDED — REQ-CM-06: Case Detail View (MVP scope, partial)
+
+Scenarios included: CM-06a, CM-06b, CM-06c, CM-06d, CM-06e, CM-06f
+
+Case detail view with:
+- **Info panel**: title (editable), description (editable), type (read-only), identifier (read-only), priority (dropdown), confidentiality (read-only, inherited), assignee (editable), creation date
+- **Status change dropdown**: shows statuses from case type, current status highlighted
+- **Deadline panel**: start date, deadline, days remaining/overdue, processing deadline duration, days elapsed, extension button (if allowed)
+
+---
+
+## INCLUDED — REQ-CM-07: Status Timeline Visualization (MVP scope)
+
+Scenarios included: CM-07a, CM-07b, CM-07c
+
+Horizontal timeline of status dots:
+- Each dot = a status type from the case type (ordered by `order`)
+- Passed statuses: filled dot + date label below
+- Current status: highlighted/active dot + date label
+- Future statuses: greyed/empty dot, no date
+
+Status dates are tracked via `statusHistory` array on the case object: `[{ status: "uuid", date: "ISO", changedBy: "user" }]`.
+
+---
+
+## INCLUDED — REQ-CM-11: Tasks Section (MVP scope)
+
+Scenarios included: CM-11a, CM-11b
+
+Already partially implemented in CaseDetail.vue from task-management change. Enhancements:
+- Keep existing task list with completion counter
+- Ensure "Add Task" button navigates to `#/tasks/new/{caseId}`
+- Status icons for each task state
+
+---
+
+## INCLUDED — REQ-CM-13: Activity Timeline (MVP scope, simplified)
+
+Scenarios included: CM-13a, CM-13b
+
+**MVP simplification**: Activity is stored as an `activity` array on the case object. NOT integrated with Nextcloud Activity system (that is V1/CM-22).
+
+Activity entries automatically recorded for:
+- Case creation
+- Status changes
+- Field updates (title, description, assignee, priority changes)
+- Deadline extension
+
+Manual notes: user can add a text note via the activity panel. Stored in same `activity` array.
+
+Each entry: `{ date: ISO, type: "created"|"status_change"|"update"|"extension"|"note", description: string, user: string }`.
+
+---
+
+## INCLUDED — REQ-CM-14: Status Change (MVP scope, partial)
+
+Scenarios included: CM-14a, CM-14b, CM-14f
+Scenarios EXCLUDED: CM-14c (properties block — V1), CM-14d (documents block — V1), CM-14e (notification — V1)
+
+- Only statuses from the case type are allowed
+- Invalid status rejected with error message
+- Final status (`isFinal = true`) sets `endDate` to current date and marks case as closed
+- Closed cases become read-only (no further edits without reopening)
+
+---
+
+## INCLUDED — REQ-CM-15: Case Result Recording (MVP scope, simplified)
+
+Scenarios included: CM-15b (simplified)
+Scenarios EXCLUDED: CM-15a (result types from case type — V1), CM-15c (archival rules — V1)
+
+**MVP simplification**: When transitioning to a final status, a text input for "Result" is shown. The result is stored as a `result` string property on the case object. No result type entities, no archival rules.
+
+---
+
+## INCLUDED — REQ-CM-16: Case Deadline Extension (MVP scope)
+
+Scenarios included: CM-16a, CM-16b, CM-16c
+
+- Extension button visible when case type `extensionAllowed = true`
+- Clicking extends deadline by `caseType.extensionPeriod`
+- Track `extensionCount` on case object; reject if already extended once
+- Reason captured as text input in extension dialog
+- Activity entry recorded for extension
+
+---
+
+## INCLUDED — REQ-CM-20: Case Validation Rules (MVP scope)
+
+Scenarios included: CM-20a, CM-20b, CM-20c, CM-20d
+Scenario CM-20e: noted but not enforced (future date warning only)
+
+Client-side validation:
+- Title required
+- Case type required + must be published + must be within validity window
+- Status must be from case type's status types
+
+---
+
+## INCLUDED — REQ-CM-21: Case Deadline Countdown Display (MVP scope)
+
+Scenarios included: CM-21a, CM-21b, CM-21c, CM-21d
+
+Deadline countdown logic (reuse overdue pattern from taskHelpers):
+- `X days remaining` — neutral style (green)
+- `Due tomorrow` — warning style (amber)
+- `Due today` — warning style (amber)
+- `X days overdue` — error style (red)
+
+Applied in both CaseList (deadline column) and CaseDetail (deadline panel).
+
+---
+
+## EXCLUDED — V1 Requirements
+
+The following are explicitly excluded from this MVP change:
+
+| Req | Description | Reason |
+|-----|-------------|--------|
+| CM-08b | Full participant/role management | Needs role types infrastructure |
+| CM-09 | Custom properties panel | Needs property definition types |
+| CM-10 | Document checklist | Needs document type infrastructure |
+| CM-12 | Decisions section | Needs decision entity types |
+| CM-14c | Status blocked by properties | Depends on CM-09 |
+| CM-14d | Status blocked by documents | Depends on CM-10 |
+| CM-14e | Notification on status change | Backend notification system |
+| CM-17 | Case suspension | Complex deadline recalculation |
+| CM-18 | Sub-cases | Parent/child hierarchy |
+| CM-19 | Confidentiality override | Read-only from case type for MVP |
+| CM-22 | Nextcloud Activity audit trail | Backend Activity provider needed |
diff --git a/openspec/changes/archive/2026-02-26-case-management/tasks.md b/openspec/changes/archive/2026-02-26-case-management/tasks.md
index c2590078..51287902 100644
--- a/openspec/changes/archive/2026-02-26-case-management/tasks.md
+++ b/openspec/changes/archive/2026-02-26-case-management/tasks.md
@@ -1,64 +1,64 @@
-# Tasks: Case Management — MVP
-
-## Implementation Tasks
-
-### Utility Modules
-
-- [x] **T01**: Create `src/utils/caseHelpers.js` — Export functions: `calculateDeadline(startDate, durationString)` (adds ISO 8601 duration to date), `generateIdentifier()` (returns `YYYY-{timestamp_suffix}`), `isCaseOverdue(caseObj)` (deadline past + not at final status), `isCaseDueToday(caseObj, isFinal)`, `getCaseOverdueText(caseObj)`, `formatDeadlineCountdown(caseObj, isFinal)` (returns `{ text, style }` — "X days remaining"/"Due today"/"X days overdue"), `getDaysElapsed(startDate)`, `getDaysRemaining(deadline)`. Import `parseDuration` from `durationHelpers.js`. Use `isFinal` flag from status type (not CMMN terminal status).
-
-- [x] **T02**: Create `src/utils/caseValidation.js` — Export functions: `validateCaseCreate(form, caseTypes)` (returns `{ valid, errors }` — checks title required, case type required, case type published, case type within validity window), `validateCaseUpdate(form)` (title required), `isCaseTypeUsable(caseType)` (published + validFrom <= today + validUntil >= today or null). Case type validity checks: `isDraft === false`, `validFrom <= today`, `validUntil === null || validUntil >= today`.
-
-### New Components
-
-- [x] **T03**: Create `src/views/cases/CaseCreateDialog.vue` — Modal dialog with: case type dropdown (fetches published + valid case types), title text field, description textarea (optional). On case type selection, show preview panel: processing deadline (formatted duration), confidentiality level, initial status name, calculated deadline from today. On submit: validate via `caseValidation.js`, construct case object with auto-fields (identifier, startDate, deadline, status = first status type by order, confidentiality from case type, `statusHistory: [{ status, date, changedBy }]`, `activity: [{ date, type: "created", description, user }]`, extensionCount: 0), call `saveObject('case', data)`, emit `@created` with new case ID. Props: none. Events: `@created(caseId)`, `@close`.
-
-- [x] **T04**: Create `src/views/cases/components/StatusTimeline.vue` — Horizontal timeline of status dots. Props: `statusTypes` (array, ordered by `order`), `currentStatusId` (string), `statusHistory` (array of `{ status, date }`). Render: one dot per status type. Passed statuses (in statusHistory before current): filled dot, date label below. Current status: highlighted/larger dot, date label. Future statuses: greyed dot, no date. Use CSS flexbox with connecting lines between dots. Color: passed = `--color-success`, current = `--color-primary`, future = `--color-text-maxcontrast`.
-
-- [x] **T05**: Create `src/views/cases/components/DeadlinePanel.vue` — Info panel showing deadline data. Props: `startDate`, `deadline`, `processingDeadline` (duration string), `extensionAllowed` (bool), `extensionPeriod` (duration string), `extensionCount` (number), `isFinal` (bool). Display: "Started: {date}", "Deadline: {date}", countdown text with color coding (green/amber/red), "Processing time: {formatted duration}", "Days elapsed: {N}", "Extension: allowed (+{period})" or "not allowed". Show "Extend" button when `extensionAllowed && extensionCount === 0 && !isFinal`. Emit `@extend` when clicked.
-
-- [x] **T06**: Create `src/views/cases/components/ActivityTimeline.vue` — Reverse-chronological event list. Props: `activity` (array), `isReadOnly` (bool). Each entry shows: date (formatted), type icon, description, user. Entry types with icons: `created` (plus), `status_change` (arrow), `update` (pencil), `extension` (clock), `note` (comment). At the top: "Add note" text area + submit button (hidden when read-only). Emit `@add-note(text)`.
-
-- [x] **T07**: Create `src/views/cases/components/QuickStatusDropdown.vue` — Inline dropdown for status change in case list. Props: `caseObj` (the case object), `statusTypes` (array). Shows NcSelect/dropdown with status type names. Current status pre-selected. On selection change: update case status, append to statusHistory and activity, emit `@changed`. Uses `@click.stop` on root element to prevent row click.
-
-### Enhanced Views
-
-- [x] **T08**: Rewrite `src/views/cases/CaseList.vue` — Replace current minimal list with full case management list. Changes: (a) Add columns: Identifier, Title, Type (case type name), Status (badge with QuickStatusDropdown), Deadline (countdown with color), Handler. Remove "Created" column. (b) Add filters: case type dropdown (fetch all case types), status dropdown (dynamically populated from selected case type's status types, or all statuses if no type filter), handler text field (debounced), priority dropdown, overdue toggle checkbox. (c) Search: existing search works, ensure it uses `_search` param. (d) Sort: all column headers sortable via `_order` param. (e) "New case" button opens CaseCreateDialog instead of navigating to `#/cases/new`. (f) Overdue rows: red left border + red deadline text. (g) Load case types on mount for type column display and type filter. (h) Load status types for status badge display.
-
-- [x] **T09**: Rewrite `src/views/cases/CaseDetail.vue` — Replace current minimal detail with full case management detail. Changes: (a) Load case type + status types on mount. (b) Info panel section: title (editable), description (editable textarea), identifier (read-only), case type name (read-only link), priority dropdown (low/normal/high/urgent), confidentiality (read-only text), assignee (editable), start date (read-only). (c) Status bar: current status badge + status change dropdown (only statuses from case type) + result input (shown when transitioning to final status). (d) Integrate StatusTimeline component below status bar. (e) Integrate DeadlinePanel component. (f) Keep existing tasks section (from task-management). (g) Integrate ActivityTimeline component. (h) Read-only mode: when current status isFinal, disable all editable fields, hide status dropdown, hide save/delete buttons. (i) On status change: update status, append to statusHistory, append to activity, if final status set endDate + prompt for result. (j) On save: validate via caseValidation.js, append update activity entry, saveObject. (k) On delete: confirmation dialog with linked task count warning.
-
-### Extension Support
-
-- [x] **T10**: Add extension dialog in CaseDetail.vue — When "Extend" is emitted from DeadlinePanel: show confirmation dialog with reason text input. On confirm: calculate new deadline (current deadline + extensionPeriod via `calculateDeadline`), update case object (`deadline`, `extensionCount++`, append to activity), saveObject. If extensionCount >= 1 already, the Extend button is hidden (handled by DeadlinePanel props).
-
-### Case Type Integration Helpers
-
-- [x] **T11**: Add helper functions for case type data in CaseDetail.vue — Methods: `getStatusTypesForCase()` (fetch and cache status types for the case's case type, ordered by `order`), `getCurrentStatusType()` (find the status type matching current case status), `isAtFinalStatus()` (check if current status type has `isFinal === true`), `getInitialStatusType(statusTypes)` (return the one with lowest `order`). These feed into StatusTimeline, status dropdown, and read-only detection.
-
-### Result Recording
-
-- [x] **T12**: Add result prompt on final status transition in CaseDetail.vue — When user selects a final status from the dropdown: show inline text input "Result" below the dropdown. Text is required before confirming. On confirm: set `result` on case object, set `endDate` to today, update status + statusHistory + activity, saveObject.
-
-### Validation Integration
-
-- [x] **T13**: Wire validation into CaseCreateDialog and CaseDetail — CaseCreateDialog: call `validateCaseCreate()` before saving, display field-level errors. CaseDetail: call `validateCaseUpdate()` before saving, display title error. Both: show error messages below respective fields using `.form-error` styling.
-
-### Case List — Case Type Data Loading
-
-- [x] **T14**: Add case type and status type caching to CaseList.vue — On mount, fetch all case types via `fetchCollection('caseType', { _limit: 100 })`. Cache in local data for: type column display (resolve caseType ID to name), type filter dropdown options, status filter options (grouped by type). For each case in the list, resolve its `caseType` to a name using the cache. Fetch status types once to populate filter options.
-
-### Deadline Display in List
-
-- [x] **T15**: Add deadline countdown column to CaseList.vue — For each case row, compute deadline countdown using `formatDeadlineCountdown()` from `caseHelpers.js`. Need to know if case is at final status: check case's status against its case type's status types to find `isFinal`. Apply CSS classes: `deadline--overdue` (red), `deadline--today` (amber), `deadline--ok` (green/neutral).
-
-## Verification Tasks
-
-- [x] **V01**: All 7 new files created and syntactically valid
-- [ ] **V02**: CaseList renders with all 6 columns and 5+ filters
-- [ ] **V03**: CaseCreateDialog validates and auto-fills all required fields
-- [ ] **V04**: StatusTimeline shows correct passed/current/future states
-- [ ] **V05**: DeadlinePanel shows countdown and extension button correctly
-- [ ] **V06**: ActivityTimeline displays entries and supports adding notes
-- [ ] **V07**: Status change to final status prompts for result and sets endDate
-- [ ] **V08**: Quick status change in list updates inline
-- [ ] **V09**: Extension dialog calculates new deadline correctly
-- [x] **V10**: All tasks checked off
+# Tasks: Case Management — MVP
+
+## Implementation Tasks
+
+### Utility Modules
+
+- [x] **T01**: Create `src/utils/caseHelpers.js` — Export functions: `calculateDeadline(startDate, durationString)` (adds ISO 8601 duration to date), `generateIdentifier()` (returns `YYYY-{timestamp_suffix}`), `isCaseOverdue(caseObj)` (deadline past + not at final status), `isCaseDueToday(caseObj, isFinal)`, `getCaseOverdueText(caseObj)`, `formatDeadlineCountdown(caseObj, isFinal)` (returns `{ text, style }` — "X days remaining"/"Due today"/"X days overdue"), `getDaysElapsed(startDate)`, `getDaysRemaining(deadline)`. Import `parseDuration` from `durationHelpers.js`. Use `isFinal` flag from status type (not CMMN terminal status).
+
+- [x] **T02**: Create `src/utils/caseValidation.js` — Export functions: `validateCaseCreate(form, caseTypes)` (returns `{ valid, errors }` — checks title required, case type required, case type published, case type within validity window), `validateCaseUpdate(form)` (title required), `isCaseTypeUsable(caseType)` (published + validFrom <= today + validUntil >= today or null). Case type validity checks: `isDraft === false`, `validFrom <= today`, `validUntil === null || validUntil >= today`.
+
+### New Components
+
+- [x] **T03**: Create `src/views/cases/CaseCreateDialog.vue` — Modal dialog with: case type dropdown (fetches published + valid case types), title text field, description textarea (optional). On case type selection, show preview panel: processing deadline (formatted duration), confidentiality level, initial status name, calculated deadline from today. On submit: validate via `caseValidation.js`, construct case object with auto-fields (identifier, startDate, deadline, status = first status type by order, confidentiality from case type, `statusHistory: [{ status, date, changedBy }]`, `activity: [{ date, type: "created", description, user }]`, extensionCount: 0), call `saveObject('case', data)`, emit `@created` with new case ID. Props: none. Events: `@created(caseId)`, `@close`.
+
+- [x] **T04**: Create `src/views/cases/components/StatusTimeline.vue` — Horizontal timeline of status dots. Props: `statusTypes` (array, ordered by `order`), `currentStatusId` (string), `statusHistory` (array of `{ status, date }`). Render: one dot per status type. Passed statuses (in statusHistory before current): filled dot, date label below. Current status: highlighted/larger dot, date label. Future statuses: greyed dot, no date. Use CSS flexbox with connecting lines between dots. Color: passed = `--color-success`, current = `--color-primary`, future = `--color-text-maxcontrast`.
+
+- [x] **T05**: Create `src/views/cases/components/DeadlinePanel.vue` — Info panel showing deadline data. Props: `startDate`, `deadline`, `processingDeadline` (duration string), `extensionAllowed` (bool), `extensionPeriod` (duration string), `extensionCount` (number), `isFinal` (bool). Display: "Started: {date}", "Deadline: {date}", countdown text with color coding (green/amber/red), "Processing time: {formatted duration}", "Days elapsed: {N}", "Extension: allowed (+{period})" or "not allowed". Show "Extend" button when `extensionAllowed && extensionCount === 0 && !isFinal`. Emit `@extend` when clicked.
+
+- [x] **T06**: Create `src/views/cases/components/ActivityTimeline.vue` — Reverse-chronological event list. Props: `activity` (array), `isReadOnly` (bool). Each entry shows: date (formatted), type icon, description, user. Entry types with icons: `created` (plus), `status_change` (arrow), `update` (pencil), `extension` (clock), `note` (comment). At the top: "Add note" text area + submit button (hidden when read-only). Emit `@add-note(text)`.
+
+- [x] **T07**: Create `src/views/cases/components/QuickStatusDropdown.vue` — Inline dropdown for status change in case list. Props: `caseObj` (the case object), `statusTypes` (array). Shows NcSelect/dropdown with status type names. Current status pre-selected. On selection change: update case status, append to statusHistory and activity, emit `@changed`. Uses `@click.stop` on root element to prevent row click.
+
+### Enhanced Views
+
+- [x] **T08**: Rewrite `src/views/cases/CaseList.vue` — Replace current minimal list with full case management list. Changes: (a) Add columns: Identifier, Title, Type (case type name), Status (badge with QuickStatusDropdown), Deadline (countdown with color), Handler. Remove "Created" column. (b) Add filters: case type dropdown (fetch all case types), status dropdown (dynamically populated from selected case type's status types, or all statuses if no type filter), handler text field (debounced), priority dropdown, overdue toggle checkbox. (c) Search: existing search works, ensure it uses `_search` param. (d) Sort: all column headers sortable via `_order` param. (e) "New case" button opens CaseCreateDialog instead of navigating to `#/cases/new`. (f) Overdue rows: red left border + red deadline text. (g) Load case types on mount for type column display and type filter. (h) Load status types for status badge display.
+
+- [x] **T09**: Rewrite `src/views/cases/CaseDetail.vue` — Replace current minimal detail with full case management detail. Changes: (a) Load case type + status types on mount. (b) Info panel section: title (editable), description (editable textarea), identifier (read-only), case type name (read-only link), priority dropdown (low/normal/high/urgent), confidentiality (read-only text), assignee (editable), start date (read-only). (c) Status bar: current status badge + status change dropdown (only statuses from case type) + result input (shown when transitioning to final status). (d) Integrate StatusTimeline component below status bar. (e) Integrate DeadlinePanel component. (f) Keep existing tasks section (from task-management). (g) Integrate ActivityTimeline component. (h) Read-only mode: when current status isFinal, disable all editable fields, hide status dropdown, hide save/delete buttons. (i) On status change: update status, append to statusHistory, append to activity, if final status set endDate + prompt for result. (j) On save: validate via caseValidation.js, append update activity entry, saveObject. (k) On delete: confirmation dialog with linked task count warning.
+
+### Extension Support
+
+- [x] **T10**: Add extension dialog in CaseDetail.vue — When "Extend" is emitted from DeadlinePanel: show confirmation dialog with reason text input. On confirm: calculate new deadline (current deadline + extensionPeriod via `calculateDeadline`), update case object (`deadline`, `extensionCount++`, append to activity), saveObject. If extensionCount >= 1 already, the Extend button is hidden (handled by DeadlinePanel props).
+
+### Case Type Integration Helpers
+
+- [x] **T11**: Add helper functions for case type data in CaseDetail.vue — Methods: `getStatusTypesForCase()` (fetch and cache status types for the case's case type, ordered by `order`), `getCurrentStatusType()` (find the status type matching current case status), `isAtFinalStatus()` (check if current status type has `isFinal === true`), `getInitialStatusType(statusTypes)` (return the one with lowest `order`). These feed into StatusTimeline, status dropdown, and read-only detection.
+
+### Result Recording
+
+- [x] **T12**: Add result prompt on final status transition in CaseDetail.vue — When user selects a final status from the dropdown: show inline text input "Result" below the dropdown. Text is required before confirming. On confirm: set `result` on case object, set `endDate` to today, update status + statusHistory + activity, saveObject.
+
+### Validation Integration
+
+- [x] **T13**: Wire validation into CaseCreateDialog and CaseDetail — CaseCreateDialog: call `validateCaseCreate()` before saving, display field-level errors. CaseDetail: call `validateCaseUpdate()` before saving, display title error. Both: show error messages below respective fields using `.form-error` styling.
+
+### Case List — Case Type Data Loading
+
+- [x] **T14**: Add case type and status type caching to CaseList.vue — On mount, fetch all case types via `fetchCollection('caseType', { _limit: 100 })`. Cache in local data for: type column display (resolve caseType ID to name), type filter dropdown options, status filter options (grouped by type). For each case in the list, resolve its `caseType` to a name using the cache. Fetch status types once to populate filter options.
+
+### Deadline Display in List
+
+- [x] **T15**: Add deadline countdown column to CaseList.vue — For each case row, compute deadline countdown using `formatDeadlineCountdown()` from `caseHelpers.js`. Need to know if case is at final status: check case's status against its case type's status types to find `isFinal`. Apply CSS classes: `deadline--overdue` (red), `deadline--today` (amber), `deadline--ok` (green/neutral).
+
+## Verification Tasks
+
+- [x] **V01**: All 7 new files created and syntactically valid
+- [ ] **V02**: CaseList renders with all 6 columns and 5+ filters
+- [ ] **V03**: CaseCreateDialog validates and auto-fills all required fields
+- [ ] **V04**: StatusTimeline shows correct passed/current/future states
+- [ ] **V05**: DeadlinePanel shows countdown and extension button correctly
+- [ ] **V06**: ActivityTimeline displays entries and supports adding notes
+- [ ] **V07**: Status change to final status prompts for result and sets endDate
+- [ ] **V08**: Quick status change in list updates inline
+- [ ] **V09**: Extension dialog calculates new deadline correctly
+- [x] **V10**: All tasks checked off
diff --git a/openspec/changes/archive/2026-02-26-case-types/.openspec.yaml b/openspec/changes/archive/2026-02-26-case-types/.openspec.yaml
index da0b8b26..0a692ea1 100644
--- a/openspec/changes/archive/2026-02-26-case-types/.openspec.yaml
+++ b/openspec/changes/archive/2026-02-26-case-types/.openspec.yaml
@@ -1,5 +1,5 @@
-change: case-types
-project: procest
-schema: conduction
-created: 2026-02-25
-status: drafting
+change: case-types
+project: procest
+schema: conduction
+created: 2026-02-25
+status: drafting
diff --git a/openspec/changes/archive/2026-02-26-case-types/design.md b/openspec/changes/archive/2026-02-26-case-types/design.md
index a169fa62..6fb038c3 100644
--- a/openspec/changes/archive/2026-02-26-case-types/design.md
+++ b/openspec/changes/archive/2026-02-26-case-types/design.md
@@ -1,124 +1,124 @@
-# Design: case-types
-
-## Architecture Overview
-
-Case type management lives in the Nextcloud **admin settings** panel, rendered by a separate webpack entry point (`settings.js`). The current Settings.vue (register/schema config) will be preserved and the case type management UI will be added alongside it. All data flows through the existing `useObjectStore` Pinia store to OpenRegister.
-
-```
-Admin Settings Page (settings.js entry point)
-├── Settings.vue (existing config — register/schema IDs)
-└── CaseTypeAdmin.vue (NEW — case type management)
- ├── CaseTypeList.vue (list with search, badges, default star)
- └── CaseTypeDetail.vue (tabbed editor)
- ├── GeneralTab.vue (all case type fields)
- └── StatusesTab.vue (ordered status type list + CRUD)
-```
-
-Data flow:
-```
-Vue Components → useObjectStore → OpenRegister API
- ├── /api/objects/{register}/caseType/{id}
- └── /api/objects/{register}/statusType/{id}
-```
-
-## API Design
-
-No new backend APIs. All CRUD uses the existing OpenRegister API through `useObjectStore`:
-
-- `fetchCollection('caseType', params)` — List case types
-- `fetchObject('caseType', id)` — Get single case type
-- `saveObject('caseType', data)` — Create (POST) or update (PUT)
-- `deleteObject('caseType', id)` — Delete case type
-- Same pattern for `statusType` with `_filters[caseType]={id}` for scoped queries
-
-## Database Changes
-
-None. Case types and status types are stored as OpenRegister objects.
-
-**New app config keys** (added to `SettingsService.php`):
-- `case_type_schema` — Schema ID for the `caseType` OpenRegister schema
-- `status_type_schema` — Schema ID for the `statusType` OpenRegister schema
-
-## Nextcloud Integration
-
-- **Settings**: Existing `AdminSettings.php` + `SettingsSection.php` — no changes needed, already renders `templates/settings/admin.php`
-- **Entry point**: `settings.js` already exists as webpack `adminSettings` entry — will be enhanced to mount the new admin root component
-
-## File Structure
-
-```
-src/
- settings.js (MODIFIED — mount AdminRoot instead of bare Settings)
- views/
- settings/
- Settings.vue (EXISTING — register/schema config, no changes)
- AdminRoot.vue (NEW — root component, renders Settings + CaseTypeAdmin)
- CaseTypeAdmin.vue (NEW — list/detail router for case types)
- CaseTypeList.vue (NEW — case type list with badges)
- CaseTypeDetail.vue (NEW — tabbed editor)
- tabs/
- GeneralTab.vue (NEW — all case type fields)
- StatusesTab.vue (NEW — ordered status type CRUD)
- utils/
- durationHelpers.js (NEW — ISO 8601 parse/format/validate)
- caseTypeValidation.js (NEW — publish validation, field validation)
- store/
- store.js (MODIFIED — register caseType + statusType)
-lib/
- Service/
- SettingsService.php (MODIFIED — add case_type_schema, status_type_schema keys)
-```
-
-## Decisions
-
-### 1. Admin UI architecture: Single root component with inline routing
-
-**Decision**: Create an `AdminRoot.vue` that manages view state (list vs detail) internally, similar to how `App.vue` handles the main app routing. The settings.js entry point mounts AdminRoot, which renders both the existing Settings config and the CaseTypeAdmin.
-
-**Why not vue-router**: The admin settings page is a small surface. Hash routing would conflict with Nextcloud's own admin panel. A simple `currentView`/`currentId` data-driven approach (same as App.vue) keeps it simple.
-
-### 2. Status type reordering: Save on drop
-
-**Decision**: When a status type is dragged to a new position, immediately recalculate all `order` values and save each affected status type via `saveObject`. No separate "save order" button.
-
-**Why**: Matches user expectation from drag-and-drop UIs. The order must be persisted immediately so refreshing the page preserves the new order. Saving only changed items keeps API calls minimal.
-
-**Implementation**: Use HTML5 drag-and-drop (no external library) since we only need simple vertical reordering within a small list.
-
-### 3. Publish validation: Frontend-only
-
-**Decision**: Validate publish prerequisites (required fields, >=1 status type, >=1 final status, validFrom set) in the frontend before calling `saveObject`. No backend validation endpoint.
-
-**Why**: OpenRegister doesn't enforce business rules — it's a generic object store. All validation logic lives in `caseTypeValidation.js`. This is consistent with how task management does validation (frontend-side in TaskDetail.vue).
-
-### 4. Default case type: Stored in app config
-
-**Decision**: Store the default case type ID in Nextcloud's `IAppConfig` via the existing settings API (`default_case_type` key), not as a field on the case type object.
-
-**Why**: Only one type can be default at a time. Storing it as app config avoids having to query all case types to find which one has `isDefault=true`. Simple read from settings.
-
-### 5. Identifier auto-generation: Frontend timestamp
-
-**Decision**: Generate `identifier` as `CT-{Date.now()}` in the frontend when creating a new case type.
-
-**Why**: Simple, unique enough for this context. OpenRegister assigns its own UUID as the object ID. The `identifier` is a human-readable reference, not the primary key.
-
-### 6. Delete with cascade: Sequential deletes
-
-**Decision**: When deleting a case type, first fetch and delete all linked status types (`_filters[caseType]={id}`), then delete the case type itself.
-
-**Why**: OpenRegister doesn't support cascade deletes. The frontend must handle this explicitly. Status types are the only child entity in MVP scope.
-
-## Risks / Trade-offs
-
-- **[No server-side validation]** → All validation is in the frontend. A direct API call could create invalid case types. Acceptable for MVP since only admins manage case types.
-- **[Drag-and-drop without library]** → HTML5 drag-and-drop can be finicky on touch devices. Status type lists are typically small (5-10 items), so this is acceptable. Could add a library in V1 if needed.
-- **[Sequential cascade deletes]** → If a case type has many status types, deletion involves N+1 API calls. Acceptable for MVP since status type counts are small.
-
-## Migration Plan
-
-No migration needed. This is a new feature. The admin must configure `case_type_schema` and `status_type_schema` in settings before case types can be managed.
-
-## Open Questions
-
-None.
+# Design: case-types
+
+## Architecture Overview
+
+Case type management lives in the Nextcloud **admin settings** panel, rendered by a separate webpack entry point (`settings.js`). The current Settings.vue (register/schema config) will be preserved and the case type management UI will be added alongside it. All data flows through the existing `useObjectStore` Pinia store to OpenRegister.
+
+```
+Admin Settings Page (settings.js entry point)
+├── Settings.vue (existing config — register/schema IDs)
+└── CaseTypeAdmin.vue (NEW — case type management)
+ ├── CaseTypeList.vue (list with search, badges, default star)
+ └── CaseTypeDetail.vue (tabbed editor)
+ ├── GeneralTab.vue (all case type fields)
+ └── StatusesTab.vue (ordered status type list + CRUD)
+```
+
+Data flow:
+```
+Vue Components → useObjectStore → OpenRegister API
+ ├── /api/objects/{register}/caseType/{id}
+ └── /api/objects/{register}/statusType/{id}
+```
+
+## API Design
+
+No new backend APIs. All CRUD uses the existing OpenRegister API through `useObjectStore`:
+
+- `fetchCollection('caseType', params)` — List case types
+- `fetchObject('caseType', id)` — Get single case type
+- `saveObject('caseType', data)` — Create (POST) or update (PUT)
+- `deleteObject('caseType', id)` — Delete case type
+- Same pattern for `statusType` with `_filters[caseType]={id}` for scoped queries
+
+## Database Changes
+
+None. Case types and status types are stored as OpenRegister objects.
+
+**New app config keys** (added to `SettingsService.php`):
+- `case_type_schema` — Schema ID for the `caseType` OpenRegister schema
+- `status_type_schema` — Schema ID for the `statusType` OpenRegister schema
+
+## Nextcloud Integration
+
+- **Settings**: Existing `AdminSettings.php` + `SettingsSection.php` — no changes needed, already renders `templates/settings/admin.php`
+- **Entry point**: `settings.js` already exists as webpack `adminSettings` entry — will be enhanced to mount the new admin root component
+
+## File Structure
+
+```
+src/
+ settings.js (MODIFIED — mount AdminRoot instead of bare Settings)
+ views/
+ settings/
+ Settings.vue (EXISTING — register/schema config, no changes)
+ AdminRoot.vue (NEW — root component, renders Settings + CaseTypeAdmin)
+ CaseTypeAdmin.vue (NEW — list/detail router for case types)
+ CaseTypeList.vue (NEW — case type list with badges)
+ CaseTypeDetail.vue (NEW — tabbed editor)
+ tabs/
+ GeneralTab.vue (NEW — all case type fields)
+ StatusesTab.vue (NEW — ordered status type CRUD)
+ utils/
+ durationHelpers.js (NEW — ISO 8601 parse/format/validate)
+ caseTypeValidation.js (NEW — publish validation, field validation)
+ store/
+ store.js (MODIFIED — register caseType + statusType)
+lib/
+ Service/
+ SettingsService.php (MODIFIED — add case_type_schema, status_type_schema keys)
+```
+
+## Decisions
+
+### 1. Admin UI architecture: Single root component with inline routing
+
+**Decision**: Create an `AdminRoot.vue` that manages view state (list vs detail) internally, similar to how `App.vue` handles the main app routing. The settings.js entry point mounts AdminRoot, which renders both the existing Settings config and the CaseTypeAdmin.
+
+**Why not vue-router**: The admin settings page is a small surface. Hash routing would conflict with Nextcloud's own admin panel. A simple `currentView`/`currentId` data-driven approach (same as App.vue) keeps it simple.
+
+### 2. Status type reordering: Save on drop
+
+**Decision**: When a status type is dragged to a new position, immediately recalculate all `order` values and save each affected status type via `saveObject`. No separate "save order" button.
+
+**Why**: Matches user expectation from drag-and-drop UIs. The order must be persisted immediately so refreshing the page preserves the new order. Saving only changed items keeps API calls minimal.
+
+**Implementation**: Use HTML5 drag-and-drop (no external library) since we only need simple vertical reordering within a small list.
+
+### 3. Publish validation: Frontend-only
+
+**Decision**: Validate publish prerequisites (required fields, >=1 status type, >=1 final status, validFrom set) in the frontend before calling `saveObject`. No backend validation endpoint.
+
+**Why**: OpenRegister doesn't enforce business rules — it's a generic object store. All validation logic lives in `caseTypeValidation.js`. This is consistent with how task management does validation (frontend-side in TaskDetail.vue).
+
+### 4. Default case type: Stored in app config
+
+**Decision**: Store the default case type ID in Nextcloud's `IAppConfig` via the existing settings API (`default_case_type` key), not as a field on the case type object.
+
+**Why**: Only one type can be default at a time. Storing it as app config avoids having to query all case types to find which one has `isDefault=true`. Simple read from settings.
+
+### 5. Identifier auto-generation: Frontend timestamp
+
+**Decision**: Generate `identifier` as `CT-{Date.now()}` in the frontend when creating a new case type.
+
+**Why**: Simple, unique enough for this context. OpenRegister assigns its own UUID as the object ID. The `identifier` is a human-readable reference, not the primary key.
+
+### 6. Delete with cascade: Sequential deletes
+
+**Decision**: When deleting a case type, first fetch and delete all linked status types (`_filters[caseType]={id}`), then delete the case type itself.
+
+**Why**: OpenRegister doesn't support cascade deletes. The frontend must handle this explicitly. Status types are the only child entity in MVP scope.
+
+## Risks / Trade-offs
+
+- **[No server-side validation]** → All validation is in the frontend. A direct API call could create invalid case types. Acceptable for MVP since only admins manage case types.
+- **[Drag-and-drop without library]** → HTML5 drag-and-drop can be finicky on touch devices. Status type lists are typically small (5-10 items), so this is acceptable. Could add a library in V1 if needed.
+- **[Sequential cascade deletes]** → If a case type has many status types, deletion involves N+1 API calls. Acceptable for MVP since status type counts are small.
+
+## Migration Plan
+
+No migration needed. This is a new feature. The admin must configure `case_type_schema` and `status_type_schema` in settings before case types can be managed.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/archive/2026-02-26-case-types/proposal.md b/openspec/changes/archive/2026-02-26-case-types/proposal.md
index 7797a8f7..2bc2696f 100644
--- a/openspec/changes/archive/2026-02-26-case-types/proposal.md
+++ b/openspec/changes/archive/2026-02-26-case-types/proposal.md
@@ -1,62 +1,62 @@
-# Proposal: case-types
-
-## Summary
-
-Implement the MVP tier of the case type system for Procest. Case types are configurable definitions that control case behavior — allowed statuses, processing deadlines, extension rules, and validation. This is the admin-facing configuration backbone that all case management features build on.
-
-## Motivation
-
-Procest currently has hardcoded case statuses and no configurable case type system. Without case types, every case behaves identically — same statuses, no deadline enforcement, no validation rules. The case-types spec (REQ-CT-01 through REQ-CT-16) defines a full type system modeled after CMMN 1.1 `CaseDefinition` and ZGW `ZaakType`. Implementing the MVP tier unlocks configurable case lifecycles, deadline calculation, and publish/draft workflows.
-
-## Affected Projects
-
-- [ ] Project: `procest` — New admin settings UI for case type management, new utility modules for case type logic, enhanced App.vue routing for admin views
-
-## Scope
-
-### In Scope (MVP)
-
-- **Case Type CRUD** (REQ-CT-01): Create, read, update, delete case types as OpenRegister objects
-- **Draft/Published lifecycle** (REQ-CT-02): Case types default to draft, must meet requirements before publishing
-- **Validity periods** (REQ-CT-03): ValidFrom/ValidUntil windows controlling when types are usable
-- **Status Type management** (REQ-CT-04): Ordered status types per case type with drag reordering, final status enforcement
-- **Processing deadline config** (REQ-CT-05): ISO 8601 duration fields with human-readable display
-- **Extension configuration** (REQ-CT-06 MVP): Extension allowed/period fields with conditional validation
-- **Default case type** (REQ-CT-13): Admin can set one published type as default
-- **Validation rules** (REQ-CT-14): Required fields, ISO 8601 format, date ordering
-- **Admin UI tabs** (REQ-CT-15 MVP): General and Statuses tabs in case type editor
-- **Error scenarios** (REQ-CT-16): Graceful handling of publish blockers, delete blockers, duplicate orders
-
-### Out of Scope (V1)
-
-- Result Type management (REQ-CT-07)
-- Role Type management (REQ-CT-08)
-- Property Definition management (REQ-CT-09)
-- Document Type management (REQ-CT-10)
-- Decision Type management (REQ-CT-11)
-- Confidentiality defaults (REQ-CT-12)
-- Suspension configuration (REQ-CT-06d/e)
-- V1 tabs: Results, Roles, Properties, Docs (REQ-CT-15d-g)
-
-## Approach
-
-Frontend-only implementation using the existing `useObjectStore` Pinia store to CRUD case types and status types via the OpenRegister API. New admin settings components will be registered in the Nextcloud admin panel. The existing `caseType` and `statusType` object types are already registered in the store initialization (`store.js`), so the data layer is ready.
-
-Key architectural decisions:
-1. **Admin UI in Nextcloud settings panel** — Separate Vue entry point (`settings.js`) rendering into the admin settings template
-2. **Tab-based editor** — Case type detail page with General + Statuses tabs (V1 adds more tabs)
-3. **ISO 8601 duration helpers** — New utility module for parsing/formatting durations
-4. **Publish validation** — Frontend-side validation before setting `isDraft = false`
-
-## Cross-Project Dependencies
-
-- **OpenRegister**: Must have `caseType` and `statusType` schemas registered in the `procest` register
-- **Case Management**: Will consume case types once available (future change — not blocked by this)
-
-## Rollback Strategy
-
-All changes are frontend-only (Vue components + utilities). Rollback by reverting the source files. No database migrations or schema changes required.
-
-## Open Questions
-
-None — the spec is comprehensive and the existing codebase patterns are well-established.
+# Proposal: case-types
+
+## Summary
+
+Implement the MVP tier of the case type system for Procest. Case types are configurable definitions that control case behavior — allowed statuses, processing deadlines, extension rules, and validation. This is the admin-facing configuration backbone that all case management features build on.
+
+## Motivation
+
+Procest currently has hardcoded case statuses and no configurable case type system. Without case types, every case behaves identically — same statuses, no deadline enforcement, no validation rules. The case-types spec (REQ-CT-01 through REQ-CT-16) defines a full type system modeled after CMMN 1.1 `CaseDefinition` and ZGW `ZaakType`. Implementing the MVP tier unlocks configurable case lifecycles, deadline calculation, and publish/draft workflows.
+
+## Affected Projects
+
+- [ ] Project: `procest` — New admin settings UI for case type management, new utility modules for case type logic, enhanced App.vue routing for admin views
+
+## Scope
+
+### In Scope (MVP)
+
+- **Case Type CRUD** (REQ-CT-01): Create, read, update, delete case types as OpenRegister objects
+- **Draft/Published lifecycle** (REQ-CT-02): Case types default to draft, must meet requirements before publishing
+- **Validity periods** (REQ-CT-03): ValidFrom/ValidUntil windows controlling when types are usable
+- **Status Type management** (REQ-CT-04): Ordered status types per case type with drag reordering, final status enforcement
+- **Processing deadline config** (REQ-CT-05): ISO 8601 duration fields with human-readable display
+- **Extension configuration** (REQ-CT-06 MVP): Extension allowed/period fields with conditional validation
+- **Default case type** (REQ-CT-13): Admin can set one published type as default
+- **Validation rules** (REQ-CT-14): Required fields, ISO 8601 format, date ordering
+- **Admin UI tabs** (REQ-CT-15 MVP): General and Statuses tabs in case type editor
+- **Error scenarios** (REQ-CT-16): Graceful handling of publish blockers, delete blockers, duplicate orders
+
+### Out of Scope (V1)
+
+- Result Type management (REQ-CT-07)
+- Role Type management (REQ-CT-08)
+- Property Definition management (REQ-CT-09)
+- Document Type management (REQ-CT-10)
+- Decision Type management (REQ-CT-11)
+- Confidentiality defaults (REQ-CT-12)
+- Suspension configuration (REQ-CT-06d/e)
+- V1 tabs: Results, Roles, Properties, Docs (REQ-CT-15d-g)
+
+## Approach
+
+Frontend-only implementation using the existing `useObjectStore` Pinia store to CRUD case types and status types via the OpenRegister API. New admin settings components will be registered in the Nextcloud admin panel. The existing `caseType` and `statusType` object types are already registered in the store initialization (`store.js`), so the data layer is ready.
+
+Key architectural decisions:
+1. **Admin UI in Nextcloud settings panel** — Separate Vue entry point (`settings.js`) rendering into the admin settings template
+2. **Tab-based editor** — Case type detail page with General + Statuses tabs (V1 adds more tabs)
+3. **ISO 8601 duration helpers** — New utility module for parsing/formatting durations
+4. **Publish validation** — Frontend-side validation before setting `isDraft = false`
+
+## Cross-Project Dependencies
+
+- **OpenRegister**: Must have `caseType` and `statusType` schemas registered in the `procest` register
+- **Case Management**: Will consume case types once available (future change — not blocked by this)
+
+## Rollback Strategy
+
+All changes are frontend-only (Vue components + utilities). Rollback by reverting the source files. No database migrations or schema changes required.
+
+## Open Questions
+
+None — the spec is comprehensive and the existing codebase patterns are well-established.
diff --git a/openspec/changes/archive/2026-02-26-case-types/review.md b/openspec/changes/archive/2026-02-26-case-types/review.md
index 3ef15258..e654aa62 100644
--- a/openspec/changes/archive/2026-02-26-case-types/review.md
+++ b/openspec/changes/archive/2026-02-26-case-types/review.md
@@ -1,51 +1,51 @@
-# Review: case-types
-
-## Summary
-- Tasks completed: 15/15
-- GitHub issues: N/A (no plan.json)
-- Spec compliance: **PASS with warnings**
-
-## Requirements Verified
-
-| Requirement | Status |
-|---|---|
-| REQ-CT-01 Case Type CRUD | PASS |
-| REQ-CT-02 Draft/Published Lifecycle | PASS |
-| REQ-CT-03 Validity Periods | WARNING |
-| REQ-CT-04 Status Type Management | WARNING |
-| REQ-CT-05 Processing Deadline | PASS |
-| REQ-CT-06 Extension Configuration | PASS |
-| REQ-CT-13 Default Case Type | PASS |
-| REQ-CT-14 Validation Rules | PASS |
-| REQ-CT-15 Admin UI Tabs | PASS |
-| REQ-CT-16 Error Scenarios | PASS |
-
-## Findings
-
-### CRITICAL
-
-None.
-
-### WARNING
-
-- [ ] **CT-03b: Missing explicit "Expired" text indicator** (spec_ref: REQ-CT-03)
- Expired case types in the list only get red-colored text on the validity date range (`validity--expired` CSS class). The spec says "MUST show an 'Expired' indicator." An explicit "Expired" badge or label should be added alongside the date styling for accessibility and clarity.
- File: `src/views/settings/CaseTypeList.vue` lines 51-53, `validityClass()` method
-
-- [ ] **CT-04e: No active-case check before status type deletion** (spec_ref: REQ-CT-04)
- `StatusesTab.vue` `deleteStatusType()` does not check whether active cases reference a status type before allowing deletion. The spec says the system MUST show "Cannot delete: active cases are at this status." Currently untriggerable since case management is not yet built, but the guard should exist for when cases are implemented.
- File: `src/views/settings/tabs/StatusesTab.vue` lines 341-363
-
-- [ ] **`validFrom` label marked required (*) but not validated on save** (spec_ref: REQ-CT-14)
- The "Valid from" label has `class="required"` (showing asterisk) but `validFrom` is not in `REQUIRED_FIELDS` and is not validated by `validateCaseType()`. It is only enforced on publish via `validateForPublish()`. This mismatch may confuse admins who see a required indicator but can save without it.
- File: `src/views/settings/tabs/GeneralTab.vue` line 177, `src/utils/caseTypeValidation.js` REQUIRED_FIELDS
-
-### SUGGESTION
-
-- CaseTypeList.vue and CaseTypeDetail.vue use native `confirm()` dialogs instead of `NcDialog`. Using NcDialog would provide a more consistent Nextcloud UX.
-- `CaseTypeList.vue:loadStatusTypeCount()` re-fetches the entire caseType collection after each status type count fetch (line 142), creating an N+1 query pattern. Consider batching or caching.
-- "Set as default" button is hidden for draft types via `v-if="!ct.isDraft"`. The spec implies showing an error when a draft is set as default, suggesting the button should be visible but error on click for drafts.
-
-## Recommendation
-
-**APPROVE** — 0 critical findings. The 3 warnings are low-risk: the expired indicator is cosmetic, the in-use check guards against a scenario that can't yet occur, and the validFrom asterisk mismatch is a UX inconsistency. Safe to archive or fix warnings first at your discretion.
+# Review: case-types
+
+## Summary
+- Tasks completed: 15/15
+- GitHub issues: N/A (no plan.json)
+- Spec compliance: **PASS with warnings**
+
+## Requirements Verified
+
+| Requirement | Status |
+|---|---|
+| REQ-CT-01 Case Type CRUD | PASS |
+| REQ-CT-02 Draft/Published Lifecycle | PASS |
+| REQ-CT-03 Validity Periods | WARNING |
+| REQ-CT-04 Status Type Management | WARNING |
+| REQ-CT-05 Processing Deadline | PASS |
+| REQ-CT-06 Extension Configuration | PASS |
+| REQ-CT-13 Default Case Type | PASS |
+| REQ-CT-14 Validation Rules | PASS |
+| REQ-CT-15 Admin UI Tabs | PASS |
+| REQ-CT-16 Error Scenarios | PASS |
+
+## Findings
+
+### CRITICAL
+
+None.
+
+### WARNING
+
+- [ ] **CT-03b: Missing explicit "Expired" text indicator** (spec_ref: REQ-CT-03)
+ Expired case types in the list only get red-colored text on the validity date range (`validity--expired` CSS class). The spec says "MUST show an 'Expired' indicator." An explicit "Expired" badge or label should be added alongside the date styling for accessibility and clarity.
+ File: `src/views/settings/CaseTypeList.vue` lines 51-53, `validityClass()` method
+
+- [ ] **CT-04e: No active-case check before status type deletion** (spec_ref: REQ-CT-04)
+ `StatusesTab.vue` `deleteStatusType()` does not check whether active cases reference a status type before allowing deletion. The spec says the system MUST show "Cannot delete: active cases are at this status." Currently untriggerable since case management is not yet built, but the guard should exist for when cases are implemented.
+ File: `src/views/settings/tabs/StatusesTab.vue` lines 341-363
+
+- [ ] **`validFrom` label marked required (*) but not validated on save** (spec_ref: REQ-CT-14)
+ The "Valid from" label has `class="required"` (showing asterisk) but `validFrom` is not in `REQUIRED_FIELDS` and is not validated by `validateCaseType()`. It is only enforced on publish via `validateForPublish()`. This mismatch may confuse admins who see a required indicator but can save without it.
+ File: `src/views/settings/tabs/GeneralTab.vue` line 177, `src/utils/caseTypeValidation.js` REQUIRED_FIELDS
+
+### SUGGESTION
+
+- CaseTypeList.vue and CaseTypeDetail.vue use native `confirm()` dialogs instead of `NcDialog`. Using NcDialog would provide a more consistent Nextcloud UX.
+- `CaseTypeList.vue:loadStatusTypeCount()` re-fetches the entire caseType collection after each status type count fetch (line 142), creating an N+1 query pattern. Consider batching or caching.
+- "Set as default" button is hidden for draft types via `v-if="!ct.isDraft"`. The spec implies showing an error when a draft is set as default, suggesting the button should be visible but error on click for drafts.
+
+## Recommendation
+
+**APPROVE** — 0 critical findings. The 3 warnings are low-risk: the expired indicator is cosmetic, the in-use check guards against a scenario that can't yet occur, and the validFrom asterisk mismatch is a UX inconsistency. Safe to archive or fix warnings first at your discretion.
diff --git a/openspec/changes/archive/2026-02-26-case-types/specs/case-types/spec.md b/openspec/changes/archive/2026-02-26-case-types/specs/case-types/spec.md
index 200d072d..74cd630b 100644
--- a/openspec/changes/archive/2026-02-26-case-types/specs/case-types/spec.md
+++ b/openspec/changes/archive/2026-02-26-case-types/specs/case-types/spec.md
@@ -1,279 +1,279 @@
-# Case Types (MVP) — Delta Specification
-
-## Purpose
-
-Implement the MVP tier of the case type system. This delta spec scopes the existing `case-types/spec.md` requirements to what will be built in this change: core CRUD, draft/publish lifecycle, validity periods, status type management, deadline configuration, extension config, default type, validation, General+Statuses tabs, and error scenarios.
-
-## ADDED Requirements
-
-_No new requirements added. All requirements below reference existing spec requirements scoped to MVP tier._
-
-## MODIFIED Requirements
-
-### Requirement: REQ-CT-01 Case Type CRUD (MVP scope)
-
-The system MUST support creating, reading, updating, and deleting case types. Case types are managed by admins via the Nextcloud admin settings page. This MVP implements scenarios CT-01a through CT-01e from the main spec.
-
-#### Scenario: CT-01a Create a case type
-
-- GIVEN an admin on the Procest admin settings page
-- WHEN they click "Add Case Type" and fill in the required fields (title, purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit, validFrom)
-- AND submit the form
-- THEN the system MUST create an OpenRegister object in the `procest` register with the `caseType` schema
-- AND `isDraft` MUST default to `true`
-- AND a unique `identifier` MUST be auto-generated (format: `CT-{timestamp}`)
-
-#### Scenario: CT-01b Update a case type
-
-- GIVEN an existing case type "Omgevingsvergunning"
-- WHEN the admin changes the `processingDeadline` from "P56D" to "P42D" and saves
-- THEN the system MUST update the OpenRegister object via PUT with the full object
-
-#### Scenario: CT-01c Delete a case type with no active cases
-
-- GIVEN a case type with no cases referencing it
-- WHEN the admin clicks "Delete" and confirms
-- THEN the system MUST delete the case type
-- AND all linked status types MUST also be deleted
-
-#### Scenario: CT-01d Prevent deletion of case type with active cases
-
-- GIVEN a case type referenced by active cases
-- WHEN the admin attempts to delete
-- THEN the system MUST show an error: "Cannot delete: active cases are using this type"
-
-#### Scenario: CT-01e Case type list display
-
-- GIVEN multiple case types exist
-- WHEN the admin views the case type list
-- THEN each type MUST display: title, status (Published/Draft badge), processing deadline, status type count, validity period
-- AND the default type MUST show a star indicator
-
-### Requirement: REQ-CT-02 Draft/Published Lifecycle (MVP scope)
-
-The system MUST support a draft/published lifecycle for case types. Draft case types MUST NOT be usable for creating cases.
-
-#### Scenario: CT-02a New case type defaults to draft
-
-- GIVEN an admin creating a new case type
-- WHEN the case type is saved
-- THEN `isDraft` MUST be `true`
-- AND the list MUST show a "DRAFT" badge
-
-#### Scenario: CT-02b Publish with valid configuration
-
-- GIVEN a draft case type with all required fields AND at least one status type with one final status AND validFrom set
-- WHEN the admin clicks "Publish"
-- THEN `isDraft` MUST be set to `false`
-- AND the type becomes available for creating cases
-
-#### Scenario: CT-02c Publish blocked — no status types
-
-- GIVEN a draft case type with no status types
-- WHEN the admin clicks "Publish"
-- THEN the system MUST show: "Cannot publish: at least one status type must be defined"
-
-#### Scenario: CT-02d Publish blocked — no final status
-
-- GIVEN a draft case type with status types but none marked `isFinal`
-- WHEN the admin clicks "Publish"
-- THEN the system MUST show: "Cannot publish: at least one status type must be marked as final"
-
-#### Scenario: CT-02e Publish blocked — no validFrom
-
-- GIVEN a draft case type without `validFrom`
-- WHEN the admin clicks "Publish"
-- THEN the system MUST show: "Cannot publish: 'Valid from' date must be set"
-
-#### Scenario: CT-02f Unpublish a case type
-
-- GIVEN a published case type
-- WHEN the admin clicks "Unpublish"
-- THEN the system MUST warn about impact on new case creation
-- AND if confirmed, revert to draft
-
-### Requirement: REQ-CT-03 Validity Periods (MVP scope)
-
-The system MUST support validity windows on case types.
-
-#### Scenario: CT-03a Valid type shown in admin list
-
-- GIVEN a case type with `validFrom` and optional `validUntil`
-- WHEN displayed in the admin list
-- THEN it MUST show the validity range (e.g., "Jan 2026 — Dec 2027" or "Jan 2026 — (no end)")
-
-#### Scenario: CT-03b Expired type indicator
-
-- GIVEN a case type with `validUntil` in the past
-- WHEN displayed in the admin list
-- THEN it MUST show an "Expired" indicator
-
-### Requirement: REQ-CT-04 Status Type Management (MVP scope)
-
-The system MUST support defining ordered status types for each case type.
-
-#### Scenario: CT-04a Add status types
-
-- GIVEN a case type in edit mode, Statuses tab active
-- WHEN the admin fills in name and order and clicks "Add"
-- THEN a new status type MUST be created as an OpenRegister object linked to the case type via the `caseType` field
-
-#### Scenario: CT-04b Reorder via drag
-
-- GIVEN a case type with multiple status types
-- WHEN the admin drags a status type to a new position
-- THEN `order` values MUST be recalculated and persisted for all affected status types
-
-#### Scenario: CT-04c Edit a status type
-
-- GIVEN an existing status type
-- WHEN the admin changes fields (name, isFinal, notifyInitiator, notificationText)
-- THEN the status type MUST be updated
-
-#### Scenario: CT-04d Delete a status type
-
-- GIVEN a status type not in use by any active case
-- WHEN the admin deletes it
-- THEN it MUST be removed
-- AND remaining types retain relative order
-
-#### Scenario: CT-04e Block deletion of in-use status type
-
-- GIVEN a status type referenced by active cases
-- WHEN the admin attempts to delete
-- THEN the system MUST show: "Cannot delete: active cases are at this status"
-
-#### Scenario: CT-04f Final status enforcement
-
-- GIVEN only one status type marked `isFinal`
-- WHEN the admin unchecks `isFinal`
-- THEN the system MUST block with: "At least one status type must be marked as final"
-
-#### Scenario: CT-04g Order is required
-
-- GIVEN the admin adding a status type without `order`
-- THEN the system MUST either reject or auto-assign the next order
-
-#### Scenario: CT-04h Name is required
-
-- GIVEN the admin adding a status type without `name`
-- THEN the system MUST reject with: "Status type name is required"
-
-### Requirement: REQ-CT-05 Processing Deadline Configuration (MVP scope)
-
-The system MUST support ISO 8601 duration fields for processing deadlines.
-
-#### Scenario: CT-05a Set and display processing deadline
-
-- GIVEN the admin sets `processingDeadline = "P56D"`
-- THEN the UI MUST display "56 days" as human-readable text
-- AND store the ISO 8601 duration
-
-#### Scenario: CT-05b Invalid format rejection
-
-- GIVEN the admin enters "56 days" (not ISO 8601)
-- THEN the system MUST reject with: "Must be a valid ISO 8601 duration (e.g., P56D)"
-
-#### Scenario: CT-05c Service target (optional)
-
-- GIVEN the admin sets `serviceTarget = "P42D"` alongside `processingDeadline = "P56D"`
-- THEN both MUST be stored independently
-
-### Requirement: REQ-CT-06 Extension Configuration (MVP scope)
-
-The system MUST support configuring extension rules on case types.
-
-#### Scenario: CT-06a Enable extension with period
-
-- GIVEN the admin sets `extensionAllowed = true` and `extensionPeriod = "P28D"`
-- THEN both MUST be stored
-
-#### Scenario: CT-06b Extension period required when enabled
-
-- GIVEN `extensionAllowed = true` and `extensionPeriod` empty
-- THEN the system MUST reject: "Extension period is required when extension is allowed"
-
-#### Scenario: CT-06c Disable extension hides period
-
-- GIVEN `extensionAllowed = false`
-- THEN `extensionPeriod` field MUST be hidden or disabled
-
-### Requirement: REQ-CT-13 Default Case Type (MVP scope)
-
-The system MUST support selecting a default case type.
-
-#### Scenario: CT-13a Set default
-
-- GIVEN published case types
-- WHEN the admin clicks "Set as default" on one
-- THEN it MUST be marked as default (star indicator)
-- AND any previous default MUST lose its default status
-
-#### Scenario: CT-13b Only published types can be default
-
-- GIVEN a draft case type
-- WHEN the admin tries to set it as default
-- THEN the system MUST reject: "Only published case types can be set as default"
-
-### Requirement: REQ-CT-14 Validation Rules (MVP scope)
-
-The system MUST enforce validation when creating/modifying case types.
-
-#### Scenario: CT-14a Required fields
-
-- GIVEN a case type form
-- WHEN the admin submits with any required field empty (title, purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit)
-- THEN the system MUST show validation errors for each missing field
-
-#### Scenario: CT-14b ISO 8601 duration validation
-
-- GIVEN a duration field (processingDeadline, serviceTarget, extensionPeriod)
-- WHEN the admin enters invalid format
-- THEN the system MUST reject with format guidance
-
-#### Scenario: CT-14c ValidUntil after ValidFrom
-
-- GIVEN `validFrom = "2026-01-01"` and `validUntil = "2025-12-31"`
-- THEN the system MUST reject: "'Valid until' must be after 'Valid from'"
-
-### Requirement: REQ-CT-15 Admin UI Tabs (MVP scope)
-
-The case type editor MUST have General and Statuses tabs.
-
-#### Scenario: CT-15a Tab layout
-
-- GIVEN the admin editing a case type
-- THEN the page MUST show tabs: General, Statuses
-- AND "General" MUST be active by default
-
-#### Scenario: CT-15b General tab content
-
-- GIVEN the "General" tab active
-- THEN editable fields MUST include: title, description, purpose, trigger, subject, processingDeadline, serviceTarget, extensionAllowed (with conditional period), origin, confidentiality, publicationRequired (with conditional text), validFrom, validUntil, responsibleUnit, referenceProcess, keywords
-
-#### Scenario: CT-15c Statuses tab content
-
-- GIVEN the "Statuses" tab active
-- THEN an ordered list of status types MUST display with drag handles
-- AND each shows: order, name, isFinal checkbox, notifyInitiator checkbox (with conditional text)
-- AND an "Add" button MUST be available
-
-### Requirement: REQ-CT-16 Error Scenarios (MVP scope)
-
-The system MUST handle error scenarios gracefully.
-
-#### Scenario: CT-16a Publish incomplete type
-
-- GIVEN a type missing required fields
-- WHEN admin publishes
-- THEN validation errors MUST list all missing fields
-
-#### Scenario: CT-16b Duplicate status order
-
-- GIVEN a status type at order 1 exists
-- WHEN admin adds another at order 1
-- THEN the system MUST reject: "A status type with this order already exists"
-
-## REMOVED Requirements
-
-_None removed._
+# Case Types (MVP) — Delta Specification
+
+## Purpose
+
+Implement the MVP tier of the case type system. This delta spec scopes the existing `case-types/spec.md` requirements to what will be built in this change: core CRUD, draft/publish lifecycle, validity periods, status type management, deadline configuration, extension config, default type, validation, General+Statuses tabs, and error scenarios.
+
+## ADDED Requirements
+
+_No new requirements added. All requirements below reference existing spec requirements scoped to MVP tier._
+
+## MODIFIED Requirements
+
+### Requirement: REQ-CT-01 Case Type CRUD (MVP scope)
+
+The system MUST support creating, reading, updating, and deleting case types. Case types are managed by admins via the Nextcloud admin settings page. This MVP implements scenarios CT-01a through CT-01e from the main spec.
+
+#### Scenario: CT-01a Create a case type
+
+- GIVEN an admin on the Procest admin settings page
+- WHEN they click "Add Case Type" and fill in the required fields (title, purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit, validFrom)
+- AND submit the form
+- THEN the system MUST create an OpenRegister object in the `procest` register with the `caseType` schema
+- AND `isDraft` MUST default to `true`
+- AND a unique `identifier` MUST be auto-generated (format: `CT-{timestamp}`)
+
+#### Scenario: CT-01b Update a case type
+
+- GIVEN an existing case type "Omgevingsvergunning"
+- WHEN the admin changes the `processingDeadline` from "P56D" to "P42D" and saves
+- THEN the system MUST update the OpenRegister object via PUT with the full object
+
+#### Scenario: CT-01c Delete a case type with no active cases
+
+- GIVEN a case type with no cases referencing it
+- WHEN the admin clicks "Delete" and confirms
+- THEN the system MUST delete the case type
+- AND all linked status types MUST also be deleted
+
+#### Scenario: CT-01d Prevent deletion of case type with active cases
+
+- GIVEN a case type referenced by active cases
+- WHEN the admin attempts to delete
+- THEN the system MUST show an error: "Cannot delete: active cases are using this type"
+
+#### Scenario: CT-01e Case type list display
+
+- GIVEN multiple case types exist
+- WHEN the admin views the case type list
+- THEN each type MUST display: title, status (Published/Draft badge), processing deadline, status type count, validity period
+- AND the default type MUST show a star indicator
+
+### Requirement: REQ-CT-02 Draft/Published Lifecycle (MVP scope)
+
+The system MUST support a draft/published lifecycle for case types. Draft case types MUST NOT be usable for creating cases.
+
+#### Scenario: CT-02a New case type defaults to draft
+
+- GIVEN an admin creating a new case type
+- WHEN the case type is saved
+- THEN `isDraft` MUST be `true`
+- AND the list MUST show a "DRAFT" badge
+
+#### Scenario: CT-02b Publish with valid configuration
+
+- GIVEN a draft case type with all required fields AND at least one status type with one final status AND validFrom set
+- WHEN the admin clicks "Publish"
+- THEN `isDraft` MUST be set to `false`
+- AND the type becomes available for creating cases
+
+#### Scenario: CT-02c Publish blocked — no status types
+
+- GIVEN a draft case type with no status types
+- WHEN the admin clicks "Publish"
+- THEN the system MUST show: "Cannot publish: at least one status type must be defined"
+
+#### Scenario: CT-02d Publish blocked — no final status
+
+- GIVEN a draft case type with status types but none marked `isFinal`
+- WHEN the admin clicks "Publish"
+- THEN the system MUST show: "Cannot publish: at least one status type must be marked as final"
+
+#### Scenario: CT-02e Publish blocked — no validFrom
+
+- GIVEN a draft case type without `validFrom`
+- WHEN the admin clicks "Publish"
+- THEN the system MUST show: "Cannot publish: 'Valid from' date must be set"
+
+#### Scenario: CT-02f Unpublish a case type
+
+- GIVEN a published case type
+- WHEN the admin clicks "Unpublish"
+- THEN the system MUST warn about impact on new case creation
+- AND if confirmed, revert to draft
+
+### Requirement: REQ-CT-03 Validity Periods (MVP scope)
+
+The system MUST support validity windows on case types.
+
+#### Scenario: CT-03a Valid type shown in admin list
+
+- GIVEN a case type with `validFrom` and optional `validUntil`
+- WHEN displayed in the admin list
+- THEN it MUST show the validity range (e.g., "Jan 2026 — Dec 2027" or "Jan 2026 — (no end)")
+
+#### Scenario: CT-03b Expired type indicator
+
+- GIVEN a case type with `validUntil` in the past
+- WHEN displayed in the admin list
+- THEN it MUST show an "Expired" indicator
+
+### Requirement: REQ-CT-04 Status Type Management (MVP scope)
+
+The system MUST support defining ordered status types for each case type.
+
+#### Scenario: CT-04a Add status types
+
+- GIVEN a case type in edit mode, Statuses tab active
+- WHEN the admin fills in name and order and clicks "Add"
+- THEN a new status type MUST be created as an OpenRegister object linked to the case type via the `caseType` field
+
+#### Scenario: CT-04b Reorder via drag
+
+- GIVEN a case type with multiple status types
+- WHEN the admin drags a status type to a new position
+- THEN `order` values MUST be recalculated and persisted for all affected status types
+
+#### Scenario: CT-04c Edit a status type
+
+- GIVEN an existing status type
+- WHEN the admin changes fields (name, isFinal, notifyInitiator, notificationText)
+- THEN the status type MUST be updated
+
+#### Scenario: CT-04d Delete a status type
+
+- GIVEN a status type not in use by any active case
+- WHEN the admin deletes it
+- THEN it MUST be removed
+- AND remaining types retain relative order
+
+#### Scenario: CT-04e Block deletion of in-use status type
+
+- GIVEN a status type referenced by active cases
+- WHEN the admin attempts to delete
+- THEN the system MUST show: "Cannot delete: active cases are at this status"
+
+#### Scenario: CT-04f Final status enforcement
+
+- GIVEN only one status type marked `isFinal`
+- WHEN the admin unchecks `isFinal`
+- THEN the system MUST block with: "At least one status type must be marked as final"
+
+#### Scenario: CT-04g Order is required
+
+- GIVEN the admin adding a status type without `order`
+- THEN the system MUST either reject or auto-assign the next order
+
+#### Scenario: CT-04h Name is required
+
+- GIVEN the admin adding a status type without `name`
+- THEN the system MUST reject with: "Status type name is required"
+
+### Requirement: REQ-CT-05 Processing Deadline Configuration (MVP scope)
+
+The system MUST support ISO 8601 duration fields for processing deadlines.
+
+#### Scenario: CT-05a Set and display processing deadline
+
+- GIVEN the admin sets `processingDeadline = "P56D"`
+- THEN the UI MUST display "56 days" as human-readable text
+- AND store the ISO 8601 duration
+
+#### Scenario: CT-05b Invalid format rejection
+
+- GIVEN the admin enters "56 days" (not ISO 8601)
+- THEN the system MUST reject with: "Must be a valid ISO 8601 duration (e.g., P56D)"
+
+#### Scenario: CT-05c Service target (optional)
+
+- GIVEN the admin sets `serviceTarget = "P42D"` alongside `processingDeadline = "P56D"`
+- THEN both MUST be stored independently
+
+### Requirement: REQ-CT-06 Extension Configuration (MVP scope)
+
+The system MUST support configuring extension rules on case types.
+
+#### Scenario: CT-06a Enable extension with period
+
+- GIVEN the admin sets `extensionAllowed = true` and `extensionPeriod = "P28D"`
+- THEN both MUST be stored
+
+#### Scenario: CT-06b Extension period required when enabled
+
+- GIVEN `extensionAllowed = true` and `extensionPeriod` empty
+- THEN the system MUST reject: "Extension period is required when extension is allowed"
+
+#### Scenario: CT-06c Disable extension hides period
+
+- GIVEN `extensionAllowed = false`
+- THEN `extensionPeriod` field MUST be hidden or disabled
+
+### Requirement: REQ-CT-13 Default Case Type (MVP scope)
+
+The system MUST support selecting a default case type.
+
+#### Scenario: CT-13a Set default
+
+- GIVEN published case types
+- WHEN the admin clicks "Set as default" on one
+- THEN it MUST be marked as default (star indicator)
+- AND any previous default MUST lose its default status
+
+#### Scenario: CT-13b Only published types can be default
+
+- GIVEN a draft case type
+- WHEN the admin tries to set it as default
+- THEN the system MUST reject: "Only published case types can be set as default"
+
+### Requirement: REQ-CT-14 Validation Rules (MVP scope)
+
+The system MUST enforce validation when creating/modifying case types.
+
+#### Scenario: CT-14a Required fields
+
+- GIVEN a case type form
+- WHEN the admin submits with any required field empty (title, purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit)
+- THEN the system MUST show validation errors for each missing field
+
+#### Scenario: CT-14b ISO 8601 duration validation
+
+- GIVEN a duration field (processingDeadline, serviceTarget, extensionPeriod)
+- WHEN the admin enters invalid format
+- THEN the system MUST reject with format guidance
+
+#### Scenario: CT-14c ValidUntil after ValidFrom
+
+- GIVEN `validFrom = "2026-01-01"` and `validUntil = "2025-12-31"`
+- THEN the system MUST reject: "'Valid until' must be after 'Valid from'"
+
+### Requirement: REQ-CT-15 Admin UI Tabs (MVP scope)
+
+The case type editor MUST have General and Statuses tabs.
+
+#### Scenario: CT-15a Tab layout
+
+- GIVEN the admin editing a case type
+- THEN the page MUST show tabs: General, Statuses
+- AND "General" MUST be active by default
+
+#### Scenario: CT-15b General tab content
+
+- GIVEN the "General" tab active
+- THEN editable fields MUST include: title, description, purpose, trigger, subject, processingDeadline, serviceTarget, extensionAllowed (with conditional period), origin, confidentiality, publicationRequired (with conditional text), validFrom, validUntil, responsibleUnit, referenceProcess, keywords
+
+#### Scenario: CT-15c Statuses tab content
+
+- GIVEN the "Statuses" tab active
+- THEN an ordered list of status types MUST display with drag handles
+- AND each shows: order, name, isFinal checkbox, notifyInitiator checkbox (with conditional text)
+- AND an "Add" button MUST be available
+
+### Requirement: REQ-CT-16 Error Scenarios (MVP scope)
+
+The system MUST handle error scenarios gracefully.
+
+#### Scenario: CT-16a Publish incomplete type
+
+- GIVEN a type missing required fields
+- WHEN admin publishes
+- THEN validation errors MUST list all missing fields
+
+#### Scenario: CT-16b Duplicate status order
+
+- GIVEN a status type at order 1 exists
+- WHEN admin adds another at order 1
+- THEN the system MUST reject: "A status type with this order already exists"
+
+## REMOVED Requirements
+
+_None removed._
diff --git a/openspec/changes/archive/2026-02-26-case-types/tasks.md b/openspec/changes/archive/2026-02-26-case-types/tasks.md
index 003e4042..c9b19d6c 100644
--- a/openspec/changes/archive/2026-02-26-case-types/tasks.md
+++ b/openspec/changes/archive/2026-02-26-case-types/tasks.md
@@ -1,189 +1,189 @@
-# Tasks: case-types
-
-## 1. Backend & Store Setup
-
-- [x] 1.1 Add `case_type_schema` and `status_type_schema` to SettingsService
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
- - **files**: `lib/Service/SettingsService.php`
- - **acceptance_criteria**:
- - GIVEN the SettingsService WHEN `getSettings()` is called THEN it returns `case_type_schema` and `status_type_schema` along with existing keys
- - GIVEN a POST to `/api/settings` with `case_type_schema` and `status_type_schema` THEN both are persisted in IAppConfig
-
-- [x] 1.2 Register `caseType` and `statusType` object types in store initialization
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
- - **files**: `src/store/store.js`
- - **acceptance_criteria**:
- - GIVEN settings contain `case_type_schema` and `status_type_schema` WHEN `initializeStores()` runs THEN `caseType` and `statusType` are registered with `useObjectStore`
- - GIVEN `case_type_schema` is not configured THEN `caseType` is NOT registered (no error)
-
-- [x] 1.3 Add `default_case_type` to SettingsService
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-13`
- - **files**: `lib/Service/SettingsService.php`
- - **acceptance_criteria**:
- - GIVEN the settings API WHEN `getSettings()` is called THEN `default_case_type` is included (default empty string)
- - GIVEN a POST with `default_case_type` set to a UUID THEN it is persisted
-
-- [x] 1.4 Update Settings.vue to include new schema config fields
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
- - **files**: `src/views/settings/Settings.vue`
- - **acceptance_criteria**:
- - GIVEN the admin settings page WHEN the config section renders THEN fields for `case_type_schema` and `status_type_schema` are visible and editable
-
-## 2. Utility Functions
-
-- [x] 2.1 Create ISO 8601 duration helpers (`durationHelpers.js`)
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-05`
- - **files**: `src/utils/durationHelpers.js`
- - **acceptance_criteria**:
- - GIVEN `"P56D"` WHEN calling `formatDuration("P56D")` THEN it returns "56 days"
- - GIVEN `"P8W"` WHEN calling `formatDuration("P8W")` THEN it returns "8 weeks"
- - GIVEN `"P2M"` WHEN calling `formatDuration("P2M")` THEN it returns "2 months"
- - GIVEN `"P1Y"` WHEN calling `formatDuration("P1Y")` THEN it returns "1 year"
- - GIVEN `"56 days"` WHEN calling `isValidDuration("56 days")` THEN it returns `false`
- - GIVEN `"P56D"` WHEN calling `isValidDuration("P56D")` THEN it returns `true`
- - Export `parseDuration(iso)` returning `{ years, months, weeks, days }` object
- - Export `formatDuration(iso)` returning human-readable string
- - Export `isValidDuration(value)` returning boolean
-
-- [x] 2.2 Create case type validation helpers (`caseTypeValidation.js`)
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-14`, `#REQ-CT-02`
- - **files**: `src/utils/caseTypeValidation.js`
- - **acceptance_criteria**:
- - GIVEN a case type with empty title WHEN calling `validateCaseType(data)` THEN it returns `{ valid: false, errors: { title: "Title is required" } }`
- - GIVEN a case type with all required fields filled WHEN calling `validateCaseType(data)` THEN it returns `{ valid: true, errors: {} }`
- - GIVEN `extensionAllowed = true` and empty `extensionPeriod` WHEN validating THEN error: "Extension period is required when extension is allowed"
- - GIVEN `validFrom = "2026-01-01"` and `validUntil = "2025-12-31"` WHEN validating THEN error: "'Valid until' must be after 'Valid from'"
- - Export `validateForPublish(caseType, statusTypes)` returning `{ valid, errors[] }` checking: required fields, >=1 status type, >=1 final status, validFrom set
- - Export `REQUIRED_FIELDS` constant listing all required case type fields
- - Export `ORIGIN_OPTIONS`, `CONFIDENTIALITY_OPTIONS` as arrays for select dropdowns
-
-## 3. Admin Root & Navigation
-
-- [x] 3.1 Create `AdminRoot.vue` and update `settings.js` entry point
- - **spec_ref**: `procest/openspec/changes/case-types/design.md`
- - **files**: `src/views/settings/AdminRoot.vue`, `src/settings.js`
- - **acceptance_criteria**:
- - GIVEN the admin navigates to Procest settings WHEN the page loads THEN AdminRoot renders with two sections: Configuration (existing Settings.vue) and Case Type Management (CaseTypeAdmin)
- - GIVEN AdminRoot WHEN store initialization completes THEN `caseType` and `statusType` object types are available
-
-- [x] 3.2 Create `CaseTypeAdmin.vue` with list/detail routing
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
- - **files**: `src/views/settings/CaseTypeAdmin.vue`
- - **acceptance_criteria**:
- - GIVEN the admin section WHEN no case type is selected THEN `CaseTypeList` is shown
- - GIVEN the admin clicks a case type THEN `CaseTypeDetail` is shown for that type
- - GIVEN the admin clicks "Add Case Type" THEN `CaseTypeDetail` in create mode is shown
- - GIVEN the admin clicks "Back to list" in detail view THEN the list is shown again
-
-## 4. Case Type List
-
-- [x] 4.1 Create `CaseTypeList.vue`
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`, `#REQ-CT-02`, `#REQ-CT-03`, `#REQ-CT-13`
- - **files**: `src/views/settings/CaseTypeList.vue`
- - **acceptance_criteria**:
- - GIVEN case types exist WHEN the list renders THEN each type shows: title, Published/Draft badge, processing deadline (human-readable), status type count, validity range
- - GIVEN a type is the default THEN a star icon MUST be shown
- - GIVEN a type has expired THEN an "Expired" indicator MUST be shown
- - GIVEN no case types exist THEN an empty state with "No case types configured" is shown
- - GIVEN the "Add Case Type" button WHEN clicked THEN navigates to create mode
- - Loading state with NcLoadingIcon while fetching
-
-- [x] 4.2 Add "Set as default" and delete actions to CaseTypeList
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-13`, `#REQ-CT-01`
- - **files**: `src/views/settings/CaseTypeList.vue`
- - **acceptance_criteria**:
- - GIVEN a published case type WHEN "Set as default" is clicked THEN the default_case_type setting is updated AND the star indicator moves
- - GIVEN a draft case type WHEN "Set as default" is clicked THEN an error is shown: "Only published case types can be set as default"
- - GIVEN a case type with no active cases WHEN "Delete" is clicked and confirmed THEN the type and all linked status types are deleted
- - GIVEN a case type with active cases WHEN "Delete" is clicked THEN an error is shown
-
-## 5. Case Type Detail — General Tab
-
-- [x] 5.1 Create `CaseTypeDetail.vue` with tab navigation
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-15`
- - **files**: `src/views/settings/CaseTypeDetail.vue`
- - **acceptance_criteria**:
- - GIVEN an existing case type WHEN the detail loads THEN tabs "General" and "Statuses" are shown
- - GIVEN the page loads THEN "General" tab is active by default
- - GIVEN a new case type (create mode) WHEN the detail loads THEN the form is empty with `isDraft = true`
- - Header shows case type title (or "New Case Type") and a "Back to list" button
- - Publish/Unpublish button visible in header based on `isDraft` state
- - Save button persists the case type (full PUT for updates, POST for create)
-
-- [x] 5.2 Create `GeneralTab.vue` with all case type fields
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`, `#REQ-CT-05`, `#REQ-CT-06`, `#REQ-CT-14`
- - **files**: `src/views/settings/tabs/GeneralTab.vue`
- - **acceptance_criteria**:
- - Form fields: title*, description, purpose*, trigger*, subject*, initiatorAction, handlerAction, processingDeadline* (with human-readable preview), serviceTarget, extensionAllowed (checkbox), extensionPeriod (conditional, shown when extensionAllowed), origin* (select: internal/external), confidentiality* (select), publicationRequired (checkbox), publicationText (conditional), responsibleUnit*, referenceProcess, keywords (text input), validFrom*, validUntil
- - GIVEN processingDeadline = "P56D" THEN a helper text shows "56 days"
- - GIVEN extensionAllowed = false THEN extensionPeriod field is hidden
- - GIVEN publicationRequired = false THEN publicationText field is hidden
- - All required fields marked with asterisk
- - Validation errors shown inline per field
-
-- [x] 5.3 Add publish/unpublish functionality to CaseTypeDetail
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-02`
- - **files**: `src/views/settings/CaseTypeDetail.vue`, `src/utils/caseTypeValidation.js`
- - **acceptance_criteria**:
- - GIVEN a draft type with valid config WHEN "Publish" is clicked THEN `isDraft` is set to `false` and saved
- - GIVEN a draft type missing required fields or status types WHEN "Publish" is clicked THEN validation errors are shown listing all blockers
- - GIVEN a published type WHEN "Unpublish" is clicked THEN a warning is shown about impact AND if confirmed `isDraft` is set to `true`
-
-## 6. Case Type Detail — Statuses Tab
-
-- [x] 6.1 Create `StatusesTab.vue` with status type list and CRUD
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-04`
- - **files**: `src/views/settings/tabs/StatusesTab.vue`
- - **acceptance_criteria**:
- - GIVEN a case type WHEN Statuses tab is active THEN all status types are fetched with `_filters[caseType]={id}` and displayed ordered by `order`
- - Each status type row shows: order number, name, isFinal checkbox, notifyInitiator checkbox, notification text (if notifyInitiator), edit/delete actions
- - "Add Status Type" button opens an inline form with fields: name*, order*, isFinal, notifyInitiator, notificationText
- - GIVEN an empty name WHEN submitting THEN error: "Status type name is required"
- - GIVEN a duplicate order WHEN submitting THEN error: "A status type with this order already exists"
- - Loading state while fetching status types
-
-- [x] 6.2 Add drag-and-drop reordering to StatusesTab
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-04`
- - **files**: `src/views/settings/tabs/StatusesTab.vue`
- - **acceptance_criteria**:
- - GIVEN status types [A(1), B(2), C(3)] WHEN B is dragged before A THEN orders become [B(1), A(2), C(3)] and all affected types are saved
- - Drag handles are visible on each row
- - During drag, the drop target is visually indicated
-
-- [x] 6.3 Add delete and edit for status types with validation
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-04`
- - **files**: `src/views/settings/tabs/StatusesTab.vue`
- - **acceptance_criteria**:
- - GIVEN a status type not in use WHEN "Delete" is clicked and confirmed THEN it is removed
- - GIVEN only one final status exists WHEN admin unchecks `isFinal` THEN error: "At least one status type must be marked as final"
- - GIVEN a status type WHEN "Edit" is clicked THEN fields become editable inline
- - GIVEN changes WHEN "Save" is clicked on the row THEN the status type is updated
-
-## 7. Cascade Delete & Error Handling
-
-- [x] 7.1 Implement cascade delete for case types
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
- - **files**: `src/views/settings/CaseTypeList.vue`
- - **acceptance_criteria**:
- - GIVEN a case type with 3 status types WHEN deleted THEN all 3 status types are deleted first, then the case type
- - GIVEN a deletion fails midway THEN an error message is shown and remaining items are not deleted
- - Confirmation dialog shows: "This will delete the case type and all {N} status types. Continue?"
-
-- [x] 7.2 Add error display for publish validation and API errors
- - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-16`
- - **files**: `src/views/settings/CaseTypeDetail.vue`
- - **acceptance_criteria**:
- - GIVEN publish validation fails THEN all errors are shown in a list (e.g., "Missing: purpose, trigger", "No status types defined", "No final status")
- - GIVEN an API error on save THEN a toast notification shows the error message
- - GIVEN an API error on delete THEN the error is shown and the item remains
-
-## Verification
-
-- [x] All tasks checked off
-- [ ] Manual testing: create, edit, publish, unpublish, delete case types
-- [ ] Manual testing: add, edit, reorder, delete status types
-- [ ] Manual testing: validation errors for all required fields
-- [ ] Manual testing: publish blocked when prerequisites not met
-- [ ] Manual testing: default case type set/change/draft-blocked
-- [ ] Manual testing: cascade delete removes status types
-- [ ] Manual testing: ISO 8601 duration display and validation
+# Tasks: case-types
+
+## 1. Backend & Store Setup
+
+- [x] 1.1 Add `case_type_schema` and `status_type_schema` to SettingsService
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
+ - **files**: `lib/Service/SettingsService.php`
+ - **acceptance_criteria**:
+ - GIVEN the SettingsService WHEN `getSettings()` is called THEN it returns `case_type_schema` and `status_type_schema` along with existing keys
+ - GIVEN a POST to `/api/settings` with `case_type_schema` and `status_type_schema` THEN both are persisted in IAppConfig
+
+- [x] 1.2 Register `caseType` and `statusType` object types in store initialization
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
+ - **files**: `src/store/store.js`
+ - **acceptance_criteria**:
+ - GIVEN settings contain `case_type_schema` and `status_type_schema` WHEN `initializeStores()` runs THEN `caseType` and `statusType` are registered with `useObjectStore`
+ - GIVEN `case_type_schema` is not configured THEN `caseType` is NOT registered (no error)
+
+- [x] 1.3 Add `default_case_type` to SettingsService
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-13`
+ - **files**: `lib/Service/SettingsService.php`
+ - **acceptance_criteria**:
+ - GIVEN the settings API WHEN `getSettings()` is called THEN `default_case_type` is included (default empty string)
+ - GIVEN a POST with `default_case_type` set to a UUID THEN it is persisted
+
+- [x] 1.4 Update Settings.vue to include new schema config fields
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
+ - **files**: `src/views/settings/Settings.vue`
+ - **acceptance_criteria**:
+ - GIVEN the admin settings page WHEN the config section renders THEN fields for `case_type_schema` and `status_type_schema` are visible and editable
+
+## 2. Utility Functions
+
+- [x] 2.1 Create ISO 8601 duration helpers (`durationHelpers.js`)
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-05`
+ - **files**: `src/utils/durationHelpers.js`
+ - **acceptance_criteria**:
+ - GIVEN `"P56D"` WHEN calling `formatDuration("P56D")` THEN it returns "56 days"
+ - GIVEN `"P8W"` WHEN calling `formatDuration("P8W")` THEN it returns "8 weeks"
+ - GIVEN `"P2M"` WHEN calling `formatDuration("P2M")` THEN it returns "2 months"
+ - GIVEN `"P1Y"` WHEN calling `formatDuration("P1Y")` THEN it returns "1 year"
+ - GIVEN `"56 days"` WHEN calling `isValidDuration("56 days")` THEN it returns `false`
+ - GIVEN `"P56D"` WHEN calling `isValidDuration("P56D")` THEN it returns `true`
+ - Export `parseDuration(iso)` returning `{ years, months, weeks, days }` object
+ - Export `formatDuration(iso)` returning human-readable string
+ - Export `isValidDuration(value)` returning boolean
+
+- [x] 2.2 Create case type validation helpers (`caseTypeValidation.js`)
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-14`, `#REQ-CT-02`
+ - **files**: `src/utils/caseTypeValidation.js`
+ - **acceptance_criteria**:
+ - GIVEN a case type with empty title WHEN calling `validateCaseType(data)` THEN it returns `{ valid: false, errors: { title: "Title is required" } }`
+ - GIVEN a case type with all required fields filled WHEN calling `validateCaseType(data)` THEN it returns `{ valid: true, errors: {} }`
+ - GIVEN `extensionAllowed = true` and empty `extensionPeriod` WHEN validating THEN error: "Extension period is required when extension is allowed"
+ - GIVEN `validFrom = "2026-01-01"` and `validUntil = "2025-12-31"` WHEN validating THEN error: "'Valid until' must be after 'Valid from'"
+ - Export `validateForPublish(caseType, statusTypes)` returning `{ valid, errors[] }` checking: required fields, >=1 status type, >=1 final status, validFrom set
+ - Export `REQUIRED_FIELDS` constant listing all required case type fields
+ - Export `ORIGIN_OPTIONS`, `CONFIDENTIALITY_OPTIONS` as arrays for select dropdowns
+
+## 3. Admin Root & Navigation
+
+- [x] 3.1 Create `AdminRoot.vue` and update `settings.js` entry point
+ - **spec_ref**: `procest/openspec/changes/case-types/design.md`
+ - **files**: `src/views/settings/AdminRoot.vue`, `src/settings.js`
+ - **acceptance_criteria**:
+ - GIVEN the admin navigates to Procest settings WHEN the page loads THEN AdminRoot renders with two sections: Configuration (existing Settings.vue) and Case Type Management (CaseTypeAdmin)
+ - GIVEN AdminRoot WHEN store initialization completes THEN `caseType` and `statusType` object types are available
+
+- [x] 3.2 Create `CaseTypeAdmin.vue` with list/detail routing
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
+ - **files**: `src/views/settings/CaseTypeAdmin.vue`
+ - **acceptance_criteria**:
+ - GIVEN the admin section WHEN no case type is selected THEN `CaseTypeList` is shown
+ - GIVEN the admin clicks a case type THEN `CaseTypeDetail` is shown for that type
+ - GIVEN the admin clicks "Add Case Type" THEN `CaseTypeDetail` in create mode is shown
+ - GIVEN the admin clicks "Back to list" in detail view THEN the list is shown again
+
+## 4. Case Type List
+
+- [x] 4.1 Create `CaseTypeList.vue`
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`, `#REQ-CT-02`, `#REQ-CT-03`, `#REQ-CT-13`
+ - **files**: `src/views/settings/CaseTypeList.vue`
+ - **acceptance_criteria**:
+ - GIVEN case types exist WHEN the list renders THEN each type shows: title, Published/Draft badge, processing deadline (human-readable), status type count, validity range
+ - GIVEN a type is the default THEN a star icon MUST be shown
+ - GIVEN a type has expired THEN an "Expired" indicator MUST be shown
+ - GIVEN no case types exist THEN an empty state with "No case types configured" is shown
+ - GIVEN the "Add Case Type" button WHEN clicked THEN navigates to create mode
+ - Loading state with NcLoadingIcon while fetching
+
+- [x] 4.2 Add "Set as default" and delete actions to CaseTypeList
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-13`, `#REQ-CT-01`
+ - **files**: `src/views/settings/CaseTypeList.vue`
+ - **acceptance_criteria**:
+ - GIVEN a published case type WHEN "Set as default" is clicked THEN the default_case_type setting is updated AND the star indicator moves
+ - GIVEN a draft case type WHEN "Set as default" is clicked THEN an error is shown: "Only published case types can be set as default"
+ - GIVEN a case type with no active cases WHEN "Delete" is clicked and confirmed THEN the type and all linked status types are deleted
+ - GIVEN a case type with active cases WHEN "Delete" is clicked THEN an error is shown
+
+## 5. Case Type Detail — General Tab
+
+- [x] 5.1 Create `CaseTypeDetail.vue` with tab navigation
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-15`
+ - **files**: `src/views/settings/CaseTypeDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN an existing case type WHEN the detail loads THEN tabs "General" and "Statuses" are shown
+ - GIVEN the page loads THEN "General" tab is active by default
+ - GIVEN a new case type (create mode) WHEN the detail loads THEN the form is empty with `isDraft = true`
+ - Header shows case type title (or "New Case Type") and a "Back to list" button
+ - Publish/Unpublish button visible in header based on `isDraft` state
+ - Save button persists the case type (full PUT for updates, POST for create)
+
+- [x] 5.2 Create `GeneralTab.vue` with all case type fields
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`, `#REQ-CT-05`, `#REQ-CT-06`, `#REQ-CT-14`
+ - **files**: `src/views/settings/tabs/GeneralTab.vue`
+ - **acceptance_criteria**:
+ - Form fields: title*, description, purpose*, trigger*, subject*, initiatorAction, handlerAction, processingDeadline* (with human-readable preview), serviceTarget, extensionAllowed (checkbox), extensionPeriod (conditional, shown when extensionAllowed), origin* (select: internal/external), confidentiality* (select), publicationRequired (checkbox), publicationText (conditional), responsibleUnit*, referenceProcess, keywords (text input), validFrom*, validUntil
+ - GIVEN processingDeadline = "P56D" THEN a helper text shows "56 days"
+ - GIVEN extensionAllowed = false THEN extensionPeriod field is hidden
+ - GIVEN publicationRequired = false THEN publicationText field is hidden
+ - All required fields marked with asterisk
+ - Validation errors shown inline per field
+
+- [x] 5.3 Add publish/unpublish functionality to CaseTypeDetail
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-02`
+ - **files**: `src/views/settings/CaseTypeDetail.vue`, `src/utils/caseTypeValidation.js`
+ - **acceptance_criteria**:
+ - GIVEN a draft type with valid config WHEN "Publish" is clicked THEN `isDraft` is set to `false` and saved
+ - GIVEN a draft type missing required fields or status types WHEN "Publish" is clicked THEN validation errors are shown listing all blockers
+ - GIVEN a published type WHEN "Unpublish" is clicked THEN a warning is shown about impact AND if confirmed `isDraft` is set to `true`
+
+## 6. Case Type Detail — Statuses Tab
+
+- [x] 6.1 Create `StatusesTab.vue` with status type list and CRUD
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-04`
+ - **files**: `src/views/settings/tabs/StatusesTab.vue`
+ - **acceptance_criteria**:
+ - GIVEN a case type WHEN Statuses tab is active THEN all status types are fetched with `_filters[caseType]={id}` and displayed ordered by `order`
+ - Each status type row shows: order number, name, isFinal checkbox, notifyInitiator checkbox, notification text (if notifyInitiator), edit/delete actions
+ - "Add Status Type" button opens an inline form with fields: name*, order*, isFinal, notifyInitiator, notificationText
+ - GIVEN an empty name WHEN submitting THEN error: "Status type name is required"
+ - GIVEN a duplicate order WHEN submitting THEN error: "A status type with this order already exists"
+ - Loading state while fetching status types
+
+- [x] 6.2 Add drag-and-drop reordering to StatusesTab
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-04`
+ - **files**: `src/views/settings/tabs/StatusesTab.vue`
+ - **acceptance_criteria**:
+ - GIVEN status types [A(1), B(2), C(3)] WHEN B is dragged before A THEN orders become [B(1), A(2), C(3)] and all affected types are saved
+ - Drag handles are visible on each row
+ - During drag, the drop target is visually indicated
+
+- [x] 6.3 Add delete and edit for status types with validation
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-04`
+ - **files**: `src/views/settings/tabs/StatusesTab.vue`
+ - **acceptance_criteria**:
+ - GIVEN a status type not in use WHEN "Delete" is clicked and confirmed THEN it is removed
+ - GIVEN only one final status exists WHEN admin unchecks `isFinal` THEN error: "At least one status type must be marked as final"
+ - GIVEN a status type WHEN "Edit" is clicked THEN fields become editable inline
+ - GIVEN changes WHEN "Save" is clicked on the row THEN the status type is updated
+
+## 7. Cascade Delete & Error Handling
+
+- [x] 7.1 Implement cascade delete for case types
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-01`
+ - **files**: `src/views/settings/CaseTypeList.vue`
+ - **acceptance_criteria**:
+ - GIVEN a case type with 3 status types WHEN deleted THEN all 3 status types are deleted first, then the case type
+ - GIVEN a deletion fails midway THEN an error message is shown and remaining items are not deleted
+ - Confirmation dialog shows: "This will delete the case type and all {N} status types. Continue?"
+
+- [x] 7.2 Add error display for publish validation and API errors
+ - **spec_ref**: `procest/openspec/specs/case-types/spec.md#REQ-CT-16`
+ - **files**: `src/views/settings/CaseTypeDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN publish validation fails THEN all errors are shown in a list (e.g., "Missing: purpose, trigger", "No status types defined", "No final status")
+ - GIVEN an API error on save THEN a toast notification shows the error message
+ - GIVEN an API error on delete THEN the error is shown and the item remains
+
+## Verification
+
+- [x] All tasks checked off
+- [ ] Manual testing: create, edit, publish, unpublish, delete case types
+- [ ] Manual testing: add, edit, reorder, delete status types
+- [ ] Manual testing: validation errors for all required fields
+- [ ] Manual testing: publish blocked when prerequisites not met
+- [ ] Manual testing: default case type set/change/draft-blocked
+- [ ] Manual testing: cascade delete removes status types
+- [ ] Manual testing: ISO 8601 duration display and validation
diff --git a/openspec/changes/archive/2026-02-26-dashboard-mvp/.openspec.yaml b/openspec/changes/archive/2026-02-26-dashboard-mvp/.openspec.yaml
index 940c47bc..95f35fef 100644
--- a/openspec/changes/archive/2026-02-26-dashboard-mvp/.openspec.yaml
+++ b/openspec/changes/archive/2026-02-26-dashboard-mvp/.openspec.yaml
@@ -1,2 +1,2 @@
-schema: conduction
-created: 2026-02-26
+schema: conduction
+created: 2026-02-26
diff --git a/openspec/changes/archive/2026-02-26-dashboard-mvp/design.md b/openspec/changes/archive/2026-02-26-dashboard-mvp/design.md
index 6546d0ed..39f87873 100644
--- a/openspec/changes/archive/2026-02-26-dashboard-mvp/design.md
+++ b/openspec/changes/archive/2026-02-26-dashboard-mvp/design.md
@@ -1,246 +1,246 @@
-# Design: dashboard-mvp
-
-## Architecture Overview
-
-This change rewrites the placeholder `Dashboard.vue` into a full dashboard with data-driven sections. All data flows through the existing `useObjectStore` Pinia store and OpenRegister API. No new backend code is needed — the dashboard is entirely frontend.
-
-```
-Dashboard.vue
-├── KpiCards.vue (4 metric cards in a flex row)
-├── StatusChart.vue (horizontal CSS bar chart)
-├── OverduePanel.vue (list of overdue cases)
-├── MyWorkPreview.vue (top 5 items: cases + tasks)
-├── ActivityFeed.vue (last 10 events from case activity arrays)
-└── CaseCreateDialog.vue (already exists from case-management)
-```
-
-## File Map
-
-### New Files
-
-| File | Purpose |
-|------|---------|
-| `src/utils/dashboardHelpers.js` | KPI calculation, activity aggregation, overdue sorting, status aggregation |
-| `src/views/dashboard/KpiCards.vue` | Row of 4 KPI metric cards with counts and sub-labels |
-| `src/views/dashboard/StatusChart.vue` | Horizontal bar chart of cases by status (pure CSS) |
-| `src/views/dashboard/OverduePanel.vue` | Scrollable list of overdue cases with severity |
-| `src/views/dashboard/MyWorkPreview.vue` | Top 5 most urgent assigned items |
-| `src/views/dashboard/ActivityFeed.vue` | Last 10 activity entries across all visible cases |
-
-### Modified Files
-
-| File | Changes |
-|------|---------|
-| `src/views/Dashboard.vue` | Complete rewrite: fetch data on mount, compose sub-components, loading/error/empty states, refresh button, quick actions |
-| `src/App.vue` | No changes needed — `Dashboard` component already referenced, default route is `dashboard` |
-
-### Unchanged Files
-
-| File | Reason |
-|------|--------|
-| `src/store/modules/object.js` | Generic CRUD store already supports `fetchCollection` with filters |
-| `src/store/store.js` | Object types (case, task, caseType, statusType) already registered |
-| `src/views/cases/CaseCreateDialog.vue` | Already exists from case-management change, reused as-is |
-
-## Design Decisions
-
-### DD-01: CSS Bar Chart (No Charting Library)
-
-**Decision**: Implement the status distribution chart using pure CSS — `div` bars with proportional `width` percentages.
-
-**Rationale**: Adding Chart.js or similar is ~200KB gzipped and overkill for a single horizontal bar chart. The chart only needs labeled bars proportional to counts. CSS flex/grid achieves this with zero dependencies.
-
-**Implementation**: Each bar is a `div` with `width: (count / maxCount * 100)%`, a background color from a 6-color palette, and labels showing status name (left) and count (right).
-
-### DD-02: Activity Aggregation from Case Objects
-
-**Decision**: Aggregate activity entries by iterating over all visible cases' `activity` arrays, merge into a single list, sort by date descending, take the top 10.
-
-**Rationale**: MVP stores activity as embedded arrays on case objects (design decision DD-03 from case-management). No separate activity API exists. Fetching all cases (which are already loaded for KPIs) and extracting their activity arrays is efficient for MVP scale (< 1000 cases).
-
-**Trade-off**: For large installations, this means loading all case objects to get activity. Acceptable for MVP; V1 can add a dedicated activity query endpoint.
-
-### DD-03: Parallel Data Fetching
-
-**Decision**: On mount, fire all data queries concurrently using `Promise.allSettled()`:
-1. Fetch all cases (open + completed) in a single query, split client-side using statusType `isFinal`
-2. Fetch case types for name resolution
-3. Fetch status types for status name resolution, chart labels, and open/completed splitting
-4. Fetch tasks assigned to current user for "My Tasks" KPI and My Work
-
-**Rationale**: Sequential fetching would multiply the load time. `Promise.allSettled` ensures partial failures don't block the entire dashboard (REQ-DASH-012). Fetching all cases in one query (instead of separate open/completed queries) reduces API calls from 5 to 4 while keeping the same data available.
-
-**Implementation**: Dashboard.vue calls a `loadDashboardData()` method that awaits `Promise.allSettled([fetchCases(), fetchCaseTypes(), fetchStatusTypes(), fetchTasks()])`. Cases are split into open vs completed client-side by checking each case's status against the statusType `isFinal` flag. Each section checks its own loading/error state independently.
-
-### DD-04: Section-Level Loading and Error States
-
-**Decision**: Each dashboard section (KPIs, chart, overdue, my work, activity) has independent loading/error state.
-
-**Rationale**: REQ-DASH-012 requires that "the system MUST NOT block the entire dashboard due to a single section failure." If activity loading fails, KPI cards and overdue panel still show.
-
-**Implementation**: Each section component receives `loading` and `error` props. When `loading`, show `NcLoadingIcon` or skeleton. When `error`, show inline error message with retry callback.
-
-### DD-05: KPI Sub-Label Calculations
-
-**Decision**: Calculate sub-labels client-side from fetched data:
-- Open Cases: count cases with `startDate == today` → "+N today"
-- Overdue: static text "action needed" when count > 0
-- Completed: average `(endDate - startDate)` in days → "avg N days"
-- My Tasks: count tasks with `dueDate == today` → "N due today"
-
-**Rationale**: All data needed for sub-labels is already fetched for the main counts. No additional API calls.
-
-### DD-06: My Work Preview Reuses My Work Sorting Logic
-
-**Decision**: Import sorting/grouping utilities from the existing `taskHelpers.js` and new `caseHelpers.js` to sort the top-5 items by priority then deadline.
-
-**Rationale**: The My Work preview (REQ-DASH-005) uses the same sort order as the full My Work view. Reusing the helpers ensures consistency.
-
-### DD-07: Overdue Panel Shows All (Scrollable)
-
-**Decision**: Show all overdue cases in a scrollable panel with `max-height: 300px` and `overflow-y: auto`, rather than limiting to N items.
-
-**Rationale**: REQ-DASH-004 says "show all overdue cases (or a scrollable list if many)." The "View all overdue" link navigates to case list filtered by overdue. The panel itself is a scrollable summary.
-
-## Component Props
-
-### KpiCards.vue
-```
-Props:
- openCases: Number
- newToday: Number
- overdueCases: Number
- completedThisMonth: Number
- avgProcessingDays: Number | null
- myTasks: Number
- tasksDueToday: Number
- loading: Boolean
-Events:
- @click-card(cardId: String) — navigates to filtered view
-```
-
-### StatusChart.vue
-```
-Props:
- statusData: Array<{ name: String, count: Number }>
- loading: Boolean
- error: String | null
-```
-
-### OverduePanel.vue
-```
-Props:
- cases: Array<{ identifier, title, caseTypeName, daysOverdue, handler }>
- loading: Boolean
- error: String | null
-Events:
- @view-all — navigates to cases filtered by overdue
- @click-case(caseId) — navigates to case detail
-```
-
-### MyWorkPreview.vue
-```
-Props:
- items: Array<{ type: 'case'|'task', title, reference, deadline, daysText, isOverdue, priority }>
- loading: Boolean
- error: String | null
-Events:
- @view-all — navigates to My Work view
- @click-item(type, id) — navigates to detail view
-```
-
-### ActivityFeed.vue
-```
-Props:
- entries: Array<{ date, type, description, user, caseIdentifier }>
- loading: Boolean
- error: String | null
-Events:
- @view-all — navigates to activity view
-```
-
-## Data Flow
-
-### Dashboard Mount Sequence
-1. Dashboard.vue `mounted()` calls `loadDashboardData()`
-2. Fire 4 parallel queries via `Promise.allSettled()`:
- - `fetchCollection('case', { '_limit': 1000 })` — all cases (split into open/completed client-side)
- - `fetchCollection('caseType', { '_limit': 100 })` — for case type name resolution
- - `fetchCollection('statusType', { '_limit': 500 })` — for status name resolution, chart, and open/completed splitting
- - `fetchCollection('task', { '_filters[assignee]': currentUser, '_limit': 100 })` — my tasks
-3. Split cases into open vs completed by checking each case's status against statusType `isFinal` flag
-4. On each resolved: compute derived data, pass to sub-components
-5. On rejection: set section-level error, show retry
-
-### KPI Computation (in dashboardHelpers.js)
-```javascript
-computeKpis(openCases, completedCases, myTasks) → {
- openCount, newToday,
- overdueCount,
- completedCount, avgDays,
- taskCount, tasksDueToday
-}
-```
-
-### Status Aggregation (in dashboardHelpers.js)
-```javascript
-aggregateByStatus(openCases, statusTypes) → [{ name, count }]
-// Groups by status name across case types, orders by status type order
-```
-
-### Overdue Extraction (in dashboardHelpers.js)
-```javascript
-getOverdueCases(openCases, caseTypes) → [{ identifier, title, caseTypeName, daysOverdue, handler }]
-// Filters deadline < today, sorts by daysOverdue desc
-```
-
-### Activity Aggregation (in dashboardHelpers.js)
-```javascript
-getRecentActivity(cases, limit = 10) → [{ date, type, description, user, caseIdentifier }]
-// Flattens all cases' activity arrays, sorts by date desc, takes top N
-```
-
-## Layout Structure
-
-```
-┌──────────────────────────────────────────────────────────┐
-│ [+ New Case] [Refresh] │
-├──────────┬──────────┬──────────┬──────────────────────────┤
-│ Open (24)│Overdue(3)│Done (12) │ My Tasks (7) │
-│ +3 today │ action! │ avg 18d │ 2 due today │
-├──────────┴──────────┴──────────┴──────────────────────────┤
-│ │ │
-│ Cases by Status │ Overdue Cases │
-│ ██████████ 8 │ #042 Bouwverg.. 5 days overdue │
-│ ███████ 6 │ #038 Subsidie.. 2 days overdue │
-│ █████ 5 │ [View all overdue →] │
-│ ███ 3 │ │
-│ ├──────────────────────────────────────│
-│ My Work (top 5) │ Recent Activity │
-│ [CASE] #042... │ Status changed on #042 - 10m ago │
-│ [TASK] Review.. │ Decision on #036 - 1h ago │
-│ [View all →] │ Task completed - 2h ago │
-│ │ [View all activity →] │
-└────────────────────┴──────────────────────────────────────┘
-```
-
-Responsive: below 768px, two columns collapse to single column with all sections stacked vertically.
-
-## Security Considerations
-
-- All data queries go through OpenRegister which enforces RBAC — no additional auth needed
-- No new API endpoints — frontend-only
-- No CSRF concerns — read-only data display
-
-## NL Design System
-
-- KPI cards: Use Nextcloud `NcNoteCard` or custom card styling with `--color-primary`, `--color-warning`, `--color-success` CSS variables
-- Text: Use `--font-face` and Nextcloud typography classes for consistent sizing
-- Colors: All chart colors from CSS custom properties, no hardcoded hex values
-- Responsive: Use CSS Grid with `grid-template-columns: 3fr 2fr` collapsing to `1fr` via media query (left column wider for charts and work preview)
-
-## Trade-offs
-
-1. **No charting library** → Simple CSS bars. Pro: zero dependency. Con: no animations, no hover tooltips. Acceptable for MVP.
-2. **Activity from case objects** → Must load all cases. Pro: no new API. Con: scales poorly past ~1000 cases. V1 can add dedicated endpoint.
-3. **Client-side KPI computation** → All aggregation in JavaScript. Pro: no backend changes. Con: CPU cost for large datasets. Acceptable for MVP volume.
+# Design: dashboard-mvp
+
+## Architecture Overview
+
+This change rewrites the placeholder `Dashboard.vue` into a full dashboard with data-driven sections. All data flows through the existing `useObjectStore` Pinia store and OpenRegister API. No new backend code is needed — the dashboard is entirely frontend.
+
+```
+Dashboard.vue
+├── KpiCards.vue (4 metric cards in a flex row)
+├── StatusChart.vue (horizontal CSS bar chart)
+├── OverduePanel.vue (list of overdue cases)
+├── MyWorkPreview.vue (top 5 items: cases + tasks)
+├── ActivityFeed.vue (last 10 events from case activity arrays)
+└── CaseCreateDialog.vue (already exists from case-management)
+```
+
+## File Map
+
+### New Files
+
+| File | Purpose |
+|------|---------|
+| `src/utils/dashboardHelpers.js` | KPI calculation, activity aggregation, overdue sorting, status aggregation |
+| `src/views/dashboard/KpiCards.vue` | Row of 4 KPI metric cards with counts and sub-labels |
+| `src/views/dashboard/StatusChart.vue` | Horizontal bar chart of cases by status (pure CSS) |
+| `src/views/dashboard/OverduePanel.vue` | Scrollable list of overdue cases with severity |
+| `src/views/dashboard/MyWorkPreview.vue` | Top 5 most urgent assigned items |
+| `src/views/dashboard/ActivityFeed.vue` | Last 10 activity entries across all visible cases |
+
+### Modified Files
+
+| File | Changes |
+|------|---------|
+| `src/views/Dashboard.vue` | Complete rewrite: fetch data on mount, compose sub-components, loading/error/empty states, refresh button, quick actions |
+| `src/App.vue` | No changes needed — `Dashboard` component already referenced, default route is `dashboard` |
+
+### Unchanged Files
+
+| File | Reason |
+|------|--------|
+| `src/store/modules/object.js` | Generic CRUD store already supports `fetchCollection` with filters |
+| `src/store/store.js` | Object types (case, task, caseType, statusType) already registered |
+| `src/views/cases/CaseCreateDialog.vue` | Already exists from case-management change, reused as-is |
+
+## Design Decisions
+
+### DD-01: CSS Bar Chart (No Charting Library)
+
+**Decision**: Implement the status distribution chart using pure CSS — `div` bars with proportional `width` percentages.
+
+**Rationale**: Adding Chart.js or similar is ~200KB gzipped and overkill for a single horizontal bar chart. The chart only needs labeled bars proportional to counts. CSS flex/grid achieves this with zero dependencies.
+
+**Implementation**: Each bar is a `div` with `width: (count / maxCount * 100)%`, a background color from a 6-color palette, and labels showing status name (left) and count (right).
+
+### DD-02: Activity Aggregation from Case Objects
+
+**Decision**: Aggregate activity entries by iterating over all visible cases' `activity` arrays, merge into a single list, sort by date descending, take the top 10.
+
+**Rationale**: MVP stores activity as embedded arrays on case objects (design decision DD-03 from case-management). No separate activity API exists. Fetching all cases (which are already loaded for KPIs) and extracting their activity arrays is efficient for MVP scale (< 1000 cases).
+
+**Trade-off**: For large installations, this means loading all case objects to get activity. Acceptable for MVP; V1 can add a dedicated activity query endpoint.
+
+### DD-03: Parallel Data Fetching
+
+**Decision**: On mount, fire all data queries concurrently using `Promise.allSettled()`:
+1. Fetch all cases (open + completed) in a single query, split client-side using statusType `isFinal`
+2. Fetch case types for name resolution
+3. Fetch status types for status name resolution, chart labels, and open/completed splitting
+4. Fetch tasks assigned to current user for "My Tasks" KPI and My Work
+
+**Rationale**: Sequential fetching would multiply the load time. `Promise.allSettled` ensures partial failures don't block the entire dashboard (REQ-DASH-012). Fetching all cases in one query (instead of separate open/completed queries) reduces API calls from 5 to 4 while keeping the same data available.
+
+**Implementation**: Dashboard.vue calls a `loadDashboardData()` method that awaits `Promise.allSettled([fetchCases(), fetchCaseTypes(), fetchStatusTypes(), fetchTasks()])`. Cases are split into open vs completed client-side by checking each case's status against the statusType `isFinal` flag. Each section checks its own loading/error state independently.
+
+### DD-04: Section-Level Loading and Error States
+
+**Decision**: Each dashboard section (KPIs, chart, overdue, my work, activity) has independent loading/error state.
+
+**Rationale**: REQ-DASH-012 requires that "the system MUST NOT block the entire dashboard due to a single section failure." If activity loading fails, KPI cards and overdue panel still show.
+
+**Implementation**: Each section component receives `loading` and `error` props. When `loading`, show `NcLoadingIcon` or skeleton. When `error`, show inline error message with retry callback.
+
+### DD-05: KPI Sub-Label Calculations
+
+**Decision**: Calculate sub-labels client-side from fetched data:
+- Open Cases: count cases with `startDate == today` → "+N today"
+- Overdue: static text "action needed" when count > 0
+- Completed: average `(endDate - startDate)` in days → "avg N days"
+- My Tasks: count tasks with `dueDate == today` → "N due today"
+
+**Rationale**: All data needed for sub-labels is already fetched for the main counts. No additional API calls.
+
+### DD-06: My Work Preview Reuses My Work Sorting Logic
+
+**Decision**: Import sorting/grouping utilities from the existing `taskHelpers.js` and new `caseHelpers.js` to sort the top-5 items by priority then deadline.
+
+**Rationale**: The My Work preview (REQ-DASH-005) uses the same sort order as the full My Work view. Reusing the helpers ensures consistency.
+
+### DD-07: Overdue Panel Shows All (Scrollable)
+
+**Decision**: Show all overdue cases in a scrollable panel with `max-height: 300px` and `overflow-y: auto`, rather than limiting to N items.
+
+**Rationale**: REQ-DASH-004 says "show all overdue cases (or a scrollable list if many)." The "View all overdue" link navigates to case list filtered by overdue. The panel itself is a scrollable summary.
+
+## Component Props
+
+### KpiCards.vue
+```
+Props:
+ openCases: Number
+ newToday: Number
+ overdueCases: Number
+ completedThisMonth: Number
+ avgProcessingDays: Number | null
+ myTasks: Number
+ tasksDueToday: Number
+ loading: Boolean
+Events:
+ @click-card(cardId: String) — navigates to filtered view
+```
+
+### StatusChart.vue
+```
+Props:
+ statusData: Array<{ name: String, count: Number }>
+ loading: Boolean
+ error: String | null
+```
+
+### OverduePanel.vue
+```
+Props:
+ cases: Array<{ identifier, title, caseTypeName, daysOverdue, handler }>
+ loading: Boolean
+ error: String | null
+Events:
+ @view-all — navigates to cases filtered by overdue
+ @click-case(caseId) — navigates to case detail
+```
+
+### MyWorkPreview.vue
+```
+Props:
+ items: Array<{ type: 'case'|'task', title, reference, deadline, daysText, isOverdue, priority }>
+ loading: Boolean
+ error: String | null
+Events:
+ @view-all — navigates to My Work view
+ @click-item(type, id) — navigates to detail view
+```
+
+### ActivityFeed.vue
+```
+Props:
+ entries: Array<{ date, type, description, user, caseIdentifier }>
+ loading: Boolean
+ error: String | null
+Events:
+ @view-all — navigates to activity view
+```
+
+## Data Flow
+
+### Dashboard Mount Sequence
+1. Dashboard.vue `mounted()` calls `loadDashboardData()`
+2. Fire 4 parallel queries via `Promise.allSettled()`:
+ - `fetchCollection('case', { '_limit': 1000 })` — all cases (split into open/completed client-side)
+ - `fetchCollection('caseType', { '_limit': 100 })` — for case type name resolution
+ - `fetchCollection('statusType', { '_limit': 500 })` — for status name resolution, chart, and open/completed splitting
+ - `fetchCollection('task', { '_filters[assignee]': currentUser, '_limit': 100 })` — my tasks
+3. Split cases into open vs completed by checking each case's status against statusType `isFinal` flag
+4. On each resolved: compute derived data, pass to sub-components
+5. On rejection: set section-level error, show retry
+
+### KPI Computation (in dashboardHelpers.js)
+```javascript
+computeKpis(openCases, completedCases, myTasks) → {
+ openCount, newToday,
+ overdueCount,
+ completedCount, avgDays,
+ taskCount, tasksDueToday
+}
+```
+
+### Status Aggregation (in dashboardHelpers.js)
+```javascript
+aggregateByStatus(openCases, statusTypes) → [{ name, count }]
+// Groups by status name across case types, orders by status type order
+```
+
+### Overdue Extraction (in dashboardHelpers.js)
+```javascript
+getOverdueCases(openCases, caseTypes) → [{ identifier, title, caseTypeName, daysOverdue, handler }]
+// Filters deadline < today, sorts by daysOverdue desc
+```
+
+### Activity Aggregation (in dashboardHelpers.js)
+```javascript
+getRecentActivity(cases, limit = 10) → [{ date, type, description, user, caseIdentifier }]
+// Flattens all cases' activity arrays, sorts by date desc, takes top N
+```
+
+## Layout Structure
+
+```
+┌──────────────────────────────────────────────────────────┐
+│ [+ New Case] [Refresh] │
+├──────────┬──────────┬──────────┬──────────────────────────┤
+│ Open (24)│Overdue(3)│Done (12) │ My Tasks (7) │
+│ +3 today │ action! │ avg 18d │ 2 due today │
+├──────────┴──────────┴──────────┴──────────────────────────┤
+│ │ │
+│ Cases by Status │ Overdue Cases │
+│ ██████████ 8 │ #042 Bouwverg.. 5 days overdue │
+│ ███████ 6 │ #038 Subsidie.. 2 days overdue │
+│ █████ 5 │ [View all overdue →] │
+│ ███ 3 │ │
+│ ├──────────────────────────────────────│
+│ My Work (top 5) │ Recent Activity │
+│ [CASE] #042... │ Status changed on #042 - 10m ago │
+│ [TASK] Review.. │ Decision on #036 - 1h ago │
+│ [View all →] │ Task completed - 2h ago │
+│ │ [View all activity →] │
+└────────────────────┴──────────────────────────────────────┘
+```
+
+Responsive: below 768px, two columns collapse to single column with all sections stacked vertically.
+
+## Security Considerations
+
+- All data queries go through OpenRegister which enforces RBAC — no additional auth needed
+- No new API endpoints — frontend-only
+- No CSRF concerns — read-only data display
+
+## NL Design System
+
+- KPI cards: Use Nextcloud `NcNoteCard` or custom card styling with `--color-primary`, `--color-warning`, `--color-success` CSS variables
+- Text: Use `--font-face` and Nextcloud typography classes for consistent sizing
+- Colors: All chart colors from CSS custom properties, no hardcoded hex values
+- Responsive: Use CSS Grid with `grid-template-columns: 3fr 2fr` collapsing to `1fr` via media query (left column wider for charts and work preview)
+
+## Trade-offs
+
+1. **No charting library** → Simple CSS bars. Pro: zero dependency. Con: no animations, no hover tooltips. Acceptable for MVP.
+2. **Activity from case objects** → Must load all cases. Pro: no new API. Con: scales poorly past ~1000 cases. V1 can add dedicated endpoint.
+3. **Client-side KPI computation** → All aggregation in JavaScript. Pro: no backend changes. Con: CPU cost for large datasets. Acceptable for MVP volume.
diff --git a/openspec/changes/archive/2026-02-26-dashboard-mvp/proposal.md b/openspec/changes/archive/2026-02-26-dashboard-mvp/proposal.md
index 9b98b2bb..44b07f09 100644
--- a/openspec/changes/archive/2026-02-26-dashboard-mvp/proposal.md
+++ b/openspec/changes/archive/2026-02-26-dashboard-mvp/proposal.md
@@ -1,60 +1,60 @@
-# Proposal: dashboard-mvp
-
-## Summary
-
-Implement the MVP tier of the Procest dashboard — the landing page providing at-a-glance case management metrics, overdue alerts, personal workload preview, recent activity, and quick actions. Replaces the current placeholder Dashboard.vue with a full data-driven dashboard.
-
-## Motivation
-
-The current dashboard is a static placeholder with a welcome message and a button to navigate to cases. Users have no visibility into their case management workload without clicking through to the case list. The dashboard spec (REQ-DASH-001 through REQ-DASH-013) defines a comprehensive overview page with KPI cards, status distribution chart, overdue panel, my work preview, activity feed, and quick actions. Implementing the MVP tier gives case handlers immediate situational awareness on login.
-
-## Affected Projects
-
-- [ ] Project: `procest` — Rewrite Dashboard.vue with KPI cards, chart, overdue panel, my work preview, activity feed, quick actions; add dashboard helper utilities
-
-## Scope
-
-### In Scope (MVP)
-
-- **KPI Cards** (REQ-DASH-001): Open Cases, Overdue, Completed This Month (with avg processing days), My Tasks — all with sub-labels
-- **Cases by Status Chart** (REQ-DASH-002): Horizontal bar chart showing case count per status type
-- **Overdue Cases Panel** (REQ-DASH-004): List of overdue cases with days overdue, severity indicators, "View all" link
-- **My Work Preview** (REQ-DASH-005): Top 5 items (cases + tasks) sorted by priority/deadline, link to full My Work view
-- **Recent Activity Feed** (REQ-DASH-006): Last 10 activity events from case activity arrays
-- **Quick Actions** (REQ-DASH-007): "+ New Case" button opening CaseCreateDialog
-- **Data Scope / RBAC** (REQ-DASH-008): All metrics respect user's visible cases
-- **Empty State** (REQ-DASH-009): Welcome message for fresh installs, guidance for no-access users
-- **Refresh** (REQ-DASH-010): Load on mount, manual refresh button, loading skeletons, error handling
-- **Error Handling** (REQ-DASH-012): Graceful partial failures, deleted case type fallback
-- **Layout** (REQ-DASH-013): KPI row + two-column layout, responsive single column on narrow viewports
-
-### Out of Scope (V1)
-
-- Cases by Type chart (REQ-DASH-003)
-- Average Processing Time as separate KPI (REQ-DASH-011) — basic avg is included in "Completed This Month" sub-label
-- Auto-refresh on timer interval
-- Dashboard widget for Nextcloud Dashboard app
-
-## Approach
-
-Frontend-only implementation. Rewrite the existing `Dashboard.vue` from a placeholder into a full component that fetches data via `useObjectStore`. New utility module `dashboardHelpers.js` for KPI calculations, date grouping, and data aggregation. The horizontal bar chart uses pure CSS (no charting library) — proportional-width divs with labels.
-
-Key decisions:
-1. **No charting library** — CSS bar chart keeps bundle small and avoids a new dependency
-2. **Activity from case objects** — Aggregate `activity` arrays from recent cases (no Nextcloud Activity API for MVP)
-3. **Parallel data fetching** — KPI queries, overdue list, my work items, and activity all fetched concurrently on mount
-4. **Loading skeletons** — Use Nextcloud's `NcLoadingIcon` or shimmer placeholders per section
-
-## Cross-Project Dependencies
-
-- **case-management** (in progress) — Dashboard consumes case data with case type integration, status history, activity arrays, deadlines. The dashboard can be implemented in parallel but will need case-management's enhanced data model to be meaningful.
-- **task-management** (archived) — My Tasks KPI and My Work preview query tasks
-- **OpenRegister** — All data queries via `fetchCollection`
-
-## Rollback Strategy
-
-All changes are frontend-only (Dashboard.vue rewrite + new utility file). Rollback by reverting to the placeholder Dashboard.vue. No database migrations or schema changes.
-
-## Open Questions
-
-None — the spec is comprehensive and the data model is established by the case-management and task-management changes.
+# Proposal: dashboard-mvp
+
+## Summary
+
+Implement the MVP tier of the Procest dashboard — the landing page providing at-a-glance case management metrics, overdue alerts, personal workload preview, recent activity, and quick actions. Replaces the current placeholder Dashboard.vue with a full data-driven dashboard.
+
+## Motivation
+
+The current dashboard is a static placeholder with a welcome message and a button to navigate to cases. Users have no visibility into their case management workload without clicking through to the case list. The dashboard spec (REQ-DASH-001 through REQ-DASH-013) defines a comprehensive overview page with KPI cards, status distribution chart, overdue panel, my work preview, activity feed, and quick actions. Implementing the MVP tier gives case handlers immediate situational awareness on login.
+
+## Affected Projects
+
+- [ ] Project: `procest` — Rewrite Dashboard.vue with KPI cards, chart, overdue panel, my work preview, activity feed, quick actions; add dashboard helper utilities
+
+## Scope
+
+### In Scope (MVP)
+
+- **KPI Cards** (REQ-DASH-001): Open Cases, Overdue, Completed This Month (with avg processing days), My Tasks — all with sub-labels
+- **Cases by Status Chart** (REQ-DASH-002): Horizontal bar chart showing case count per status type
+- **Overdue Cases Panel** (REQ-DASH-004): List of overdue cases with days overdue, severity indicators, "View all" link
+- **My Work Preview** (REQ-DASH-005): Top 5 items (cases + tasks) sorted by priority/deadline, link to full My Work view
+- **Recent Activity Feed** (REQ-DASH-006): Last 10 activity events from case activity arrays
+- **Quick Actions** (REQ-DASH-007): "+ New Case" button opening CaseCreateDialog
+- **Data Scope / RBAC** (REQ-DASH-008): All metrics respect user's visible cases
+- **Empty State** (REQ-DASH-009): Welcome message for fresh installs, guidance for no-access users
+- **Refresh** (REQ-DASH-010): Load on mount, manual refresh button, loading skeletons, error handling
+- **Error Handling** (REQ-DASH-012): Graceful partial failures, deleted case type fallback
+- **Layout** (REQ-DASH-013): KPI row + two-column layout, responsive single column on narrow viewports
+
+### Out of Scope (V1)
+
+- Cases by Type chart (REQ-DASH-003)
+- Average Processing Time as separate KPI (REQ-DASH-011) — basic avg is included in "Completed This Month" sub-label
+- Auto-refresh on timer interval
+- Dashboard widget for Nextcloud Dashboard app
+
+## Approach
+
+Frontend-only implementation. Rewrite the existing `Dashboard.vue` from a placeholder into a full component that fetches data via `useObjectStore`. New utility module `dashboardHelpers.js` for KPI calculations, date grouping, and data aggregation. The horizontal bar chart uses pure CSS (no charting library) — proportional-width divs with labels.
+
+Key decisions:
+1. **No charting library** — CSS bar chart keeps bundle small and avoids a new dependency
+2. **Activity from case objects** — Aggregate `activity` arrays from recent cases (no Nextcloud Activity API for MVP)
+3. **Parallel data fetching** — KPI queries, overdue list, my work items, and activity all fetched concurrently on mount
+4. **Loading skeletons** — Use Nextcloud's `NcLoadingIcon` or shimmer placeholders per section
+
+## Cross-Project Dependencies
+
+- **case-management** (in progress) — Dashboard consumes case data with case type integration, status history, activity arrays, deadlines. The dashboard can be implemented in parallel but will need case-management's enhanced data model to be meaningful.
+- **task-management** (archived) — My Tasks KPI and My Work preview query tasks
+- **OpenRegister** — All data queries via `fetchCollection`
+
+## Rollback Strategy
+
+All changes are frontend-only (Dashboard.vue rewrite + new utility file). Rollback by reverting to the placeholder Dashboard.vue. No database migrations or schema changes.
+
+## Open Questions
+
+None — the spec is comprehensive and the data model is established by the case-management and task-management changes.
diff --git a/openspec/changes/archive/2026-02-26-dashboard-mvp/review.md b/openspec/changes/archive/2026-02-26-dashboard-mvp/review.md
index 1f29cdb7..83ff3107 100644
--- a/openspec/changes/archive/2026-02-26-dashboard-mvp/review.md
+++ b/openspec/changes/archive/2026-02-26-dashboard-mvp/review.md
@@ -1,84 +1,84 @@
-# Review: dashboard-mvp
-
-## Summary
-- Tasks completed: 8/8
-- GitHub issues closed: N/A (used `/opsx:ff`, no `plan.json` or GitHub issues created)
-- Spec compliance: **PASS** (with warnings)
-
-## Verification
-
-### Task Completion
-All 8 implementation tasks (T01-T08) are marked complete in `tasks.md`. Verification tasks V01-V13 remain unchecked (manual browser testing).
-
-### Files Created/Modified
-All 6 new files exist and are syntactically valid:
-- `src/utils/dashboardHelpers.js`
-- `src/views/dashboard/KpiCards.vue`
-- `src/views/dashboard/StatusChart.vue`
-- `src/views/dashboard/OverduePanel.vue`
-- `src/views/dashboard/MyWorkPreview.vue`
-- `src/views/dashboard/ActivityFeed.vue`
-
-Modified file:
-- `src/views/Dashboard.vue` — full rewrite from placeholder
-
-### Import Verification
-- `prioritySortWeight` from `taskHelpers.js` — confirmed exists (line 101)
-- `isTerminalStatus` from `taskLifecycle.js` — confirmed exists (line 95)
-- `isCaseOverdue`, `getDaysRemaining`, `formatDeadlineCountdown` from `caseHelpers.js` — confirmed (case-management change)
-
----
-
-## Requirement-by-Requirement Verification
-
-| Requirement | Status | Notes |
-|-------------|--------|-------|
-| KPI Cards Row (REQ-DASH-001) | PASS | 4 cards with correct counts and sub-labels. Click navigation works. |
-| Cases by Status Chart (REQ-DASH-002) | PASS | Proportional CSS bars, 6-color palette, same-name aggregation, loading/error/empty states |
-| Overdue Cases Panel (REQ-DASH-004) | PASS | Sorted by severity, scrollable, severity color bands, click navigation, "View all" link |
-| My Work Preview (REQ-DASH-005) | PASS | Top 5 items, priority+deadline sort, type badges, overdue indicators, priority icons |
-| Recent Activity Feed (REQ-DASH-006) | PASS | Last 10 entries, type icons, relative timestamps, case references |
-| Quick Actions (REQ-DASH-007) | PASS | "+ New Case" button opens CaseCreateDialog, navigates on creation |
-| Dashboard Data Scope (REQ-DASH-008) | PASS | All queries via OpenRegister which enforces RBAC |
-| Empty State (REQ-DASH-009) | PASS (with warning) | Welcome message shown, admin/non-admin guidance. See W01. |
-| Dashboard Refresh (REQ-DASH-010) | PASS | Load on mount, refresh button, section-level loading skeletons |
-| Error Handling (REQ-DASH-012) | PASS | Section-level try/catch, error messages with retry |
-| Dashboard Layout (REQ-DASH-013) | PASS | KPI row + 3fr/2fr grid, responsive @768px breakpoint |
-
----
-
-## Findings
-
-### CRITICAL
-None.
-
-### WARNING
-- [ ] **W01**: REQ-DASH-009 — Empty state uses `v-if/v-else` which hides KPI cards entirely when the welcome message is shown. The spec says "THEN a welcome message MUST be displayed AND KPI cards MUST show '0' without errors" — implying both should be visible simultaneously. Current implementation shows only the welcome message without KPI cards. From a UX perspective this is reasonable (zeros are meaningless on a fresh install), but it deviates from the spec letter.
-
-- [ ] **W02**: REQ-DASH-012 — `retrySection(section)` calls `loadDashboardData()` which re-fetches ALL data, not just the failed section. This means clicking "Retry" on the activity feed reloads cases, case types, status types, and tasks too. Functionally correct but inefficient and may cause unnecessary loading flickers in sections that were already loaded.
-
-### SUGGESTION
-- The `isFinal` check in `Dashboard.vue:201` uses loose truthiness (`!st?.isFinal`) instead of explicit `=== true || === 'true'` matching. This works correctly because JavaScript's truthiness handles both boolean `true` and string `'true'` — but it's inconsistent with the explicit checks used everywhere else (CaseList, CaseDetail, DeadlinePanel). Consider aligning for consistency.
-- `ActivityFeed.vue:29` uses `v-html="typeIcon(entry.type)"` with HTML entity strings. While the icon values are hardcoded constants (not user input), `v-html` is generally discouraged for XSS safety. Consider using Unicode characters directly instead of HTML entities to use `{{ }}` interpolation.
-- The dashboard fetches up to 1000 cases on every mount/refresh. For large installations this could be slow. The design doc acknowledges this as a known MVP trade-off.
-- The `My Work` section only shows cases where `assignee === currentUser` (exact string match). If `OC.currentUser` returns a different format than what's stored in the case's `assignee` field, items will be missed. This is a data contract concern, not a code bug.
-
----
-
-## Cross-Reference with Shared Specs
-
-| Shared Spec | Status | Notes |
-|-------------|--------|-------|
-| nextcloud-app | PASS | No PHP/routing changes; frontend only |
-| api-patterns | N/A | No new API endpoints |
-| nl-design | PASS | All colors from CSS variables; responsive layout; no hardcoded hex values |
-| docker | PASS | No Docker changes |
-
----
-
-## Recommendation
-**APPROVE** — All 11 MVP requirements are implemented with correct behavior. The 2 warnings are minor:
-- W01 (empty state KPIs) is a UX decision that arguably improves the fresh-install experience
-- W02 (retry reloads all) is an efficiency concern, not a correctness issue
-
-No blocking issues. Safe to archive.
+# Review: dashboard-mvp
+
+## Summary
+- Tasks completed: 8/8
+- GitHub issues closed: N/A (used `/opsx:ff`, no `plan.json` or GitHub issues created)
+- Spec compliance: **PASS** (with warnings)
+
+## Verification
+
+### Task Completion
+All 8 implementation tasks (T01-T08) are marked complete in `tasks.md`. Verification tasks V01-V13 remain unchecked (manual browser testing).
+
+### Files Created/Modified
+All 6 new files exist and are syntactically valid:
+- `src/utils/dashboardHelpers.js`
+- `src/views/dashboard/KpiCards.vue`
+- `src/views/dashboard/StatusChart.vue`
+- `src/views/dashboard/OverduePanel.vue`
+- `src/views/dashboard/MyWorkPreview.vue`
+- `src/views/dashboard/ActivityFeed.vue`
+
+Modified file:
+- `src/views/Dashboard.vue` — full rewrite from placeholder
+
+### Import Verification
+- `prioritySortWeight` from `taskHelpers.js` — confirmed exists (line 101)
+- `isTerminalStatus` from `taskLifecycle.js` — confirmed exists (line 95)
+- `isCaseOverdue`, `getDaysRemaining`, `formatDeadlineCountdown` from `caseHelpers.js` — confirmed (case-management change)
+
+---
+
+## Requirement-by-Requirement Verification
+
+| Requirement | Status | Notes |
+|-------------|--------|-------|
+| KPI Cards Row (REQ-DASH-001) | PASS | 4 cards with correct counts and sub-labels. Click navigation works. |
+| Cases by Status Chart (REQ-DASH-002) | PASS | Proportional CSS bars, 6-color palette, same-name aggregation, loading/error/empty states |
+| Overdue Cases Panel (REQ-DASH-004) | PASS | Sorted by severity, scrollable, severity color bands, click navigation, "View all" link |
+| My Work Preview (REQ-DASH-005) | PASS | Top 5 items, priority+deadline sort, type badges, overdue indicators, priority icons |
+| Recent Activity Feed (REQ-DASH-006) | PASS | Last 10 entries, type icons, relative timestamps, case references |
+| Quick Actions (REQ-DASH-007) | PASS | "+ New Case" button opens CaseCreateDialog, navigates on creation |
+| Dashboard Data Scope (REQ-DASH-008) | PASS | All queries via OpenRegister which enforces RBAC |
+| Empty State (REQ-DASH-009) | PASS (with warning) | Welcome message shown, admin/non-admin guidance. See W01. |
+| Dashboard Refresh (REQ-DASH-010) | PASS | Load on mount, refresh button, section-level loading skeletons |
+| Error Handling (REQ-DASH-012) | PASS | Section-level try/catch, error messages with retry |
+| Dashboard Layout (REQ-DASH-013) | PASS | KPI row + 3fr/2fr grid, responsive @768px breakpoint |
+
+---
+
+## Findings
+
+### CRITICAL
+None.
+
+### WARNING
+- [ ] **W01**: REQ-DASH-009 — Empty state uses `v-if/v-else` which hides KPI cards entirely when the welcome message is shown. The spec says "THEN a welcome message MUST be displayed AND KPI cards MUST show '0' without errors" — implying both should be visible simultaneously. Current implementation shows only the welcome message without KPI cards. From a UX perspective this is reasonable (zeros are meaningless on a fresh install), but it deviates from the spec letter.
+
+- [ ] **W02**: REQ-DASH-012 — `retrySection(section)` calls `loadDashboardData()` which re-fetches ALL data, not just the failed section. This means clicking "Retry" on the activity feed reloads cases, case types, status types, and tasks too. Functionally correct but inefficient and may cause unnecessary loading flickers in sections that were already loaded.
+
+### SUGGESTION
+- The `isFinal` check in `Dashboard.vue:201` uses loose truthiness (`!st?.isFinal`) instead of explicit `=== true || === 'true'` matching. This works correctly because JavaScript's truthiness handles both boolean `true` and string `'true'` — but it's inconsistent with the explicit checks used everywhere else (CaseList, CaseDetail, DeadlinePanel). Consider aligning for consistency.
+- `ActivityFeed.vue:29` uses `v-html="typeIcon(entry.type)"` with HTML entity strings. While the icon values are hardcoded constants (not user input), `v-html` is generally discouraged for XSS safety. Consider using Unicode characters directly instead of HTML entities to use `{{ }}` interpolation.
+- The dashboard fetches up to 1000 cases on every mount/refresh. For large installations this could be slow. The design doc acknowledges this as a known MVP trade-off.
+- The `My Work` section only shows cases where `assignee === currentUser` (exact string match). If `OC.currentUser` returns a different format than what's stored in the case's `assignee` field, items will be missed. This is a data contract concern, not a code bug.
+
+---
+
+## Cross-Reference with Shared Specs
+
+| Shared Spec | Status | Notes |
+|-------------|--------|-------|
+| nextcloud-app | PASS | No PHP/routing changes; frontend only |
+| api-patterns | N/A | No new API endpoints |
+| nl-design | PASS | All colors from CSS variables; responsive layout; no hardcoded hex values |
+| docker | PASS | No Docker changes |
+
+---
+
+## Recommendation
+**APPROVE** — All 11 MVP requirements are implemented with correct behavior. The 2 warnings are minor:
+- W01 (empty state KPIs) is a UX decision that arguably improves the fresh-install experience
+- W02 (retry reloads all) is an efficiency concern, not a correctness issue
+
+No blocking issues. Safe to archive.
diff --git a/openspec/changes/archive/2026-02-26-dashboard-mvp/specs/dashboard/spec.md b/openspec/changes/archive/2026-02-26-dashboard-mvp/specs/dashboard/spec.md
index 841569cf..89cd6016 100644
--- a/openspec/changes/archive/2026-02-26-dashboard-mvp/specs/dashboard/spec.md
+++ b/openspec/changes/archive/2026-02-26-dashboard-mvp/specs/dashboard/spec.md
@@ -1,150 +1,150 @@
-# Dashboard MVP — Delta Specification
-
-## Purpose
-
-Implement the MVP tier of the dashboard spec (`openspec/specs/dashboard/spec.md`). This change implements all requirements tagged [MVP] from the main spec without modifications.
-
-## ADDED Requirements
-
-All requirements below reference the main spec verbatim. No behavioral changes — this delta confirms the MVP scope being implemented.
-
-### Requirement: KPI Cards Row
-
-The system MUST display four KPI cards: Open Cases (+N today), Overdue (action needed), Completed This Month (avg N days), My Tasks (N due today). Per REQ-DASH-001.
-
-#### Scenario: KPI cards display correct counts
-- GIVEN the dashboard loads with cases and tasks in OpenRegister
-- WHEN the user views the dashboard
-- THEN four KPI cards MUST display with correct counts and sub-labels
-- AND counts MUST only include cases visible to the current user (RBAC)
-
-#### Scenario: Zero state for all KPI cards
-- GIVEN no cases or tasks exist
-- WHEN the user views the dashboard
-- THEN all KPI cards MUST show "0" with graceful sub-labels
-- AND no errors or broken layouts MUST appear
-
-### Requirement: Cases by Status Chart
-
-The system MUST display a horizontal bar chart showing open case distribution by status type. Per REQ-DASH-002.
-
-#### Scenario: Status bars render proportionally
-- GIVEN open cases distributed across multiple statuses
-- WHEN the user views the dashboard
-- THEN a horizontal bar chart MUST show each status with name and count
-- AND bar widths MUST be proportional to count relative to maximum
-
-#### Scenario: Same-named statuses aggregate
-- GIVEN multiple case types with identically named statuses
-- WHEN the user views the status chart
-- THEN counts MUST be aggregated by status name across all case types
-
-### Requirement: Overdue Cases Panel
-
-The system MUST display a panel listing overdue cases sorted by severity. Per REQ-DASH-004.
-
-#### Scenario: Overdue cases listed with details
-- GIVEN cases with deadline before today and non-final status
-- WHEN the user views the dashboard
-- THEN the overdue panel MUST list each case with identifier, title, type, days overdue, and handler
-- AND cases MUST be sorted by days overdue descending
-
-#### Scenario: No overdue cases
-- GIVEN all open cases have deadline >= today
-- WHEN the user views the dashboard
-- THEN the overdue panel MUST show a positive message or be hidden
-
-### Requirement: My Work Preview
-
-The system MUST display the top 5 most urgent items assigned to the current user. Per REQ-DASH-005.
-
-#### Scenario: Top 5 items shown
-- GIVEN the user has assigned cases and tasks
-- WHEN the user views the dashboard
-- THEN a "My Work" panel MUST show the top 5 items sorted by priority then deadline
-- AND each item MUST show entity type badge, title, deadline, and overdue status
-
-#### Scenario: View all link
-- GIVEN the My Work preview is displayed
-- WHEN the user clicks "View all my work"
-- THEN the system MUST navigate to the full My Work view
-
-### Requirement: Recent Activity Feed
-
-The system MUST display the last 10 case management events. Per REQ-DASH-006.
-
-#### Scenario: Activity feed from case objects
-- GIVEN cases with activity arrays containing status changes, updates, notes
-- WHEN the user views the dashboard
-- THEN the activity feed MUST show the 10 most recent entries across all visible cases
-- AND each entry MUST show description, user, and relative timestamp
-
-### Requirement: Quick Actions
-
-The system MUST provide a "+ New Case" button. Per REQ-DASH-007.
-
-#### Scenario: New case button opens dialog
-- GIVEN the user is on the dashboard
-- WHEN they click "+ New Case"
-- THEN the CaseCreateDialog MUST open
-
-### Requirement: Dashboard Data Scope
-
-All dashboard metrics MUST respect RBAC — only cases visible to the current user. Per REQ-DASH-008.
-
-#### Scenario: User sees only their permitted data
-- GIVEN user A has access to 20 cases and user B has access to 15
-- WHEN user A views the dashboard
-- THEN all counts and panels MUST reflect only user A's 20 cases
-
-### Requirement: Empty State
-
-The dashboard MUST show a helpful setup message when no cases exist. Per REQ-DASH-009.
-
-#### Scenario: Fresh install empty state
-- GIVEN no cases or case types exist
-- WHEN the user views the dashboard
-- THEN a welcome message with setup guidance MUST be displayed
-- AND KPI cards MUST show "0" without errors
-
-### Requirement: Dashboard Refresh
-
-The dashboard MUST load on mount and support manual refresh. Per REQ-DASH-010.
-
-#### Scenario: Load with skeletons
-- GIVEN the user navigates to the dashboard
-- WHEN data is loading
-- THEN loading skeletons MUST be shown per section
-- AND stale data from previous sessions MUST NOT be displayed
-
-#### Scenario: Manual refresh
-- GIVEN the user clicks the refresh button
-- WHEN the API is available
-- THEN all dashboard data MUST be re-fetched and displayed fresh
-
-### Requirement: Error Handling
-
-The dashboard MUST handle errors gracefully. Per REQ-DASH-012.
-
-#### Scenario: Partial section failure
-- GIVEN cases load successfully but activity fails
-- WHEN the user views the dashboard
-- THEN available data MUST be displayed
-- AND the failed section MUST show an error with retry option
-
-### Requirement: Dashboard Layout
-
-The dashboard MUST follow the defined layout structure. Per REQ-DASH-013.
-
-#### Scenario: Two-column responsive layout
-- GIVEN the user views the dashboard on a wide viewport
-- THEN the layout MUST show KPI row at top, then two columns (left: chart + my work; right: overdue + activity)
-- AND on narrow viewports the layout MUST collapse to single column
-
-## MODIFIED Requirements
-
-None — the main spec is being implemented as-is.
-
-## REMOVED Requirements
-
-None.
+# Dashboard MVP — Delta Specification
+
+## Purpose
+
+Implement the MVP tier of the dashboard spec (`openspec/specs/dashboard/spec.md`). This change implements all requirements tagged [MVP] from the main spec without modifications.
+
+## ADDED Requirements
+
+All requirements below reference the main spec verbatim. No behavioral changes — this delta confirms the MVP scope being implemented.
+
+### Requirement: KPI Cards Row
+
+The system MUST display four KPI cards: Open Cases (+N today), Overdue (action needed), Completed This Month (avg N days), My Tasks (N due today). Per REQ-DASH-001.
+
+#### Scenario: KPI cards display correct counts
+- GIVEN the dashboard loads with cases and tasks in OpenRegister
+- WHEN the user views the dashboard
+- THEN four KPI cards MUST display with correct counts and sub-labels
+- AND counts MUST only include cases visible to the current user (RBAC)
+
+#### Scenario: Zero state for all KPI cards
+- GIVEN no cases or tasks exist
+- WHEN the user views the dashboard
+- THEN all KPI cards MUST show "0" with graceful sub-labels
+- AND no errors or broken layouts MUST appear
+
+### Requirement: Cases by Status Chart
+
+The system MUST display a horizontal bar chart showing open case distribution by status type. Per REQ-DASH-002.
+
+#### Scenario: Status bars render proportionally
+- GIVEN open cases distributed across multiple statuses
+- WHEN the user views the dashboard
+- THEN a horizontal bar chart MUST show each status with name and count
+- AND bar widths MUST be proportional to count relative to maximum
+
+#### Scenario: Same-named statuses aggregate
+- GIVEN multiple case types with identically named statuses
+- WHEN the user views the status chart
+- THEN counts MUST be aggregated by status name across all case types
+
+### Requirement: Overdue Cases Panel
+
+The system MUST display a panel listing overdue cases sorted by severity. Per REQ-DASH-004.
+
+#### Scenario: Overdue cases listed with details
+- GIVEN cases with deadline before today and non-final status
+- WHEN the user views the dashboard
+- THEN the overdue panel MUST list each case with identifier, title, type, days overdue, and handler
+- AND cases MUST be sorted by days overdue descending
+
+#### Scenario: No overdue cases
+- GIVEN all open cases have deadline >= today
+- WHEN the user views the dashboard
+- THEN the overdue panel MUST show a positive message or be hidden
+
+### Requirement: My Work Preview
+
+The system MUST display the top 5 most urgent items assigned to the current user. Per REQ-DASH-005.
+
+#### Scenario: Top 5 items shown
+- GIVEN the user has assigned cases and tasks
+- WHEN the user views the dashboard
+- THEN a "My Work" panel MUST show the top 5 items sorted by priority then deadline
+- AND each item MUST show entity type badge, title, deadline, and overdue status
+
+#### Scenario: View all link
+- GIVEN the My Work preview is displayed
+- WHEN the user clicks "View all my work"
+- THEN the system MUST navigate to the full My Work view
+
+### Requirement: Recent Activity Feed
+
+The system MUST display the last 10 case management events. Per REQ-DASH-006.
+
+#### Scenario: Activity feed from case objects
+- GIVEN cases with activity arrays containing status changes, updates, notes
+- WHEN the user views the dashboard
+- THEN the activity feed MUST show the 10 most recent entries across all visible cases
+- AND each entry MUST show description, user, and relative timestamp
+
+### Requirement: Quick Actions
+
+The system MUST provide a "+ New Case" button. Per REQ-DASH-007.
+
+#### Scenario: New case button opens dialog
+- GIVEN the user is on the dashboard
+- WHEN they click "+ New Case"
+- THEN the CaseCreateDialog MUST open
+
+### Requirement: Dashboard Data Scope
+
+All dashboard metrics MUST respect RBAC — only cases visible to the current user. Per REQ-DASH-008.
+
+#### Scenario: User sees only their permitted data
+- GIVEN user A has access to 20 cases and user B has access to 15
+- WHEN user A views the dashboard
+- THEN all counts and panels MUST reflect only user A's 20 cases
+
+### Requirement: Empty State
+
+The dashboard MUST show a helpful setup message when no cases exist. Per REQ-DASH-009.
+
+#### Scenario: Fresh install empty state
+- GIVEN no cases or case types exist
+- WHEN the user views the dashboard
+- THEN a welcome message with setup guidance MUST be displayed
+- AND KPI cards MUST show "0" without errors
+
+### Requirement: Dashboard Refresh
+
+The dashboard MUST load on mount and support manual refresh. Per REQ-DASH-010.
+
+#### Scenario: Load with skeletons
+- GIVEN the user navigates to the dashboard
+- WHEN data is loading
+- THEN loading skeletons MUST be shown per section
+- AND stale data from previous sessions MUST NOT be displayed
+
+#### Scenario: Manual refresh
+- GIVEN the user clicks the refresh button
+- WHEN the API is available
+- THEN all dashboard data MUST be re-fetched and displayed fresh
+
+### Requirement: Error Handling
+
+The dashboard MUST handle errors gracefully. Per REQ-DASH-012.
+
+#### Scenario: Partial section failure
+- GIVEN cases load successfully but activity fails
+- WHEN the user views the dashboard
+- THEN available data MUST be displayed
+- AND the failed section MUST show an error with retry option
+
+### Requirement: Dashboard Layout
+
+The dashboard MUST follow the defined layout structure. Per REQ-DASH-013.
+
+#### Scenario: Two-column responsive layout
+- GIVEN the user views the dashboard on a wide viewport
+- THEN the layout MUST show KPI row at top, then two columns (left: chart + my work; right: overdue + activity)
+- AND on narrow viewports the layout MUST collapse to single column
+
+## MODIFIED Requirements
+
+None — the main spec is being implemented as-is.
+
+## REMOVED Requirements
+
+None.
diff --git a/openspec/changes/archive/2026-02-26-dashboard-mvp/tasks.md b/openspec/changes/archive/2026-02-26-dashboard-mvp/tasks.md
index 63b88c2a..5e73932b 100644
--- a/openspec/changes/archive/2026-02-26-dashboard-mvp/tasks.md
+++ b/openspec/changes/archive/2026-02-26-dashboard-mvp/tasks.md
@@ -1,43 +1,43 @@
-# Tasks: Dashboard MVP
-
-## Implementation Tasks
-
-### Utility Module
-
-- [x] **T01**: Create `src/utils/dashboardHelpers.js` — Export functions: `computeKpis(openCases, completedCases, myTasks)` (returns `{ openCount, newToday, overdueCount, completedCount, avgDays, taskCount, tasksDueToday }`), `aggregateByStatus(openCases, statusTypes)` (returns `[{ name, count }]` ordered by status type `order`; aggregates same-named statuses across case types), `getOverdueCases(openCases, caseTypes)` (returns `[{ id, identifier, title, caseTypeName, daysOverdue, handler }]` sorted by daysOverdue desc; uses `isCaseOverdue` from `caseHelpers.js`), `getRecentActivity(cases, limit = 10)` (flattens all cases' `activity` arrays, adds `caseIdentifier` to each entry, sorts by date desc, returns top `limit`), `getMyWorkItems(cases, tasks, limit = 5)` (merges cases + tasks into unified items with `{ type, id, title, reference, deadline, daysText, isOverdue, priority }`, sorts by priority then deadline asc, returns top `limit`). Import `isCaseOverdue`, `getDaysRemaining`, `formatDeadlineCountdown` from `caseHelpers.js`. Today checks: `new Date().toISOString().slice(0, 10)`.
-
-### Sub-Components
-
-- [x] **T02**: Create `src/views/dashboard/KpiCards.vue` — Row of 4 metric cards using CSS Grid (4 columns, collapses to 2x2 on narrow). Each card: title (h3), count (large number), sub-label (small text). Props: `openCases` (Number), `newToday` (Number), `overdueCases` (Number), `completedThisMonth` (Number), `avgProcessingDays` (Number|null), `myTasks` (Number), `tasksDueToday` (Number), `loading` (Boolean). When `loading`, show `NcLoadingIcon` inside each card. Card 1: "Open Cases" / count / "+N today". Card 2: "Overdue" / count / "action needed" (red) when > 0 or "all on track" when 0. Card 3: "Completed This Month" / count / "avg N days" or "no data". Card 4: "My Tasks" / count / "N due today". Emit `@click-card(cardId)` on card click (cardId = 'open', 'overdue', 'completed', 'tasks'). Overdue card: red border when count > 0. Use `--color-warning` for overdue, `--color-success` for completed, `--color-primary` for others.
-
-- [x] **T03**: Create `src/views/dashboard/StatusChart.vue` — Horizontal bar chart, pure CSS. Props: `statusData` (Array of `{ name, count }`), `loading` (Boolean), `error` (String|null). Title: "Cases by Status". Each bar: `div` with `width: (count / maxCount * 100)%`, minimum 20px for visibility. Status name left-aligned, count right-aligned. 6-color palette from CSS variables (`--color-primary`, `--color-primary-element-light`, `--color-warning`, `--color-success`, `--color-error`, `--color-text-maxcontrast`), cycling. Empty state: "No open cases" message. Error state: show `error` with retry button (emit `@retry`). Loading state: 4 skeleton bars.
-
-- [x] **T04**: Create `src/views/dashboard/OverduePanel.vue` — Scrollable list (max-height 300px). Props: `cases` (Array of `{ id, identifier, title, caseTypeName, daysOverdue, handler }`), `loading` (Boolean), `error` (String|null). Title: "Overdue Cases" with count badge. Each row: identifier (bold), title (truncated), case type (muted), "N days overdue" (red text), handler name. Click row → emit `@click-case(caseId)`. Footer: "View all overdue" link → emit `@view-all`. Empty state: green checkmark + "No overdue cases" when list is empty. Severity: > 7 days overdue gets red background tint, 3-7 gets orange, 1-2 gets yellow.
-
-- [x] **T05**: Create `src/views/dashboard/MyWorkPreview.vue` — List of top 5 items. Props: `items` (Array of `{ type, id, title, reference, deadline, daysText, isOverdue, priority }`), `loading` (Boolean), `error` (String|null). Title: "My Work". Each row: type badge ([CASE] blue / [TASK] green), title, reference/case type (muted), deadline with `daysText`, overdue indicator (red text if overdue), priority icon for high/urgent. Click row → emit `@click-item(type, id)`. Footer: "View all my work" link → emit `@view-all`. Empty state: "No items assigned to you". Loading: 5 skeleton rows.
-
-- [x] **T06**: Create `src/views/dashboard/ActivityFeed.vue` — Reverse-chronological event list (last 10). Props: `entries` (Array of `{ date, type, description, user, caseIdentifier }`), `loading` (Boolean), `error` (String|null). Title: "Recent Activity". Each entry: type icon (same icons as ActivityTimeline — plus/arrow/pencil/clock/comment), description text, "by {user}" (muted), relative timestamp (e.g., "10 min ago", "1 hour ago", "yesterday"), case reference "#{identifier}" (muted). Footer: "View all activity" link → emit `@view-all`. Empty state: "No recent activity". Loading: 5 skeleton entries.
-
-### Main Dashboard Rewrite
-
-- [x] **T07**: Rewrite `src/views/Dashboard.vue` — Replace placeholder with full dashboard. (a) Import all 5 sub-components + CaseCreateDialog + `dashboardHelpers.js`. (b) Data: `loading` (Boolean), `error` (String|null), `openCases` (Array), `completedCases` (Array), `myTasks` (Array), `caseTypes` (Array), `statusTypes` (Array), `showCreateDialog` (Boolean), section-level loading/error flags for each section. (c) On `mounted()`: call `loadDashboardData()` which fires 5 parallel `fetchCollection` queries via `Promise.allSettled()`: open cases, completed cases this month, my tasks, case types, status types. (d) On resolve: compute KPIs via `computeKpis()`, status data via `aggregateByStatus()`, overdue list via `getOverdueCases()`, my work items via `getMyWorkItems()`, activity via `getRecentActivity()`. Pass computed data as props to sub-components. (e) Quick actions: "+ New Case" button top-right opens CaseCreateDialog. On `@created`: navigate to case detail. (f) Refresh: button top-right triggers `loadDashboardData()` again. (g) Empty state: when no cases AND no case types exist, show welcome message with setup guidance (admin: "Create your first case type in Settings"; non-admin: "The app needs configuration by an administrator"). (h) Layout: CSS Grid — KPI row full width, then two columns (left 60% / right 40%), responsive to single column below 768px. (i) Section error handling: each section independently shows data or error with retry.
-
-### Navigation Integration
-
-- [x] **T08**: Wire dashboard card clicks and panel links — In Dashboard.vue: handle `@click-card` from KpiCards (overdue → `navigateTo('cases')` with overdue filter hash, tasks → `navigateTo('tasks')`), handle `@view-all` from OverduePanel (navigate to cases with `#/cases?overdue=true`), handle `@view-all` from MyWorkPreview (navigate to `#/my-work`), handle `@click-case` from OverduePanel (navigate to `#/cases/{id}`), handle `@click-item` from MyWorkPreview (navigate to `#/cases/{id}` or `#/tasks/{id}` based on type). All navigation via `this.$emit('navigate', route, id)` matching App.vue's handler.
-
-## Verification Tasks
-
-- [ ] **V01**: All 7 new/modified files created and syntactically valid (1 utility + 5 components + 1 rewrite)
-- [ ] **V02**: KPI cards show correct counts for open cases, overdue, completed, my tasks
-- [ ] **V03**: KPI sub-labels display correctly: "+N today", "action needed", "avg N days", "N due today"
-- [ ] **V04**: Status chart renders horizontal bars proportional to case counts
-- [ ] **V05**: Overdue panel lists cases sorted by days overdue, with severity colors
-- [ ] **V06**: My Work preview shows top 5 items sorted by priority then deadline
-- [ ] **V07**: Activity feed shows last 10 events with relative timestamps
-- [ ] **V08**: Empty state displays welcome message when no data exists
-- [ ] **V09**: Refresh button re-fetches all dashboard data
-- [ ] **V10**: Partial API failure shows section-level error without blocking other sections
-- [ ] **V11**: Layout is responsive — two columns on wide, single column on narrow
-- [ ] **V12**: "+ New Case" opens CaseCreateDialog
-- [ ] **V13**: All tasks checked off
+# Tasks: Dashboard MVP
+
+## Implementation Tasks
+
+### Utility Module
+
+- [x] **T01**: Create `src/utils/dashboardHelpers.js` — Export functions: `computeKpis(openCases, completedCases, myTasks)` (returns `{ openCount, newToday, overdueCount, completedCount, avgDays, taskCount, tasksDueToday }`), `aggregateByStatus(openCases, statusTypes)` (returns `[{ name, count }]` ordered by status type `order`; aggregates same-named statuses across case types), `getOverdueCases(openCases, caseTypes)` (returns `[{ id, identifier, title, caseTypeName, daysOverdue, handler }]` sorted by daysOverdue desc; uses `isCaseOverdue` from `caseHelpers.js`), `getRecentActivity(cases, limit = 10)` (flattens all cases' `activity` arrays, adds `caseIdentifier` to each entry, sorts by date desc, returns top `limit`), `getMyWorkItems(cases, tasks, limit = 5)` (merges cases + tasks into unified items with `{ type, id, title, reference, deadline, daysText, isOverdue, priority }`, sorts by priority then deadline asc, returns top `limit`). Import `isCaseOverdue`, `getDaysRemaining`, `formatDeadlineCountdown` from `caseHelpers.js`. Today checks: `new Date().toISOString().slice(0, 10)`.
+
+### Sub-Components
+
+- [x] **T02**: Create `src/views/dashboard/KpiCards.vue` — Row of 4 metric cards using CSS Grid (4 columns, collapses to 2x2 on narrow). Each card: title (h3), count (large number), sub-label (small text). Props: `openCases` (Number), `newToday` (Number), `overdueCases` (Number), `completedThisMonth` (Number), `avgProcessingDays` (Number|null), `myTasks` (Number), `tasksDueToday` (Number), `loading` (Boolean). When `loading`, show `NcLoadingIcon` inside each card. Card 1: "Open Cases" / count / "+N today". Card 2: "Overdue" / count / "action needed" (red) when > 0 or "all on track" when 0. Card 3: "Completed This Month" / count / "avg N days" or "no data". Card 4: "My Tasks" / count / "N due today". Emit `@click-card(cardId)` on card click (cardId = 'open', 'overdue', 'completed', 'tasks'). Overdue card: red border when count > 0. Use `--color-warning` for overdue, `--color-success` for completed, `--color-primary` for others.
+
+- [x] **T03**: Create `src/views/dashboard/StatusChart.vue` — Horizontal bar chart, pure CSS. Props: `statusData` (Array of `{ name, count }`), `loading` (Boolean), `error` (String|null). Title: "Cases by Status". Each bar: `div` with `width: (count / maxCount * 100)%`, minimum 20px for visibility. Status name left-aligned, count right-aligned. 6-color palette from CSS variables (`--color-primary`, `--color-primary-element-light`, `--color-warning`, `--color-success`, `--color-error`, `--color-text-maxcontrast`), cycling. Empty state: "No open cases" message. Error state: show `error` with retry button (emit `@retry`). Loading state: 4 skeleton bars.
+
+- [x] **T04**: Create `src/views/dashboard/OverduePanel.vue` — Scrollable list (max-height 300px). Props: `cases` (Array of `{ id, identifier, title, caseTypeName, daysOverdue, handler }`), `loading` (Boolean), `error` (String|null). Title: "Overdue Cases" with count badge. Each row: identifier (bold), title (truncated), case type (muted), "N days overdue" (red text), handler name. Click row → emit `@click-case(caseId)`. Footer: "View all overdue" link → emit `@view-all`. Empty state: green checkmark + "No overdue cases" when list is empty. Severity: > 7 days overdue gets red background tint, 3-7 gets orange, 1-2 gets yellow.
+
+- [x] **T05**: Create `src/views/dashboard/MyWorkPreview.vue` — List of top 5 items. Props: `items` (Array of `{ type, id, title, reference, deadline, daysText, isOverdue, priority }`), `loading` (Boolean), `error` (String|null). Title: "My Work". Each row: type badge ([CASE] blue / [TASK] green), title, reference/case type (muted), deadline with `daysText`, overdue indicator (red text if overdue), priority icon for high/urgent. Click row → emit `@click-item(type, id)`. Footer: "View all my work" link → emit `@view-all`. Empty state: "No items assigned to you". Loading: 5 skeleton rows.
+
+- [x] **T06**: Create `src/views/dashboard/ActivityFeed.vue` — Reverse-chronological event list (last 10). Props: `entries` (Array of `{ date, type, description, user, caseIdentifier }`), `loading` (Boolean), `error` (String|null). Title: "Recent Activity". Each entry: type icon (same icons as ActivityTimeline — plus/arrow/pencil/clock/comment), description text, "by {user}" (muted), relative timestamp (e.g., "10 min ago", "1 hour ago", "yesterday"), case reference "#{identifier}" (muted). Footer: "View all activity" link → emit `@view-all`. Empty state: "No recent activity". Loading: 5 skeleton entries.
+
+### Main Dashboard Rewrite
+
+- [x] **T07**: Rewrite `src/views/Dashboard.vue` — Replace placeholder with full dashboard. (a) Import all 5 sub-components + CaseCreateDialog + `dashboardHelpers.js`. (b) Data: `loading` (Boolean), `error` (String|null), `openCases` (Array), `completedCases` (Array), `myTasks` (Array), `caseTypes` (Array), `statusTypes` (Array), `showCreateDialog` (Boolean), section-level loading/error flags for each section. (c) On `mounted()`: call `loadDashboardData()` which fires 5 parallel `fetchCollection` queries via `Promise.allSettled()`: open cases, completed cases this month, my tasks, case types, status types. (d) On resolve: compute KPIs via `computeKpis()`, status data via `aggregateByStatus()`, overdue list via `getOverdueCases()`, my work items via `getMyWorkItems()`, activity via `getRecentActivity()`. Pass computed data as props to sub-components. (e) Quick actions: "+ New Case" button top-right opens CaseCreateDialog. On `@created`: navigate to case detail. (f) Refresh: button top-right triggers `loadDashboardData()` again. (g) Empty state: when no cases AND no case types exist, show welcome message with setup guidance (admin: "Create your first case type in Settings"; non-admin: "The app needs configuration by an administrator"). (h) Layout: CSS Grid — KPI row full width, then two columns (left 60% / right 40%), responsive to single column below 768px. (i) Section error handling: each section independently shows data or error with retry.
+
+### Navigation Integration
+
+- [x] **T08**: Wire dashboard card clicks and panel links — In Dashboard.vue: handle `@click-card` from KpiCards (overdue → `navigateTo('cases')` with overdue filter hash, tasks → `navigateTo('tasks')`), handle `@view-all` from OverduePanel (navigate to cases with `#/cases?overdue=true`), handle `@view-all` from MyWorkPreview (navigate to `#/my-work`), handle `@click-case` from OverduePanel (navigate to `#/cases/{id}`), handle `@click-item` from MyWorkPreview (navigate to `#/cases/{id}` or `#/tasks/{id}` based on type). All navigation via `this.$emit('navigate', route, id)` matching App.vue's handler.
+
+## Verification Tasks
+
+- [ ] **V01**: All 7 new/modified files created and syntactically valid (1 utility + 5 components + 1 rewrite)
+- [ ] **V02**: KPI cards show correct counts for open cases, overdue, completed, my tasks
+- [ ] **V03**: KPI sub-labels display correctly: "+N today", "action needed", "avg N days", "N due today"
+- [ ] **V04**: Status chart renders horizontal bars proportional to case counts
+- [ ] **V05**: Overdue panel lists cases sorted by days overdue, with severity colors
+- [ ] **V06**: My Work preview shows top 5 items sorted by priority then deadline
+- [ ] **V07**: Activity feed shows last 10 events with relative timestamps
+- [ ] **V08**: Empty state displays welcome message when no data exists
+- [ ] **V09**: Refresh button re-fetches all dashboard data
+- [ ] **V10**: Partial API failure shows section-level error without blocking other sections
+- [ ] **V11**: Layout is responsive — two columns on wide, single column on narrow
+- [ ] **V12**: "+ New Case" opens CaseCreateDialog
+- [ ] **V13**: All tasks checked off
diff --git a/openspec/changes/archive/2026-02-26-my-work/.openspec.yaml b/openspec/changes/archive/2026-02-26-my-work/.openspec.yaml
index 8f132ed6..529f7989 100644
--- a/openspec/changes/archive/2026-02-26-my-work/.openspec.yaml
+++ b/openspec/changes/archive/2026-02-26-my-work/.openspec.yaml
@@ -1 +1 @@
-schema: conduction
+schema: conduction
diff --git a/openspec/changes/archive/2026-02-26-my-work/design.md b/openspec/changes/archive/2026-02-26-my-work/design.md
index 1be59f97..0c52040b 100644
--- a/openspec/changes/archive/2026-02-26-my-work/design.md
+++ b/openspec/changes/archive/2026-02-26-my-work/design.md
@@ -1,105 +1,105 @@
-# Design: my-work
-
-## Context
-
-Procest's dashboard already has a My Work preview widget (`MyWorkPreview.vue`) powered by `getMyWorkItems()` which merges cases and tasks into a flat sorted list limited to 5 items. The full My Work view needs to expand this into a complete productivity hub with urgency grouping, filter tabs, and completed item toggling.
-
-**Key architecture change**: Tasks now come from Nextcloud CalDAV (VTODO items) via OpenRegister's tasks convenience API, not from Procest's OpenRegister `task` schema. This means the task data shape is different (CalDAV fields like `summary`, `due`, `status` vs OpenRegister fields like `title`, `dueDate`).
-
-## Goals / Non-Goals
-
-**Goals:**
-- Create a full-page My Work view with urgency-grouped sections
-- Add filter tabs (All, Cases, Tasks) with live counts
-- Add "Show completed" toggle
-- Wire up routing and navigation
-- Fetch CalDAV tasks via OpenRegister tasks API
-- Reuse existing helpers where possible
-
-**Non-Goals:**
-- Cross-app Pipelinq integration (V1, REQ-MYWORK-008)
-- Server-side aggregation endpoint
-- Task creation/editing from My Work (users create tasks from case detail)
-- Infinite scroll (pagination sufficient for MVP)
-
-## Decisions
-
-### DD-01: Task Fetching Strategy
-
-**Decision**: Fetch all user's CalDAV tasks linked to the Procest register in a single API call, rather than per-case fetching.
-
-**Rationale**: Per-case fetching (one API call per case) would be N+1 requests. Instead, we call the OpenRegister tasks API once to get all tasks where `X-OPENREGISTER-REGISTER` matches Procest's register ID. This returns all Procest-linked tasks for the user in one response.
-
-**API call**: `GET /apps/openregister/api/objects/{register}/tasks?assignee={currentUser}` (if a register-level endpoint exists) or fall back to fetching from CalDAV directly.
-
-**Fallback**: If no register-level task endpoint exists in the MVP API, fetch user's cases first, then batch-fetch tasks per case. Limit to 20 most recent cases.
-
-### DD-02: New `getGroupedMyWorkItems()` Helper
-
-**Decision**: Add a new function to `dashboardHelpers.js` that accepts both OpenRegister case objects and CalDAV task objects (different shapes), normalizes them, and groups by urgency.
-
-**Rationale**: `getMyWorkItems()` currently expects OpenRegister task objects with `title`, `dueDate`, etc. CalDAV tasks have `summary`, `due`, `status` (NEEDS-ACTION/IN-PROCESS/COMPLETED/CANCELLED). The new function handles both shapes.
-
-**Normalization**: Map CalDAV fields to the existing work item shape:
-- `summary` → `title`
-- `due` → `deadline`
-- CalDAV `priority` (1-9, iCalendar scale) → app priority (urgent/high/normal/low)
-- CalDAV `status` (needs-action/in-process) → "active" tasks
-- CalDAV `status` (completed) → completed tasks (for toggle)
-
-**Signature**:
-```javascript
-getGroupedMyWorkItems(cases, calDavTasks) → {
- overdue: WorkItem[],
- dueThisWeek: WorkItem[],
- upcoming: WorkItem[],
- noDeadline: WorkItem[],
- totalCount: number
-}
-```
-
-### DD-03: Client-Side Filtering via Tabs
-
-**Decision**: Fetch all cases + tasks upfront, merge client-side, then filter by tab selection.
-
-**Rationale**: Total item count per user is typically under 50. Tab filtering is instant on the loaded data.
-
-### DD-04: Show Completed Toggle
-
-**Decision**: When toggled, make additional fetch for completed cases (final status) and completed CalDAV tasks (STATUS=COMPLETED). Append to a "COMPLETED" section.
-
-**Rationale**: Completed items excluded from default queries. Supplemental requests only when toggle is on.
-
-### DD-05: Single Component + Task API Wrapper
-
-**Decision**: `MyWork.vue` is a single component. A thin `src/services/taskApi.js` module wraps the OpenRegister tasks API calls.
-
-**Rationale**: Separating the API calls from the view keeps MyWork.vue focused on display logic. The taskApi module handles the fetch + CalDAV-to-app-format normalization.
-
-### DD-06: Route and Navigation Placement
-
-**Decision**: Add `my-work` route to App.vue. Nav item between Dashboard and Cases with `AccountCheck` icon.
-
-## File Map
-
-### New Files
-
-| File | Purpose |
-|------|---------|
-| `src/views/MyWork.vue` | Full "My Work" view with grouped sections, filter tabs, completed toggle |
-| `src/services/taskApi.js` | Thin wrapper around OpenRegister tasks API + CalDAV field normalization |
-
-### Modified Files
-
-| File | Changes |
-|------|---------|
-| `src/utils/dashboardHelpers.js` | Add `getGroupedMyWorkItems()` function accepting normalized items |
-| `src/App.vue` | Add `my-work` route, import MyWork component |
-| `src/navigation/MainMenu.vue` | Add "My Work" nav item between Dashboard and Cases |
-
-## Risks / Trade-offs
-
-- **[Dependency] OpenRegister object-interactions** — Must be deployed first. If not available, My Work can fall back to showing only cases (no tasks). The view should handle missing task API gracefully.
-- **[Risk] CalDAV priority mapping** — iCalendar priority (1=highest, 9=lowest) differs from Procest's (urgent/high/normal/low). Mapping: 1-3→urgent, 4→high, 5-6→normal, 7-9→low, 0→normal.
-- **[Trade-off] Per-user tasks only** — CalDAV tasks are in the user's personal calendar. A task created by user A on a case is not visible to user B in My Work. This matches Nextcloud Tasks behavior.
-- **[Trade-off] Client-side grouping** — Grouping by week boundary depends on local timezone. Use Sunday as end of week.
+# Design: my-work
+
+## Context
+
+Procest's dashboard already has a My Work preview widget (`MyWorkPreview.vue`) powered by `getMyWorkItems()` which merges cases and tasks into a flat sorted list limited to 5 items. The full My Work view needs to expand this into a complete productivity hub with urgency grouping, filter tabs, and completed item toggling.
+
+**Key architecture change**: Tasks now come from Nextcloud CalDAV (VTODO items) via OpenRegister's tasks convenience API, not from Procest's OpenRegister `task` schema. This means the task data shape is different (CalDAV fields like `summary`, `due`, `status` vs OpenRegister fields like `title`, `dueDate`).
+
+## Goals / Non-Goals
+
+**Goals:**
+- Create a full-page My Work view with urgency-grouped sections
+- Add filter tabs (All, Cases, Tasks) with live counts
+- Add "Show completed" toggle
+- Wire up routing and navigation
+- Fetch CalDAV tasks via OpenRegister tasks API
+- Reuse existing helpers where possible
+
+**Non-Goals:**
+- Cross-app Pipelinq integration (V1, REQ-MYWORK-008)
+- Server-side aggregation endpoint
+- Task creation/editing from My Work (users create tasks from case detail)
+- Infinite scroll (pagination sufficient for MVP)
+
+## Decisions
+
+### DD-01: Task Fetching Strategy
+
+**Decision**: Fetch all user's CalDAV tasks linked to the Procest register in a single API call, rather than per-case fetching.
+
+**Rationale**: Per-case fetching (one API call per case) would be N+1 requests. Instead, we call the OpenRegister tasks API once to get all tasks where `X-OPENREGISTER-REGISTER` matches Procest's register ID. This returns all Procest-linked tasks for the user in one response.
+
+**API call**: `GET /apps/openregister/api/objects/{register}/tasks?assignee={currentUser}` (if a register-level endpoint exists) or fall back to fetching from CalDAV directly.
+
+**Fallback**: If no register-level task endpoint exists in the MVP API, fetch user's cases first, then batch-fetch tasks per case. Limit to 20 most recent cases.
+
+### DD-02: New `getGroupedMyWorkItems()` Helper
+
+**Decision**: Add a new function to `dashboardHelpers.js` that accepts both OpenRegister case objects and CalDAV task objects (different shapes), normalizes them, and groups by urgency.
+
+**Rationale**: `getMyWorkItems()` currently expects OpenRegister task objects with `title`, `dueDate`, etc. CalDAV tasks have `summary`, `due`, `status` (NEEDS-ACTION/IN-PROCESS/COMPLETED/CANCELLED). The new function handles both shapes.
+
+**Normalization**: Map CalDAV fields to the existing work item shape:
+- `summary` → `title`
+- `due` → `deadline`
+- CalDAV `priority` (1-9, iCalendar scale) → app priority (urgent/high/normal/low)
+- CalDAV `status` (needs-action/in-process) → "active" tasks
+- CalDAV `status` (completed) → completed tasks (for toggle)
+
+**Signature**:
+```javascript
+getGroupedMyWorkItems(cases, calDavTasks) → {
+ overdue: WorkItem[],
+ dueThisWeek: WorkItem[],
+ upcoming: WorkItem[],
+ noDeadline: WorkItem[],
+ totalCount: number
+}
+```
+
+### DD-03: Client-Side Filtering via Tabs
+
+**Decision**: Fetch all cases + tasks upfront, merge client-side, then filter by tab selection.
+
+**Rationale**: Total item count per user is typically under 50. Tab filtering is instant on the loaded data.
+
+### DD-04: Show Completed Toggle
+
+**Decision**: When toggled, make additional fetch for completed cases (final status) and completed CalDAV tasks (STATUS=COMPLETED). Append to a "COMPLETED" section.
+
+**Rationale**: Completed items excluded from default queries. Supplemental requests only when toggle is on.
+
+### DD-05: Single Component + Task API Wrapper
+
+**Decision**: `MyWork.vue` is a single component. A thin `src/services/taskApi.js` module wraps the OpenRegister tasks API calls.
+
+**Rationale**: Separating the API calls from the view keeps MyWork.vue focused on display logic. The taskApi module handles the fetch + CalDAV-to-app-format normalization.
+
+### DD-06: Route and Navigation Placement
+
+**Decision**: Add `my-work` route to App.vue. Nav item between Dashboard and Cases with `AccountCheck` icon.
+
+## File Map
+
+### New Files
+
+| File | Purpose |
+|------|---------|
+| `src/views/MyWork.vue` | Full "My Work" view with grouped sections, filter tabs, completed toggle |
+| `src/services/taskApi.js` | Thin wrapper around OpenRegister tasks API + CalDAV field normalization |
+
+### Modified Files
+
+| File | Changes |
+|------|---------|
+| `src/utils/dashboardHelpers.js` | Add `getGroupedMyWorkItems()` function accepting normalized items |
+| `src/App.vue` | Add `my-work` route, import MyWork component |
+| `src/navigation/MainMenu.vue` | Add "My Work" nav item between Dashboard and Cases |
+
+## Risks / Trade-offs
+
+- **[Dependency] OpenRegister object-interactions** — Must be deployed first. If not available, My Work can fall back to showing only cases (no tasks). The view should handle missing task API gracefully.
+- **[Risk] CalDAV priority mapping** — iCalendar priority (1=highest, 9=lowest) differs from Procest's (urgent/high/normal/low). Mapping: 1-3→urgent, 4→high, 5-6→normal, 7-9→low, 0→normal.
+- **[Trade-off] Per-user tasks only** — CalDAV tasks are in the user's personal calendar. A task created by user A on a case is not visible to user B in My Work. This matches Nextcloud Tasks behavior.
+- **[Trade-off] Client-side grouping** — Grouping by week boundary depends on local timezone. Use Sunday as end of week.
diff --git a/openspec/changes/archive/2026-02-26-my-work/proposal.md b/openspec/changes/archive/2026-02-26-my-work/proposal.md
index fc47ea50..4587e26d 100644
--- a/openspec/changes/archive/2026-02-26-my-work/proposal.md
+++ b/openspec/changes/archive/2026-02-26-my-work/proposal.md
@@ -1,39 +1,39 @@
-# Proposal: my-work
-
-## Why
-
-The Procest dashboard already has a `MyWorkPreview` widget showing the top 5 assigned items, powered by `getMyWorkItems()` in dashboardHelpers.js. However, there is no dedicated full-page "My Work" view. The dashboard widget emits a `@view-all` event that navigates to `'my-work'`, but no such route or view exists — clicking "View all my work" does nothing.
-
-Case handlers need a personal productivity hub that shows *all* their assigned cases and tasks, grouped by urgency, with the ability to filter by entity type and navigate to any item. This is the daily "what should I work on next?" view.
-
-## Architecture Change: CalDAV Tasks
-
-**Important**: This change uses **Nextcloud CalDAV tasks** instead of Procest's OpenRegister task objects. OpenRegister provides a convenience API (`/api/objects/{register}/{schema}/{id}/tasks`) that wraps CalDAV VTODO items with `X-OPENREGISTER-*` properties. Procest's `task` schema in OpenRegister is no longer the source for My Work tasks — CalDAV is.
-
-**Dependency**: Requires OpenRegister's `object-interactions` change to be implemented first (TaskService, TasksController, NoteService, NotesController).
-
-My Work fetches:
-- **Cases**: OpenRegister API (as before) — `assignee == currentUser`, non-final status
-- **Tasks**: OpenRegister Tasks API — `GET /api/objects/{register}/{schema}/{id}/tasks` for each user case, OR query CalDAV directly for all VTODOs with `X-OPENREGISTER-REGISTER` matching Procest's register ID
-
-## What Changes
-
-- **Create `src/views/MyWork.vue`** — Full "My Work" view with grouped sections (Overdue, Due This Week, Upcoming, No Deadline), filter tabs (All/Cases/Tasks with counts), overdue highlighting, item navigation, empty state, and a "Show completed" toggle
-- **Extend `src/utils/dashboardHelpers.js`** — Add a `getGroupedMyWorkItems()` function that returns items organized into urgency groups instead of a flat limited list
-- **Add `src/services/taskApi.js`** — Thin wrapper around the OpenRegister tasks convenience API for fetching user's CalDAV tasks
-- **Update `src/App.vue`** — Add `'my-work'` route mapping to the MyWork component
-- **Update `src/navigation/MainMenu.vue`** — Add "My Work" navigation item between Dashboard and Cases
-
-## Capabilities
-
-### Modified Capabilities
-
-- **my-work** — All MVP requirements (REQ-MYWORK-001 through REQ-MYWORK-007, REQ-MYWORK-009, REQ-MYWORK-010). REQ-MYWORK-008 (cross-app workload with Pipelinq, V1) is deferred.
-
-## Impact
-
-- **Frontend only**: No Procest backend changes — uses OpenRegister's task and case APIs
-- **Reuses existing infrastructure**: `getMyWorkItems()`, `formatDeadlineCountdown()`, `isCaseOverdue()`, `prioritySortWeight()` all exist
-- **Navigation**: Adds a 4th nav item; Dashboard's "View all my work" link will now work
-- **Performance**: Cases from OpenRegister API + tasks from CalDAV via OpenRegister tasks API, client-side merge and grouping
-- **Dependency**: OpenRegister `object-interactions` must be deployed first
+# Proposal: my-work
+
+## Why
+
+The Procest dashboard already has a `MyWorkPreview` widget showing the top 5 assigned items, powered by `getMyWorkItems()` in dashboardHelpers.js. However, there is no dedicated full-page "My Work" view. The dashboard widget emits a `@view-all` event that navigates to `'my-work'`, but no such route or view exists — clicking "View all my work" does nothing.
+
+Case handlers need a personal productivity hub that shows *all* their assigned cases and tasks, grouped by urgency, with the ability to filter by entity type and navigate to any item. This is the daily "what should I work on next?" view.
+
+## Architecture Change: CalDAV Tasks
+
+**Important**: This change uses **Nextcloud CalDAV tasks** instead of Procest's OpenRegister task objects. OpenRegister provides a convenience API (`/api/objects/{register}/{schema}/{id}/tasks`) that wraps CalDAV VTODO items with `X-OPENREGISTER-*` properties. Procest's `task` schema in OpenRegister is no longer the source for My Work tasks — CalDAV is.
+
+**Dependency**: Requires OpenRegister's `object-interactions` change to be implemented first (TaskService, TasksController, NoteService, NotesController).
+
+My Work fetches:
+- **Cases**: OpenRegister API (as before) — `assignee == currentUser`, non-final status
+- **Tasks**: OpenRegister Tasks API — `GET /api/objects/{register}/{schema}/{id}/tasks` for each user case, OR query CalDAV directly for all VTODOs with `X-OPENREGISTER-REGISTER` matching Procest's register ID
+
+## What Changes
+
+- **Create `src/views/MyWork.vue`** — Full "My Work" view with grouped sections (Overdue, Due This Week, Upcoming, No Deadline), filter tabs (All/Cases/Tasks with counts), overdue highlighting, item navigation, empty state, and a "Show completed" toggle
+- **Extend `src/utils/dashboardHelpers.js`** — Add a `getGroupedMyWorkItems()` function that returns items organized into urgency groups instead of a flat limited list
+- **Add `src/services/taskApi.js`** — Thin wrapper around the OpenRegister tasks convenience API for fetching user's CalDAV tasks
+- **Update `src/App.vue`** — Add `'my-work'` route mapping to the MyWork component
+- **Update `src/navigation/MainMenu.vue`** — Add "My Work" navigation item between Dashboard and Cases
+
+## Capabilities
+
+### Modified Capabilities
+
+- **my-work** — All MVP requirements (REQ-MYWORK-001 through REQ-MYWORK-007, REQ-MYWORK-009, REQ-MYWORK-010). REQ-MYWORK-008 (cross-app workload with Pipelinq, V1) is deferred.
+
+## Impact
+
+- **Frontend only**: No Procest backend changes — uses OpenRegister's task and case APIs
+- **Reuses existing infrastructure**: `getMyWorkItems()`, `formatDeadlineCountdown()`, `isCaseOverdue()`, `prioritySortWeight()` all exist
+- **Navigation**: Adds a 4th nav item; Dashboard's "View all my work" link will now work
+- **Performance**: Cases from OpenRegister API + tasks from CalDAV via OpenRegister tasks API, client-side merge and grouping
+- **Dependency**: OpenRegister `object-interactions` must be deployed first
diff --git a/openspec/changes/archive/2026-02-26-my-work/specs/my-work/spec.md b/openspec/changes/archive/2026-02-26-my-work/specs/my-work/spec.md
index 9b43dc83..7b5a124c 100644
--- a/openspec/changes/archive/2026-02-26-my-work/specs/my-work/spec.md
+++ b/openspec/changes/archive/2026-02-26-my-work/specs/my-work/spec.md
@@ -1,95 +1,95 @@
-# Delta Spec: my-work
-
-This delta spec scopes the MVP implementation of the My Work view using CalDAV tasks via OpenRegister's convenience API.
-
-## Scope
-
-**In scope (MVP)**: REQ-MYWORK-001, REQ-MYWORK-002, REQ-MYWORK-003, REQ-MYWORK-004, REQ-MYWORK-005, REQ-MYWORK-006, REQ-MYWORK-007, REQ-MYWORK-009, REQ-MYWORK-010
-
-**Deferred (V1)**: REQ-MYWORK-008 (cross-app workload with Pipelinq)
-
-**Dependency**: OpenRegister `object-interactions` change (TaskService, TasksController API)
-
----
-
-## Architecture Change: Task Data Source
-
-**Previous assumption**: Tasks come from Procest's OpenRegister `task` schema.
-**New approach**: Tasks come from **Nextcloud CalDAV VTODO** items via OpenRegister's tasks convenience API.
-
-The OpenRegister tasks API (`GET /api/objects/{register}/{schema}/{id}/tasks`) returns JSON-friendly task objects. For My Work, we need all tasks linked to the Procest register — not per-object, but per-user across all objects.
-
-**Task query strategy for My Work**:
-1. Fetch user's assigned cases (OpenRegister case objects with `assignee == currentUser`)
-2. For each case, fetch linked CalDAV tasks via OpenRegister tasks API
-3. OR: Direct CalDAV query for all user's VTODOs with `X-OPENREGISTER-REGISTER` matching Procest's register ID (more efficient, single query)
-
-For MVP, option 2 is preferred — a single API call or CalDAV query returns all Procest tasks for the user.
-
----
-
-## Current State
-
-### Existing Code
-
-- **`src/utils/dashboardHelpers.js::getMyWorkItems()`** — Merges cases + tasks into flat sorted list, limited to N items. Currently expects OpenRegister task objects. Needs updating for CalDAV task shape.
-- **`src/views/dashboard/MyWorkPreview.vue`** — Dashboard preview widget showing top 5 items. Emits `@view-all` but no route handles it.
-- **`src/App.vue`** — Hash-based routing. No `my-work` route.
-- **`src/navigation/MainMenu.vue`** — 3 nav items: Dashboard, Cases, Tasks. No My Work entry.
-
-### What's Missing
-
-1. No `MyWork.vue` full-page view
-2. No grouped sections (Overdue, Due This Week, Upcoming, No Deadline)
-3. No filter tabs (All/Cases/Tasks)
-4. No "Show completed" toggle
-5. No CalDAV task fetching — `getMyWorkItems()` expects OpenRegister objects
-6. No `my-work` route in App.vue
-7. No My Work navigation menu item
-
----
-
-## MODIFIED Requirements
-
-### Requirement: REQ-MYWORK-001 — Personal Workload View
-
-**Current state**: `getMyWorkItems()` exists but uses OpenRegister task objects and is limited to 5 items.
-
-**Change**: Create full MyWork.vue that fetches assigned cases (OpenRegister) and CalDAV tasks (via OpenRegister tasks API), displays in a unified list.
-
-#### Scenario: Full workload with CalDAV tasks
-
-- GIVEN the user has N assigned cases and M CalDAV tasks linked to Procest objects
-- WHEN they navigate to My Work
-- THEN the view MUST display all N+M items
-- AND CalDAV tasks MUST show: summary, due date, priority, status, linked case reference
-- AND cases MUST show: title, identifier, deadline, priority, status
-
----
-
-### Requirement: REQ-MYWORK-004 — Grouped Sections
-
-**Current state**: No grouping exists.
-
-**Change**: Add urgency-based grouping for both cases and CalDAV tasks.
-
-#### Scenario: CalDAV tasks grouped by due date
-
-- GIVEN CalDAV tasks with varying DUE properties
-- WHEN the user views My Work
-- THEN tasks MUST be grouped using their DUE date into: OVERDUE, DUE THIS WEEK, UPCOMING, NO DEADLINE
-- AND the grouping logic MUST handle both case deadlines and task DUE dates uniformly
-
----
-
-### Requirement: REQ-MYWORK-006 — Show Completed Toggle
-
-**Current state**: No toggle.
-
-**Change**: Toggle fetches completed cases (final status) and completed CalDAV tasks (STATUS=COMPLETED).
-
-#### Scenario: Toggle shows completed CalDAV tasks
-
-- GIVEN the user enables "Show completed"
-- THEN completed CalDAV tasks (STATUS=COMPLETED) MUST be fetched
-- AND they MUST appear in a COMPLETED section at the bottom, visually muted
+# Delta Spec: my-work
+
+This delta spec scopes the MVP implementation of the My Work view using CalDAV tasks via OpenRegister's convenience API.
+
+## Scope
+
+**In scope (MVP)**: REQ-MYWORK-001, REQ-MYWORK-002, REQ-MYWORK-003, REQ-MYWORK-004, REQ-MYWORK-005, REQ-MYWORK-006, REQ-MYWORK-007, REQ-MYWORK-009, REQ-MYWORK-010
+
+**Deferred (V1)**: REQ-MYWORK-008 (cross-app workload with Pipelinq)
+
+**Dependency**: OpenRegister `object-interactions` change (TaskService, TasksController API)
+
+---
+
+## Architecture Change: Task Data Source
+
+**Previous assumption**: Tasks come from Procest's OpenRegister `task` schema.
+**New approach**: Tasks come from **Nextcloud CalDAV VTODO** items via OpenRegister's tasks convenience API.
+
+The OpenRegister tasks API (`GET /api/objects/{register}/{schema}/{id}/tasks`) returns JSON-friendly task objects. For My Work, we need all tasks linked to the Procest register — not per-object, but per-user across all objects.
+
+**Task query strategy for My Work**:
+1. Fetch user's assigned cases (OpenRegister case objects with `assignee == currentUser`)
+2. For each case, fetch linked CalDAV tasks via OpenRegister tasks API
+3. OR: Direct CalDAV query for all user's VTODOs with `X-OPENREGISTER-REGISTER` matching Procest's register ID (more efficient, single query)
+
+For MVP, option 2 is preferred — a single API call or CalDAV query returns all Procest tasks for the user.
+
+---
+
+## Current State
+
+### Existing Code
+
+- **`src/utils/dashboardHelpers.js::getMyWorkItems()`** — Merges cases + tasks into flat sorted list, limited to N items. Currently expects OpenRegister task objects. Needs updating for CalDAV task shape.
+- **`src/views/dashboard/MyWorkPreview.vue`** — Dashboard preview widget showing top 5 items. Emits `@view-all` but no route handles it.
+- **`src/App.vue`** — Hash-based routing. No `my-work` route.
+- **`src/navigation/MainMenu.vue`** — 3 nav items: Dashboard, Cases, Tasks. No My Work entry.
+
+### What's Missing
+
+1. No `MyWork.vue` full-page view
+2. No grouped sections (Overdue, Due This Week, Upcoming, No Deadline)
+3. No filter tabs (All/Cases/Tasks)
+4. No "Show completed" toggle
+5. No CalDAV task fetching — `getMyWorkItems()` expects OpenRegister objects
+6. No `my-work` route in App.vue
+7. No My Work navigation menu item
+
+---
+
+## MODIFIED Requirements
+
+### Requirement: REQ-MYWORK-001 — Personal Workload View
+
+**Current state**: `getMyWorkItems()` exists but uses OpenRegister task objects and is limited to 5 items.
+
+**Change**: Create full MyWork.vue that fetches assigned cases (OpenRegister) and CalDAV tasks (via OpenRegister tasks API), displays in a unified list.
+
+#### Scenario: Full workload with CalDAV tasks
+
+- GIVEN the user has N assigned cases and M CalDAV tasks linked to Procest objects
+- WHEN they navigate to My Work
+- THEN the view MUST display all N+M items
+- AND CalDAV tasks MUST show: summary, due date, priority, status, linked case reference
+- AND cases MUST show: title, identifier, deadline, priority, status
+
+---
+
+### Requirement: REQ-MYWORK-004 — Grouped Sections
+
+**Current state**: No grouping exists.
+
+**Change**: Add urgency-based grouping for both cases and CalDAV tasks.
+
+#### Scenario: CalDAV tasks grouped by due date
+
+- GIVEN CalDAV tasks with varying DUE properties
+- WHEN the user views My Work
+- THEN tasks MUST be grouped using their DUE date into: OVERDUE, DUE THIS WEEK, UPCOMING, NO DEADLINE
+- AND the grouping logic MUST handle both case deadlines and task DUE dates uniformly
+
+---
+
+### Requirement: REQ-MYWORK-006 — Show Completed Toggle
+
+**Current state**: No toggle.
+
+**Change**: Toggle fetches completed cases (final status) and completed CalDAV tasks (STATUS=COMPLETED).
+
+#### Scenario: Toggle shows completed CalDAV tasks
+
+- GIVEN the user enables "Show completed"
+- THEN completed CalDAV tasks (STATUS=COMPLETED) MUST be fetched
+- AND they MUST appear in a COMPLETED section at the bottom, visually muted
diff --git a/openspec/changes/archive/2026-02-26-my-work/tasks.md b/openspec/changes/archive/2026-02-26-my-work/tasks.md
index b125fe8b..7b3ed568 100644
--- a/openspec/changes/archive/2026-02-26-my-work/tasks.md
+++ b/openspec/changes/archive/2026-02-26-my-work/tasks.md
@@ -1,36 +1,36 @@
-# Tasks: my-work
-
-**Dependency**: OpenRegister `object-interactions` change must be implemented first (TaskService, TasksController API).
-
-## 1. Task API Wrapper
-
-- [x] 1.1 Create `src/services/taskApi.js` — Thin module that wraps OpenRegister's tasks convenience API. Exports: `fetchTasksForRegister(registerId)` → fetches all CalDAV tasks linked to the Procest register for the current user; `normalizeCalDavTask(task)` → maps CalDAV task JSON (`{ uid, summary, due, status, priority, objectUuid, ... }`) to the work item shape (`{ type: 'task', id, title, reference, deadline, daysText, isOverdue, priority }`). Priority mapping: iCalendar 1-3→'urgent', 4→'high', 5-6→'normal', 7-9→'low', 0→'normal'. Uses `fetch()` with requesttoken header to call `GET /apps/openregister/api/objects/{register}/tasks`.
-
-## 2. Helper — Grouped My Work Items
-
-- [x] 2.1 Add `getGroupedMyWorkItems(cases, normalizedTasks)` to `src/utils/dashboardHelpers.js` — Accepts OpenRegister case objects and already-normalized CalDAV task items. Returns `{ overdue, dueThisWeek, upcoming, noDeadline, totalCount }`. Each group is an array of work items with shape `{ type, id, title, reference, deadline, daysText, isOverdue, priority }`. Build case items using existing logic from `getMyWorkItems()`. Classify all items by comparing deadline to today and end-of-week (Sunday). Sort within each group by priority then deadline. Skip the limit (return all items).
-
-## 3. View — MyWork.vue
-
-- [x] 3.1 Create `src/views/MyWork.vue` — Full "My Work" view with: (a) header showing title "My Work" and total item count, (b) filter tabs (All/Cases/Tasks) with counts in parentheses, (c) "Show completed" toggle checkbox, (d) grouped sections (OVERDUE with red section header, DUE THIS WEEK, UPCOMING, NO DEADLINE) — each section has a header with count and hides when empty, (e) each item row with type badge (CASE blue / TASK green), title, reference/linked case link, deadline info, priority indicator, (f) overdue items highlighted with red left border and red days text, (g) click handler navigating to case-detail (for cases) or the linked case detail (for tasks), (h) loading state with NcLoadingIcon, (i) empty state with NcEmptyContent ("No items assigned to you"), (j) "all caught up" message when all items are completed but toggle is off. Fetch data in `mounted()`: call `objectStore.fetchCollection('case', { '_filters[assignee]': currentUser })` for cases AND `fetchTasksForRegister(registerId)` for CalDAV tasks in parallel. Use `getGroupedMyWorkItems()` for grouping. Filter tabs filter the grouped data client-side. "Show completed" toggle fetches additional completed cases (final status) and completed CalDAV tasks (STATUS=COMPLETED) and shows them in a muted COMPLETED section at the bottom. Follow CaseList.vue patterns for styling.
-
-## 4. Routing — App.vue
-
-- [x] 4.1 Update `src/App.vue` — Import MyWork component, add `'my-work'` case to `currentView` computed (returns `'MyWork'`), register MyWork in components.
-
-## 5. Navigation — MainMenu.vue
-
-- [x] 5.1 Update `src/navigation/MainMenu.vue` — Add a "My Work" navigation item between Dashboard and Cases. Use `AccountCheck` icon from vue-material-design-icons. Set active state for `currentRoute === 'my-work'`.
-
-## 6. Verification
-
-- [ ] 6.1 Verify "My Work" nav item appears and is clickable
-- [ ] 6.2 Verify Dashboard "View all my work" link navigates to My Work view
-- [ ] 6.3 Verify cases from OpenRegister and CalDAV tasks both appear in the list
-- [ ] 6.4 Verify items are grouped into correct urgency sections (overdue/this week/upcoming/no deadline)
-- [ ] 6.5 Verify filter tabs (All/Cases/Tasks) filter items and show correct counts
-- [ ] 6.6 Verify clicking a case navigates to case-detail
-- [ ] 6.7 Verify clicking a task navigates to the linked case detail
-- [ ] 6.8 Verify empty state shows when user has no assigned items
-- [ ] 6.9 Verify overdue items have red highlighting
-- [ ] 6.10 Verify "Show completed" toggle shows completed items in muted section
+# Tasks: my-work
+
+**Dependency**: OpenRegister `object-interactions` change must be implemented first (TaskService, TasksController API).
+
+## 1. Task API Wrapper
+
+- [x] 1.1 Create `src/services/taskApi.js` — Thin module that wraps OpenRegister's tasks convenience API. Exports: `fetchTasksForRegister(registerId)` → fetches all CalDAV tasks linked to the Procest register for the current user; `normalizeCalDavTask(task)` → maps CalDAV task JSON (`{ uid, summary, due, status, priority, objectUuid, ... }`) to the work item shape (`{ type: 'task', id, title, reference, deadline, daysText, isOverdue, priority }`). Priority mapping: iCalendar 1-3→'urgent', 4→'high', 5-6→'normal', 7-9→'low', 0→'normal'. Uses `fetch()` with requesttoken header to call `GET /apps/openregister/api/objects/{register}/tasks`.
+
+## 2. Helper — Grouped My Work Items
+
+- [x] 2.1 Add `getGroupedMyWorkItems(cases, normalizedTasks)` to `src/utils/dashboardHelpers.js` — Accepts OpenRegister case objects and already-normalized CalDAV task items. Returns `{ overdue, dueThisWeek, upcoming, noDeadline, totalCount }`. Each group is an array of work items with shape `{ type, id, title, reference, deadline, daysText, isOverdue, priority }`. Build case items using existing logic from `getMyWorkItems()`. Classify all items by comparing deadline to today and end-of-week (Sunday). Sort within each group by priority then deadline. Skip the limit (return all items).
+
+## 3. View — MyWork.vue
+
+- [x] 3.1 Create `src/views/MyWork.vue` — Full "My Work" view with: (a) header showing title "My Work" and total item count, (b) filter tabs (All/Cases/Tasks) with counts in parentheses, (c) "Show completed" toggle checkbox, (d) grouped sections (OVERDUE with red section header, DUE THIS WEEK, UPCOMING, NO DEADLINE) — each section has a header with count and hides when empty, (e) each item row with type badge (CASE blue / TASK green), title, reference/linked case link, deadline info, priority indicator, (f) overdue items highlighted with red left border and red days text, (g) click handler navigating to case-detail (for cases) or the linked case detail (for tasks), (h) loading state with NcLoadingIcon, (i) empty state with NcEmptyContent ("No items assigned to you"), (j) "all caught up" message when all items are completed but toggle is off. Fetch data in `mounted()`: call `objectStore.fetchCollection('case', { '_filters[assignee]': currentUser })` for cases AND `fetchTasksForRegister(registerId)` for CalDAV tasks in parallel. Use `getGroupedMyWorkItems()` for grouping. Filter tabs filter the grouped data client-side. "Show completed" toggle fetches additional completed cases (final status) and completed CalDAV tasks (STATUS=COMPLETED) and shows them in a muted COMPLETED section at the bottom. Follow CaseList.vue patterns for styling.
+
+## 4. Routing — App.vue
+
+- [x] 4.1 Update `src/App.vue` — Import MyWork component, add `'my-work'` case to `currentView` computed (returns `'MyWork'`), register MyWork in components.
+
+## 5. Navigation — MainMenu.vue
+
+- [x] 5.1 Update `src/navigation/MainMenu.vue` — Add a "My Work" navigation item between Dashboard and Cases. Use `AccountCheck` icon from vue-material-design-icons. Set active state for `currentRoute === 'my-work'`.
+
+## 6. Verification
+
+- [ ] 6.1 Verify "My Work" nav item appears and is clickable
+- [ ] 6.2 Verify Dashboard "View all my work" link navigates to My Work view
+- [ ] 6.3 Verify cases from OpenRegister and CalDAV tasks both appear in the list
+- [ ] 6.4 Verify items are grouped into correct urgency sections (overdue/this week/upcoming/no deadline)
+- [ ] 6.5 Verify filter tabs (All/Cases/Tasks) filter items and show correct counts
+- [ ] 6.6 Verify clicking a case navigates to case-detail
+- [ ] 6.7 Verify clicking a task navigates to the linked case detail
+- [ ] 6.8 Verify empty state shows when user has no assigned items
+- [ ] 6.9 Verify overdue items have red highlighting
+- [ ] 6.10 Verify "Show completed" toggle shows completed items in muted section
diff --git a/openspec/changes/archive/2026-02-26-openregister-integration/.openspec.yaml b/openspec/changes/archive/2026-02-26-openregister-integration/.openspec.yaml
index 8f132ed6..529f7989 100644
--- a/openspec/changes/archive/2026-02-26-openregister-integration/.openspec.yaml
+++ b/openspec/changes/archive/2026-02-26-openregister-integration/.openspec.yaml
@@ -1 +1 @@
-schema: conduction
+schema: conduction
diff --git a/openspec/changes/archive/2026-02-26-openregister-integration/design.md b/openspec/changes/archive/2026-02-26-openregister-integration/design.md
index 4dc6c828..f937dc82 100644
--- a/openspec/changes/archive/2026-02-26-openregister-integration/design.md
+++ b/openspec/changes/archive/2026-02-26-openregister-integration/design.md
@@ -1,94 +1,94 @@
-# Design: openregister-integration
-
-## Context
-
-Procest is a thin-client Nextcloud app that stores all data in OpenRegister. The current backend uses a custom repair step that manually creates registers and schemas via RegisterService/SchemaMapper. Sister apps (opencatalogi, softwarecatalog) use a JSON config file + `ConfigurationService::importFromApp()` pattern. This change aligns Procest with that pattern and completes the data model.
-
-## Goals / Non-Goals
-
-**Goals:**
-- Create `procest_register.json` with all 12 schemas in OpenAPI 3.0.0 format
-- Rewrite repair step to use `ConfigurationService::importFromApp()`
-- Register all 12 entity types in the frontend store
-- Add HTTP status-specific error handling to the Pinia store
-
-**Non-Goals:**
-- Cascade delete logic (V1, REQ-OREG-009)
-- Per-entity Pinia stores (`useCaseStore()`, etc.) — the generic `useObjectStore()` pattern works well
-- Server-side audit trail integration (OpenRegister handles this)
-
-## Decisions
-
-### DD-01: OpenAPI 3.0.0 Config File at `lib/Settings/procest_register.json`
-
-**Decision**: Create a single JSON file following OpenAPI 3.0.0 format with `x-openregister` metadata, matching the softwarecatalog pattern.
-
-**Rationale**: `ConfigurationService::importFromApp()` reads from `lib/Settings/{slug}_register.json` by convention. The OpenAPI format provides schema validation tooling and is the standard across all Conduction apps.
-
-**Alternatives considered**: Keep the hard-coded PHP approach — rejected because it diverges from the ecosystem pattern and makes schema changes harder to review.
-
-### DD-02: ConfigurationService::importFromApp() in Repair Step
-
-**Decision**: Replace the custom `initializeRegisterAndSchemas()` method with a single call to `ConfigurationService::importFromApp('procest')`.
-
-**Rationale**: The ConfigurationService handles all the complexity: register creation/update, schema creation/update, idempotency, and version tracking. The custom code duplicates this logic poorly.
-
-**Implementation**: The repair step becomes ~30 lines: check OpenRegister exists, get ConfigurationService from container, call `importFromApp()`.
-
-### DD-03: Register Slug Migration from `case-management` to `procest`
-
-**Decision**: The new config file uses slug `procest`. Since ConfigurationService uses the app ID to find configurations, this effectively creates a new register alongside the old one.
-
-**Risk**: Existing data is in the old `case-management` register.
-
-**Mitigation**: The repair step will first check if a `case-management` register exists with Procest data. If found, it can be migrated by updating the register slug. However, for MVP (where no production data exists yet), we simply create the new `procest` register and let the old one be.
-
-### DD-04: Error Object Structure
-
-**Decision**: Expand the store error from a string to a structured object: `{ status, message, details, isValidation }`.
-
-**Rationale**: UI components need to distinguish validation errors (show field-level messages) from auth errors (show permission message) from server errors (show generic message + retry).
-
-**Implementation**:
-```javascript
-// Error structure
-{
- status: 422, // HTTP status code
- message: 'Validation failed', // User-friendly message
- details: { title: 'Required' }, // Field-level errors (if 400/422)
- isValidation: true // Quick check for form components
-}
-```
-
-### DD-05: SettingsService.php Pattern
-
-**Decision**: Create a `SettingsService.php` following the softwarecatalog pattern for configuration loading, rather than doing everything in the repair step.
-
-**Rationale**: The SettingsService provides a reusable entry point for loading configuration. The repair step delegates to it.
-
-## File Map
-
-### New Files
-
-| File | Purpose |
-|------|---------|
-| `lib/Settings/procest_register.json` | OpenAPI 3.0.0 register config with 12 schemas |
-| `lib/Service/SettingsService.php` | Configuration loading via ConfigurationService |
-
-### Modified Files
-
-| File | Changes |
-|------|---------|
-| `lib/Repair/InitializeSettings.php` | Rewrite: use SettingsService → ConfigurationService::importFromApp() |
-| `src/store/store.js` | Add 4 missing entity type registrations |
-| `src/store/modules/object.js` | Add HTTP status-specific error parsing |
-
-## Risks / Trade-offs
-
-- **[Risk] Register slug change** → Existing dev data in `case-management` register may become orphaned. Mitigation: No production deployments exist yet. Dev environments use `clean-env.sh`.
-- **[Risk] ConfigurationService API changes** → The import method signature may differ from what softwarecatalog uses. Mitigation: Read the actual ConfigurationService source before implementing.
-- **[Trade-off] Generic store vs per-entity stores** → The spec suggests per-entity stores (`useCaseStore()`, etc.) but the generic `useObjectStore()` is simpler and already works. We keep the generic pattern.
-
-## Open Questions
-
-None — the pattern is well-established in sister apps.
+# Design: openregister-integration
+
+## Context
+
+Procest is a thin-client Nextcloud app that stores all data in OpenRegister. The current backend uses a custom repair step that manually creates registers and schemas via RegisterService/SchemaMapper. Sister apps (opencatalogi, softwarecatalog) use a JSON config file + `ConfigurationService::importFromApp()` pattern. This change aligns Procest with that pattern and completes the data model.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Create `procest_register.json` with all 12 schemas in OpenAPI 3.0.0 format
+- Rewrite repair step to use `ConfigurationService::importFromApp()`
+- Register all 12 entity types in the frontend store
+- Add HTTP status-specific error handling to the Pinia store
+
+**Non-Goals:**
+- Cascade delete logic (V1, REQ-OREG-009)
+- Per-entity Pinia stores (`useCaseStore()`, etc.) — the generic `useObjectStore()` pattern works well
+- Server-side audit trail integration (OpenRegister handles this)
+
+## Decisions
+
+### DD-01: OpenAPI 3.0.0 Config File at `lib/Settings/procest_register.json`
+
+**Decision**: Create a single JSON file following OpenAPI 3.0.0 format with `x-openregister` metadata, matching the softwarecatalog pattern.
+
+**Rationale**: `ConfigurationService::importFromApp()` reads from `lib/Settings/{slug}_register.json` by convention. The OpenAPI format provides schema validation tooling and is the standard across all Conduction apps.
+
+**Alternatives considered**: Keep the hard-coded PHP approach — rejected because it diverges from the ecosystem pattern and makes schema changes harder to review.
+
+### DD-02: ConfigurationService::importFromApp() in Repair Step
+
+**Decision**: Replace the custom `initializeRegisterAndSchemas()` method with a single call to `ConfigurationService::importFromApp('procest')`.
+
+**Rationale**: The ConfigurationService handles all the complexity: register creation/update, schema creation/update, idempotency, and version tracking. The custom code duplicates this logic poorly.
+
+**Implementation**: The repair step becomes ~30 lines: check OpenRegister exists, get ConfigurationService from container, call `importFromApp()`.
+
+### DD-03: Register Slug Migration from `case-management` to `procest`
+
+**Decision**: The new config file uses slug `procest`. Since ConfigurationService uses the app ID to find configurations, this effectively creates a new register alongside the old one.
+
+**Risk**: Existing data is in the old `case-management` register.
+
+**Mitigation**: The repair step will first check if a `case-management` register exists with Procest data. If found, it can be migrated by updating the register slug. However, for MVP (where no production data exists yet), we simply create the new `procest` register and let the old one be.
+
+### DD-04: Error Object Structure
+
+**Decision**: Expand the store error from a string to a structured object: `{ status, message, details, isValidation }`.
+
+**Rationale**: UI components need to distinguish validation errors (show field-level messages) from auth errors (show permission message) from server errors (show generic message + retry).
+
+**Implementation**:
+```javascript
+// Error structure
+{
+ status: 422, // HTTP status code
+ message: 'Validation failed', // User-friendly message
+ details: { title: 'Required' }, // Field-level errors (if 400/422)
+ isValidation: true // Quick check for form components
+}
+```
+
+### DD-05: SettingsService.php Pattern
+
+**Decision**: Create a `SettingsService.php` following the softwarecatalog pattern for configuration loading, rather than doing everything in the repair step.
+
+**Rationale**: The SettingsService provides a reusable entry point for loading configuration. The repair step delegates to it.
+
+## File Map
+
+### New Files
+
+| File | Purpose |
+|------|---------|
+| `lib/Settings/procest_register.json` | OpenAPI 3.0.0 register config with 12 schemas |
+| `lib/Service/SettingsService.php` | Configuration loading via ConfigurationService |
+
+### Modified Files
+
+| File | Changes |
+|------|---------|
+| `lib/Repair/InitializeSettings.php` | Rewrite: use SettingsService → ConfigurationService::importFromApp() |
+| `src/store/store.js` | Add 4 missing entity type registrations |
+| `src/store/modules/object.js` | Add HTTP status-specific error parsing |
+
+## Risks / Trade-offs
+
+- **[Risk] Register slug change** → Existing dev data in `case-management` register may become orphaned. Mitigation: No production deployments exist yet. Dev environments use `clean-env.sh`.
+- **[Risk] ConfigurationService API changes** → The import method signature may differ from what softwarecatalog uses. Mitigation: Read the actual ConfigurationService source before implementing.
+- **[Trade-off] Generic store vs per-entity stores** → The spec suggests per-entity stores (`useCaseStore()`, etc.) but the generic `useObjectStore()` is simpler and already works. We keep the generic pattern.
+
+## Open Questions
+
+None — the pattern is well-established in sister apps.
diff --git a/openspec/changes/archive/2026-02-26-openregister-integration/proposal.md b/openspec/changes/archive/2026-02-26-openregister-integration/proposal.md
index d09a08df..15192495 100644
--- a/openspec/changes/archive/2026-02-26-openregister-integration/proposal.md
+++ b/openspec/changes/archive/2026-02-26-openregister-integration/proposal.md
@@ -1,28 +1,28 @@
-# Proposal: openregister-integration
-
-## Why
-
-Procest's data layer currently uses a hard-coded PHP repair step with only 6 minimal schemas (case, task, status, role, result, decision) registered under a `case-management` slug. The spec requires 12 schemas under the `procest` slug, imported via `ConfigurationService::importFromApp()` from an OpenAPI 3.0.0 JSON file — the same pattern used by opencatalogi and softwarecatalog. The frontend store only registers 8 of 12 entity types and lacks HTTP status-specific error handling.
-
-This change brings the backend configuration and frontend store layer in line with the openregister-integration spec (MVP tier only; V1 cascade behaviors are excluded).
-
-## What Changes
-
-- **Create `lib/Settings/procest_register.json`** — OpenAPI 3.0.0 config file defining the `procest` register with all 12 schemas and their full property definitions
-- **Rewrite `lib/Repair/InitializeSettings.php`** — Replace custom register/schema creation with `ConfigurationService::importFromApp('procest')` call, matching the opencatalogi/softwarecatalog pattern
-- **Register all 12 entity types in the frontend store** — Add `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType` to `store.js`
-- **Add HTTP status-specific error handling** in `object.js` — Parse 400/401/403/404/409/500 responses with user-friendly messages
-- **Add `SettingsService.php`** — Thin service to load configuration via ConfigurationService, matching the sister-app pattern
-
-## Capabilities
-
-### Modified Capabilities
-
-- **openregister-integration** — All MVP requirements (REQ-OREG-001 through REQ-OREG-008, REQ-OREG-010 through REQ-OREG-013) are being implemented or corrected. REQ-OREG-009 (cascade behaviors, V1) is deferred.
-
-## Impact
-
-- **Backend**: New JSON config file, rewritten repair step, new SettingsService
-- **Frontend**: Updated store.js (4 new entity types), improved error handling in object.js
-- **Data migration**: Register slug changes from `case-management` to `procest`. The repair step re-import is idempotent — existing data is preserved because ConfigurationService handles upsert logic.
-- **Dependencies**: OpenRegister `ConfigurationService` must be available (already a runtime dependency)
+# Proposal: openregister-integration
+
+## Why
+
+Procest's data layer currently uses a hard-coded PHP repair step with only 6 minimal schemas (case, task, status, role, result, decision) registered under a `case-management` slug. The spec requires 12 schemas under the `procest` slug, imported via `ConfigurationService::importFromApp()` from an OpenAPI 3.0.0 JSON file — the same pattern used by opencatalogi and softwarecatalog. The frontend store only registers 8 of 12 entity types and lacks HTTP status-specific error handling.
+
+This change brings the backend configuration and frontend store layer in line with the openregister-integration spec (MVP tier only; V1 cascade behaviors are excluded).
+
+## What Changes
+
+- **Create `lib/Settings/procest_register.json`** — OpenAPI 3.0.0 config file defining the `procest` register with all 12 schemas and their full property definitions
+- **Rewrite `lib/Repair/InitializeSettings.php`** — Replace custom register/schema creation with `ConfigurationService::importFromApp('procest')` call, matching the opencatalogi/softwarecatalog pattern
+- **Register all 12 entity types in the frontend store** — Add `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType` to `store.js`
+- **Add HTTP status-specific error handling** in `object.js` — Parse 400/401/403/404/409/500 responses with user-friendly messages
+- **Add `SettingsService.php`** — Thin service to load configuration via ConfigurationService, matching the sister-app pattern
+
+## Capabilities
+
+### Modified Capabilities
+
+- **openregister-integration** — All MVP requirements (REQ-OREG-001 through REQ-OREG-008, REQ-OREG-010 through REQ-OREG-013) are being implemented or corrected. REQ-OREG-009 (cascade behaviors, V1) is deferred.
+
+## Impact
+
+- **Backend**: New JSON config file, rewritten repair step, new SettingsService
+- **Frontend**: Updated store.js (4 new entity types), improved error handling in object.js
+- **Data migration**: Register slug changes from `case-management` to `procest`. The repair step re-import is idempotent — existing data is preserved because ConfigurationService handles upsert logic.
+- **Dependencies**: OpenRegister `ConfigurationService` must be available (already a runtime dependency)
diff --git a/openspec/changes/archive/2026-02-26-openregister-integration/specs/openregister-integration/spec.md b/openspec/changes/archive/2026-02-26-openregister-integration/specs/openregister-integration/spec.md
index 7c3522a0..d5d1cb03 100644
--- a/openspec/changes/archive/2026-02-26-openregister-integration/specs/openregister-integration/spec.md
+++ b/openspec/changes/archive/2026-02-26-openregister-integration/specs/openregister-integration/spec.md
@@ -1,120 +1,120 @@
-# Delta Spec: openregister-integration
-
-This delta spec confirms the MVP requirements from the main openregister-integration spec and narrows the scope to what this change implements.
-
-## Scope
-
-**In scope (MVP)**: REQ-OREG-001, REQ-OREG-002, REQ-OREG-003, REQ-OREG-004, REQ-OREG-005, REQ-OREG-006, REQ-OREG-007, REQ-OREG-008, REQ-OREG-010, REQ-OREG-011, REQ-OREG-012, REQ-OREG-013
-
-**Deferred (V1)**: REQ-OREG-009 (cascade behaviors)
-
----
-
-## MODIFIED Requirements
-
-### Requirement: REQ-OREG-001 — Configuration File
-
-The system MUST define its register and all schemas in a JSON configuration file at `lib/Settings/procest_register.json` that follows the OpenAPI 3.0.0 format.
-
-**Current state**: No config file exists. Schemas are hard-coded in PHP with only 6 of 12 schemas and minimal properties.
-
-**Change**: Create the full OpenAPI 3.0.0 JSON config with all 12 schemas and complete property definitions as specified in the main spec.
-
-#### Scenario: Configuration file exists with all 12 schemas
-
-- GIVEN the Procest app source code
-- WHEN a developer inspects `lib/Settings/procest_register.json`
-- THEN the file MUST be valid JSON conforming to OpenAPI 3.0.0
-- AND it MUST define a register with slug `procest`
-- AND it MUST define exactly 12 schemas: `caseType`, `statusType`, `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType`, `case`, `task`, `role`, `result`, `decision`
-- AND each schema MUST include all required and optional properties as listed in the main spec
-
-#### Scenario: Schema type annotations
-
-- GIVEN each schema definition in the config file
-- THEN each MUST include a `@type` or `x-schema-org-type` annotation referencing the appropriate standard (e.g., `case` → `schema:Project`, `task` → `schema:Action`)
-
----
-
-### Requirement: REQ-OREG-002 — Auto-Configuration on Install
-
-The system MUST import the register configuration via `ConfigurationService::importFromApp('procest')` during the repair step, replacing the current custom register/schema creation logic.
-
-**Current state**: `InitializeSettings.php` uses custom code with RegisterService and SchemaMapper directly. Register slug is `case-management` instead of `procest`.
-
-**Change**: Rewrite to use `ConfigurationService::importFromApp()` pattern. Register slug becomes `procest`.
-
-#### Scenario: Repair step uses ConfigurationService
-
-- GIVEN Procest is installed on a Nextcloud instance with OpenRegister
-- WHEN the repair step `lib/Repair/InitializeSettings.php` runs
-- THEN it MUST call `ConfigurationService::importFromApp('procest')`
-- AND the `procest` register MUST be created or updated in OpenRegister
-- AND all 12 schemas MUST be created with their full property definitions
-
-#### Scenario: Repair step handles missing OpenRegister
-
-- GIVEN Procest is installed but OpenRegister is NOT installed
-- WHEN the repair step runs
-- THEN it MUST log a clear warning message
-- AND it MUST NOT crash or throw an unhandled exception
-
-#### Scenario: Repair step is idempotent
-
-- GIVEN the repair step has already run successfully
-- WHEN it runs again
-- THEN it MUST NOT create duplicate registers or schemas
-- AND existing data MUST remain intact
-
----
-
-### Requirement: REQ-OREG-005 — Pinia Store: All 12 Entity Types Registered
-
-The frontend MUST register all 12 entity types in the Pinia store on initialization.
-
-**Current state**: Only 8 types are registered (case, task, status, role, result, decision, caseType, statusType).
-
-**Change**: Add `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType` to store initialization.
-
-#### Scenario: All entity types registered on boot
-
-- GIVEN the Procest app loads in the browser
-- WHEN `initializeStores()` completes
-- THEN the object store MUST have all 12 entity types registered
-- AND each type MUST be usable for `fetchCollection`, `fetchObject`, `saveObject`, `deleteObject`
-
----
-
-### Requirement: REQ-OREG-008 — HTTP Status-Specific Error Handling
-
-The frontend store MUST parse HTTP response status codes and provide user-friendly error messages instead of generic "Failed to fetch" strings.
-
-**Current state**: All errors produce `Failed to fetch {type}: {statusText}` — no distinction between 400, 403, 404, 409, 500.
-
-**Change**: Add response status parsing with categorized error messages.
-
-#### Scenario: Validation error (HTTP 400/422)
-
-- GIVEN the user submits invalid data
-- WHEN the API returns HTTP 400 or 422
-- THEN the store error MUST include the validation details from the response body
-- AND the error MUST be structured so the UI can map errors to specific fields
-
-#### Scenario: Authorization error (HTTP 403)
-
-- GIVEN a user without sufficient permissions
-- WHEN the API returns HTTP 403
-- THEN the store error MUST include a message like "You do not have permission to perform this action"
-
-#### Scenario: Not found error (HTTP 404)
-
-- GIVEN a deleted or non-existent object
-- WHEN the API returns HTTP 404
-- THEN the store error MUST include a message like "The requested {type} could not be found"
-
-#### Scenario: Server error (HTTP 500)
-
-- GIVEN an unexpected server error
-- WHEN the API returns HTTP 500
-- THEN the store error MUST include "An unexpected error occurred. Please try again later."
-- AND the full error details MUST be logged to the browser console
+# Delta Spec: openregister-integration
+
+This delta spec confirms the MVP requirements from the main openregister-integration spec and narrows the scope to what this change implements.
+
+## Scope
+
+**In scope (MVP)**: REQ-OREG-001, REQ-OREG-002, REQ-OREG-003, REQ-OREG-004, REQ-OREG-005, REQ-OREG-006, REQ-OREG-007, REQ-OREG-008, REQ-OREG-010, REQ-OREG-011, REQ-OREG-012, REQ-OREG-013
+
+**Deferred (V1)**: REQ-OREG-009 (cascade behaviors)
+
+---
+
+## MODIFIED Requirements
+
+### Requirement: REQ-OREG-001 — Configuration File
+
+The system MUST define its register and all schemas in a JSON configuration file at `lib/Settings/procest_register.json` that follows the OpenAPI 3.0.0 format.
+
+**Current state**: No config file exists. Schemas are hard-coded in PHP with only 6 of 12 schemas and minimal properties.
+
+**Change**: Create the full OpenAPI 3.0.0 JSON config with all 12 schemas and complete property definitions as specified in the main spec.
+
+#### Scenario: Configuration file exists with all 12 schemas
+
+- GIVEN the Procest app source code
+- WHEN a developer inspects `lib/Settings/procest_register.json`
+- THEN the file MUST be valid JSON conforming to OpenAPI 3.0.0
+- AND it MUST define a register with slug `procest`
+- AND it MUST define exactly 12 schemas: `caseType`, `statusType`, `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType`, `case`, `task`, `role`, `result`, `decision`
+- AND each schema MUST include all required and optional properties as listed in the main spec
+
+#### Scenario: Schema type annotations
+
+- GIVEN each schema definition in the config file
+- THEN each MUST include a `@type` or `x-schema-org-type` annotation referencing the appropriate standard (e.g., `case` → `schema:Project`, `task` → `schema:Action`)
+
+---
+
+### Requirement: REQ-OREG-002 — Auto-Configuration on Install
+
+The system MUST import the register configuration via `ConfigurationService::importFromApp('procest')` during the repair step, replacing the current custom register/schema creation logic.
+
+**Current state**: `InitializeSettings.php` uses custom code with RegisterService and SchemaMapper directly. Register slug is `case-management` instead of `procest`.
+
+**Change**: Rewrite to use `ConfigurationService::importFromApp()` pattern. Register slug becomes `procest`.
+
+#### Scenario: Repair step uses ConfigurationService
+
+- GIVEN Procest is installed on a Nextcloud instance with OpenRegister
+- WHEN the repair step `lib/Repair/InitializeSettings.php` runs
+- THEN it MUST call `ConfigurationService::importFromApp('procest')`
+- AND the `procest` register MUST be created or updated in OpenRegister
+- AND all 12 schemas MUST be created with their full property definitions
+
+#### Scenario: Repair step handles missing OpenRegister
+
+- GIVEN Procest is installed but OpenRegister is NOT installed
+- WHEN the repair step runs
+- THEN it MUST log a clear warning message
+- AND it MUST NOT crash or throw an unhandled exception
+
+#### Scenario: Repair step is idempotent
+
+- GIVEN the repair step has already run successfully
+- WHEN it runs again
+- THEN it MUST NOT create duplicate registers or schemas
+- AND existing data MUST remain intact
+
+---
+
+### Requirement: REQ-OREG-005 — Pinia Store: All 12 Entity Types Registered
+
+The frontend MUST register all 12 entity types in the Pinia store on initialization.
+
+**Current state**: Only 8 types are registered (case, task, status, role, result, decision, caseType, statusType).
+
+**Change**: Add `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType` to store initialization.
+
+#### Scenario: All entity types registered on boot
+
+- GIVEN the Procest app loads in the browser
+- WHEN `initializeStores()` completes
+- THEN the object store MUST have all 12 entity types registered
+- AND each type MUST be usable for `fetchCollection`, `fetchObject`, `saveObject`, `deleteObject`
+
+---
+
+### Requirement: REQ-OREG-008 — HTTP Status-Specific Error Handling
+
+The frontend store MUST parse HTTP response status codes and provide user-friendly error messages instead of generic "Failed to fetch" strings.
+
+**Current state**: All errors produce `Failed to fetch {type}: {statusText}` — no distinction between 400, 403, 404, 409, 500.
+
+**Change**: Add response status parsing with categorized error messages.
+
+#### Scenario: Validation error (HTTP 400/422)
+
+- GIVEN the user submits invalid data
+- WHEN the API returns HTTP 400 or 422
+- THEN the store error MUST include the validation details from the response body
+- AND the error MUST be structured so the UI can map errors to specific fields
+
+#### Scenario: Authorization error (HTTP 403)
+
+- GIVEN a user without sufficient permissions
+- WHEN the API returns HTTP 403
+- THEN the store error MUST include a message like "You do not have permission to perform this action"
+
+#### Scenario: Not found error (HTTP 404)
+
+- GIVEN a deleted or non-existent object
+- WHEN the API returns HTTP 404
+- THEN the store error MUST include a message like "The requested {type} could not be found"
+
+#### Scenario: Server error (HTTP 500)
+
+- GIVEN an unexpected server error
+- WHEN the API returns HTTP 500
+- THEN the store error MUST include "An unexpected error occurred. Please try again later."
+- AND the full error details MUST be logged to the browser console
diff --git a/openspec/changes/archive/2026-02-26-openregister-integration/tasks.md b/openspec/changes/archive/2026-02-26-openregister-integration/tasks.md
index 292eaf71..68e4d7fa 100644
--- a/openspec/changes/archive/2026-02-26-openregister-integration/tasks.md
+++ b/openspec/changes/archive/2026-02-26-openregister-integration/tasks.md
@@ -1,29 +1,29 @@
-# Tasks: openregister-integration
-
-## 1. Backend — Configuration File
-
-- [x] 1.1 Create `lib/Settings/procest_register.json` — OpenAPI 3.0.0 JSON file defining the `procest` register with all 12 schemas. Each schema must include: all required/optional properties from the main spec (REQ-OREG-001), correct types/formats/enums/defaults, `x-schema-org-type` annotations, and `x-openregister` metadata block. Reference `softwarecatalog/lib/Settings/softwarecatalogus_register.json` for format conventions.
-
-## 2. Backend — Service and Repair Step
-
-- [x] 2.1 Create `lib/Service/SettingsService.php` — Thin service class that: gets ConfigurationService from the container, reads the JSON config file, calls `ConfigurationService::importFromApp('procest')`. Follow the softwarecatalog SettingsService pattern. Include a `loadConfiguration()` method that returns the import result.
-
-- [x] 2.2 Rewrite `lib/Repair/InitializeSettings.php` — Replace the custom `initializeRegisterAndSchemas()` method with: (a) check OpenRegister is enabled, (b) get SettingsService from container, (c) call `settingsService->loadConfiguration()`, (d) store register/schema IDs in app config for frontend consumption. Remove the hard-coded SCHEMAS constant. Keep the OpenRegister availability check and graceful error handling.
-
-## 3. Frontend — Store Registration
-
-- [x] 3.1 Update `src/store/store.js` — Add registrations for the 4 missing entity types: `resultType` (config key: `result_type_schema`), `roleType` (`role_type_schema`), `propertyDefinition` (`property_definition_schema`), `documentType` (`document_type_schema`), `decisionType` (`decision_type_schema`). Follow the existing pattern of checking `config.register && config.{key}` before calling `registerObjectType()`.
-
-## 4. Frontend — Error Handling
-
-- [x] 4.1 Add `_parseError(response, type)` method to `src/store/modules/object.js` — Parse HTTP response into structured error object `{ status, message, details, isValidation }`. Map status codes: 400/422 → parse body for field-level errors + `isValidation: true`; 401 → "Session expired, please log in again"; 403 → "You do not have permission to perform this action"; 404 → "The requested {type} could not be found"; 409 → "This {type} was modified by another user. Please reload."; 500+ → "An unexpected error occurred. Please try again later.". Log full details to console for all error types.
-
-- [x] 4.2 Update all CRUD methods in `object.js` to use `_parseError()` — Replace the current `throw new Error(...)` patterns in `fetchCollection`, `fetchObject`, `saveObject`, `deleteObject` with calls to `_parseError(response, type)`. Set `this.errors[type]` to the structured error object instead of a plain string. Ensure backward compatibility: components that read `errors[type]` as a string should still work (add a `toString()` or check for `.message` property).
-
-## 5. Verification
-
-- [ ] 5.1 Verify `procest_register.json` is valid JSON and contains all 12 schemas with correct property definitions
-- [ ] 5.2 Verify repair step runs without error on a clean environment (`occ maintenance:repair`)
-- [ ] 5.3 Verify all 12 entity types appear in `objectStore.objectTypes` after app load
-- [ ] 5.4 Verify error handling: trigger a 404 by fetching a non-existent object, confirm structured error object
-- [ ] 5.5 Verify SettingsService returns register and schema IDs to the settings endpoint
+# Tasks: openregister-integration
+
+## 1. Backend — Configuration File
+
+- [x] 1.1 Create `lib/Settings/procest_register.json` — OpenAPI 3.0.0 JSON file defining the `procest` register with all 12 schemas. Each schema must include: all required/optional properties from the main spec (REQ-OREG-001), correct types/formats/enums/defaults, `x-schema-org-type` annotations, and `x-openregister` metadata block. Reference `softwarecatalog/lib/Settings/softwarecatalogus_register.json` for format conventions.
+
+## 2. Backend — Service and Repair Step
+
+- [x] 2.1 Create `lib/Service/SettingsService.php` — Thin service class that: gets ConfigurationService from the container, reads the JSON config file, calls `ConfigurationService::importFromApp('procest')`. Follow the softwarecatalog SettingsService pattern. Include a `loadConfiguration()` method that returns the import result.
+
+- [x] 2.2 Rewrite `lib/Repair/InitializeSettings.php` — Replace the custom `initializeRegisterAndSchemas()` method with: (a) check OpenRegister is enabled, (b) get SettingsService from container, (c) call `settingsService->loadConfiguration()`, (d) store register/schema IDs in app config for frontend consumption. Remove the hard-coded SCHEMAS constant. Keep the OpenRegister availability check and graceful error handling.
+
+## 3. Frontend — Store Registration
+
+- [x] 3.1 Update `src/store/store.js` — Add registrations for the 4 missing entity types: `resultType` (config key: `result_type_schema`), `roleType` (`role_type_schema`), `propertyDefinition` (`property_definition_schema`), `documentType` (`document_type_schema`), `decisionType` (`decision_type_schema`). Follow the existing pattern of checking `config.register && config.{key}` before calling `registerObjectType()`.
+
+## 4. Frontend — Error Handling
+
+- [x] 4.1 Add `_parseError(response, type)` method to `src/store/modules/object.js` — Parse HTTP response into structured error object `{ status, message, details, isValidation }`. Map status codes: 400/422 → parse body for field-level errors + `isValidation: true`; 401 → "Session expired, please log in again"; 403 → "You do not have permission to perform this action"; 404 → "The requested {type} could not be found"; 409 → "This {type} was modified by another user. Please reload."; 500+ → "An unexpected error occurred. Please try again later.". Log full details to console for all error types.
+
+- [x] 4.2 Update all CRUD methods in `object.js` to use `_parseError()` — Replace the current `throw new Error(...)` patterns in `fetchCollection`, `fetchObject`, `saveObject`, `deleteObject` with calls to `_parseError(response, type)`. Set `this.errors[type]` to the structured error object instead of a plain string. Ensure backward compatibility: components that read `errors[type]` as a string should still work (add a `toString()` or check for `.message` property).
+
+## 5. Verification
+
+- [ ] 5.1 Verify `procest_register.json` is valid JSON and contains all 12 schemas with correct property definitions
+- [ ] 5.2 Verify repair step runs without error on a clean environment (`occ maintenance:repair`)
+- [ ] 5.3 Verify all 12 entity types appear in `objectStore.objectTypes` after app load
+- [ ] 5.4 Verify error handling: trigger a 404 by fetching a non-existent object, confirm structured error object
+- [ ] 5.5 Verify SettingsService returns register and schema IDs to the settings endpoint
diff --git a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/design.md b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/design.md
index 87ce2018..b5b1c4d8 100644
--- a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/design.md
+++ b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/design.md
@@ -1,92 +1,92 @@
-# Design: roles-decisions-mvp
-
-## Context
-
-CaseDetail.vue currently has 6 sections: header, status bar, status timeline, info+deadline panels, tasks, and activity. There is no participants section. The "result" flow uses a free-text `NcTextField` that stores a string directly on the case object's `result` field. The store already registers `role`, `result`, `roleType`, and `resultType` object types.
-
-## Goals / Non-Goals
-
-**Goals:**
-- Add a Participants section to CaseDetail showing roles grouped by type
-- Support add/remove participant with role type selection
-- Handler reassignment that updates both role and case assignee
-- Replace free-text result prompt with result type selector
-- Create proper result objects linked to case on completion
-
-**Non-Goals:**
-- Role type enforcement per case type (V1)
-- Role-based access control (V1)
-- Result type admin configuration (V1)
-- Decisions (V1)
-- External contact resolution (contacts not in Nextcloud users)
-
-## Decisions
-
-### DD-01: ParticipantsSection as Standalone Component
-
-**Decision**: Create `src/views/cases/components/ParticipantsSection.vue` that receives the case UUID as a prop and manages its own role fetching/CRUD.
-
-**Rationale**: Follows the pattern of other case detail sections (DeadlinePanel, ActivityTimeline, StatusTimeline) — each is a self-contained component. The section fetches roles on mount via `objectStore.fetchCollection('role', { '_filters[case]': caseUuid })`.
-
-### DD-02: Role Type Loading Strategy
-
-**Decision**: Load all role types once in CaseDetail (or ParticipantsSection on mount) and pass them down. For MVP, load ALL role types regardless of case type (V1 will filter by case type).
-
-**Rationale**: Role types are a small set (typically <20). Filtering by case type requires REQ-ROLE-002 (V1). For MVP, showing all role types is acceptable.
-
-### DD-03: User Picker for Participant Selection
-
-**Decision**: Use a simple `NcSelect` with Nextcloud user list fetched via OCS API (`/ocs/v2.php/cloud/users/details`) or filter the user list available from the current session.
-
-**Rationale**: Full user picker components from @nextcloud/vue are complex. A simple select with user display names is sufficient for MVP. The participant field stores the Nextcloud UID.
-
-**Fallback**: If fetching users is complex, allow free-text UID input with validation.
-
-### DD-04: Handler Shortcut via Generic Role Match
-
-**Decision**: Identify the handler role by checking `genericRole === 'handler'` on the role type. When reassigning, update both the role's `participant` and the case's `assignee` field.
-
-**Rationale**: The spec defines 8 standard generic roles. The handler generic role has special semantics — it syncs with the case `assignee` field. This avoids hardcoding a specific role type UUID.
-
-**Implementation**: When saving a handler reassignment, make two API calls: (1) save the role object with new participant, (2) save the case with new assignee. Both via `objectStore.saveObject()`.
-
-### DD-05: Result Type Selector Replaces Free-Text
-
-**Decision**: Replace the existing `NcTextField` result prompt in CaseDetail's status change flow with an `NcSelect` dropdown of result types. The selected result type creates a result object. For backward compatibility, keep the case `result` field updated with the result type name string.
-
-**Rationale**: The current free-text result is stored as `caseData.result` (string). The new flow creates a proper `result` object AND sets `caseData.result` to the result type name for display compatibility. If no result types exist for the case type, fall back to the existing free-text field.
-
-**Flow change in `onStatusSelected()`:
-1. User selects final status → result prompt shows
-2. Prompt now shows `NcSelect` with result types (fetched for the case's caseType)
-3. User selects a result type → confirm
-4. `confirmStatusChange()` creates result object + updates case with status, endDate, result name
-
-### DD-06: AddParticipantDialog Component
-
-**Decision**: Create `src/views/cases/components/AddParticipantDialog.vue` as a modal dialog with two fields: role type selector (NcSelect) and participant selector (NcSelect with user list).
-
-**Rationale**: Keeps the participants section clean. The dialog is responsible for validation (both fields required) and calls `objectStore.saveObject('role', ...)` on confirm.
-
-## File Map
-
-### New Files
-
-| File | Purpose |
-|------|---------|
-| `src/views/cases/components/ParticipantsSection.vue` | Participants section for case detail — lists roles, add/remove, handler reassign |
-| `src/views/cases/components/AddParticipantDialog.vue` | Modal dialog for adding a participant with role type + user selection |
-| `src/views/cases/components/ResultSection.vue` | Result display section for case detail — shows recorded result |
-
-### Modified Files
-
-| File | Changes |
-|------|---------|
-| `src/views/cases/CaseDetail.vue` | Add ParticipantsSection + ResultSection components, replace free-text result prompt with result type NcSelect |
-
-## Risks / Trade-offs
-
-- **[Trade-off] All role types shown in MVP** — Without case type filtering (V1), users see all role types even if irrelevant. Acceptable for MVP since the role type list is small.
-- **[Trade-off] User list fetching** — Fetching all Nextcloud users may be slow on large instances. For MVP, limit to first 100 users. V1 can add search-as-you-type.
-- **[Risk] Backward compat for `caseData.result`** — The existing `result` field is a string. The new flow writes both a result object AND the string. Old cases with string-only results still display correctly.
-- **[Risk] Role types may not exist** — If the OpenRegister schemas don't have role type seed data, the participants section has no role types to offer. The AddParticipantDialog should handle empty role types gracefully.
+# Design: roles-decisions-mvp
+
+## Context
+
+CaseDetail.vue currently has 6 sections: header, status bar, status timeline, info+deadline panels, tasks, and activity. There is no participants section. The "result" flow uses a free-text `NcTextField` that stores a string directly on the case object's `result` field. The store already registers `role`, `result`, `roleType`, and `resultType` object types.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Add a Participants section to CaseDetail showing roles grouped by type
+- Support add/remove participant with role type selection
+- Handler reassignment that updates both role and case assignee
+- Replace free-text result prompt with result type selector
+- Create proper result objects linked to case on completion
+
+**Non-Goals:**
+- Role type enforcement per case type (V1)
+- Role-based access control (V1)
+- Result type admin configuration (V1)
+- Decisions (V1)
+- External contact resolution (contacts not in Nextcloud users)
+
+## Decisions
+
+### DD-01: ParticipantsSection as Standalone Component
+
+**Decision**: Create `src/views/cases/components/ParticipantsSection.vue` that receives the case UUID as a prop and manages its own role fetching/CRUD.
+
+**Rationale**: Follows the pattern of other case detail sections (DeadlinePanel, ActivityTimeline, StatusTimeline) — each is a self-contained component. The section fetches roles on mount via `objectStore.fetchCollection('role', { '_filters[case]': caseUuid })`.
+
+### DD-02: Role Type Loading Strategy
+
+**Decision**: Load all role types once in CaseDetail (or ParticipantsSection on mount) and pass them down. For MVP, load ALL role types regardless of case type (V1 will filter by case type).
+
+**Rationale**: Role types are a small set (typically <20). Filtering by case type requires REQ-ROLE-002 (V1). For MVP, showing all role types is acceptable.
+
+### DD-03: User Picker for Participant Selection
+
+**Decision**: Use a simple `NcSelect` with Nextcloud user list fetched via OCS API (`/ocs/v2.php/cloud/users/details`) or filter the user list available from the current session.
+
+**Rationale**: Full user picker components from @nextcloud/vue are complex. A simple select with user display names is sufficient for MVP. The participant field stores the Nextcloud UID.
+
+**Fallback**: If fetching users is complex, allow free-text UID input with validation.
+
+### DD-04: Handler Shortcut via Generic Role Match
+
+**Decision**: Identify the handler role by checking `genericRole === 'handler'` on the role type. When reassigning, update both the role's `participant` and the case's `assignee` field.
+
+**Rationale**: The spec defines 8 standard generic roles. The handler generic role has special semantics — it syncs with the case `assignee` field. This avoids hardcoding a specific role type UUID.
+
+**Implementation**: When saving a handler reassignment, make two API calls: (1) save the role object with new participant, (2) save the case with new assignee. Both via `objectStore.saveObject()`.
+
+### DD-05: Result Type Selector Replaces Free-Text
+
+**Decision**: Replace the existing `NcTextField` result prompt in CaseDetail's status change flow with an `NcSelect` dropdown of result types. The selected result type creates a result object. For backward compatibility, keep the case `result` field updated with the result type name string.
+
+**Rationale**: The current free-text result is stored as `caseData.result` (string). The new flow creates a proper `result` object AND sets `caseData.result` to the result type name for display compatibility. If no result types exist for the case type, fall back to the existing free-text field.
+
+**Flow change in `onStatusSelected()`:
+1. User selects final status → result prompt shows
+2. Prompt now shows `NcSelect` with result types (fetched for the case's caseType)
+3. User selects a result type → confirm
+4. `confirmStatusChange()` creates result object + updates case with status, endDate, result name
+
+### DD-06: AddParticipantDialog Component
+
+**Decision**: Create `src/views/cases/components/AddParticipantDialog.vue` as a modal dialog with two fields: role type selector (NcSelect) and participant selector (NcSelect with user list).
+
+**Rationale**: Keeps the participants section clean. The dialog is responsible for validation (both fields required) and calls `objectStore.saveObject('role', ...)` on confirm.
+
+## File Map
+
+### New Files
+
+| File | Purpose |
+|------|---------|
+| `src/views/cases/components/ParticipantsSection.vue` | Participants section for case detail — lists roles, add/remove, handler reassign |
+| `src/views/cases/components/AddParticipantDialog.vue` | Modal dialog for adding a participant with role type + user selection |
+| `src/views/cases/components/ResultSection.vue` | Result display section for case detail — shows recorded result |
+
+### Modified Files
+
+| File | Changes |
+|------|---------|
+| `src/views/cases/CaseDetail.vue` | Add ParticipantsSection + ResultSection components, replace free-text result prompt with result type NcSelect |
+
+## Risks / Trade-offs
+
+- **[Trade-off] All role types shown in MVP** — Without case type filtering (V1), users see all role types even if irrelevant. Acceptable for MVP since the role type list is small.
+- **[Trade-off] User list fetching** — Fetching all Nextcloud users may be slow on large instances. For MVP, limit to first 100 users. V1 can add search-as-you-type.
+- **[Risk] Backward compat for `caseData.result`** — The existing `result` field is a string. The new flow writes both a result object AND the string. Old cases with string-only results still display correctly.
+- **[Risk] Role types may not exist** — If the OpenRegister schemas don't have role type seed data, the participants section has no role types to offer. The AddParticipantDialog should handle empty role types gracefully.
diff --git a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/proposal.md b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/proposal.md
index 9dc0d855..c81a28c8 100644
--- a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/proposal.md
+++ b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/proposal.md
@@ -1,37 +1,37 @@
-# Proposal: roles-decisions-mvp
-
-## Summary
-
-Implement the MVP tier of the Roles & Decisions spec for Procest: role assignment on cases (participants section with add/remove/reassign), handler assignment shortcut, participant display on case detail, role validation, and case result recording on completion.
-
-## Problem
-
-Cases currently only have a flat `assignee` field — there's no structured way to track multiple participants (handler, initiator, advisor, coordinator) or record formal case outcomes. Handlers are assigned directly on the case object without role type semantics.
-
-## Scope
-
-**In scope (MVP — 5 requirements):**
-- REQ-ROLE-001: Role assignment on cases (CRUD for role objects linked to case)
-- REQ-ROLE-003: Handler assignment shortcut (assigns handler role + updates case assignee)
-- REQ-ROLE-005: Participant display on case detail (grouped by role type, with avatars)
-- REQ-ROLE-006: Role validation (required fields, case existence check)
-- REQ-RESULT-001: Case result recording (select result type on completion, set endDate)
-
-**Out of scope (V1):**
-- REQ-ROLE-002: Role type enforcement from case type
-- REQ-ROLE-004: Role-based case access / RBAC
-- REQ-RESULT-002: Result type admin configuration UI
-- REQ-DECISION-*: Full decisions system (CRUD, validity periods, decision types)
-
-## Approach
-
-1. Create a `ParticipantsSection.vue` component for CaseDetail — fetches roles filtered by case UUID, displays grouped by role type, supports add/remove/reassign handler
-2. Create a `ResultSection.vue` component for CaseDetail — shows result if exists, provides result type selection during case completion flow
-3. Create an `AddParticipantDialog.vue` for role assignment — role type dropdown + user picker
-4. Extend the existing status change flow in CaseDetail to prompt for result selection when transitioning to a final status
-5. All data uses existing store patterns (role, result, roleType, resultType are already registered)
-
-## Dependencies
-
-- OpenRegister backend (role/result/roleType/resultType schemas must exist in the register)
-- Store object types already registered: `role`, `result`, `roleType`, `resultType`
+# Proposal: roles-decisions-mvp
+
+## Summary
+
+Implement the MVP tier of the Roles & Decisions spec for Procest: role assignment on cases (participants section with add/remove/reassign), handler assignment shortcut, participant display on case detail, role validation, and case result recording on completion.
+
+## Problem
+
+Cases currently only have a flat `assignee` field — there's no structured way to track multiple participants (handler, initiator, advisor, coordinator) or record formal case outcomes. Handlers are assigned directly on the case object without role type semantics.
+
+## Scope
+
+**In scope (MVP — 5 requirements):**
+- REQ-ROLE-001: Role assignment on cases (CRUD for role objects linked to case)
+- REQ-ROLE-003: Handler assignment shortcut (assigns handler role + updates case assignee)
+- REQ-ROLE-005: Participant display on case detail (grouped by role type, with avatars)
+- REQ-ROLE-006: Role validation (required fields, case existence check)
+- REQ-RESULT-001: Case result recording (select result type on completion, set endDate)
+
+**Out of scope (V1):**
+- REQ-ROLE-002: Role type enforcement from case type
+- REQ-ROLE-004: Role-based case access / RBAC
+- REQ-RESULT-002: Result type admin configuration UI
+- REQ-DECISION-*: Full decisions system (CRUD, validity periods, decision types)
+
+## Approach
+
+1. Create a `ParticipantsSection.vue` component for CaseDetail — fetches roles filtered by case UUID, displays grouped by role type, supports add/remove/reassign handler
+2. Create a `ResultSection.vue` component for CaseDetail — shows result if exists, provides result type selection during case completion flow
+3. Create an `AddParticipantDialog.vue` for role assignment — role type dropdown + user picker
+4. Extend the existing status change flow in CaseDetail to prompt for result selection when transitioning to a final status
+5. All data uses existing store patterns (role, result, roleType, resultType are already registered)
+
+## Dependencies
+
+- OpenRegister backend (role/result/roleType/resultType schemas must exist in the register)
+- Store object types already registered: `role`, `result`, `roleType`, `resultType`
diff --git a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/specs/roles-decisions-mvp/spec.md b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/specs/roles-decisions-mvp/spec.md
index f11af56c..cb99595d 100644
--- a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/specs/roles-decisions-mvp/spec.md
+++ b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/specs/roles-decisions-mvp/spec.md
@@ -1,142 +1,142 @@
-# Delta Spec: roles-decisions-mvp
-
-This delta spec scopes the MVP implementation of roles and results from the main `roles-decisions` spec.
-
-## Scope
-
-**In scope (MVP)**: REQ-ROLE-001, REQ-ROLE-003, REQ-ROLE-005, REQ-ROLE-006, REQ-RESULT-001
-
-**Deferred (V1)**: REQ-ROLE-002, REQ-ROLE-004, REQ-RESULT-002, REQ-DECISION-001 through REQ-DECISION-005
-
----
-
-## Current State
-
-### Existing Code
-
-- **`src/store/store.js`** — Already registers `role`, `result`, `roleType`, `resultType` object types with OpenRegister schema/register IDs
-- **`src/views/cases/CaseDetail.vue`** — Has 6 sections (header, status bar, timeline, info+deadline, tasks, activity). No participants or results sections exist.
-- **`src/store/modules/object.js`** — Full CRUD for any registered object type: `fetchCollection`, `fetchObject`, `saveObject`, `deleteObject`
-- **Case object** — Has `assignee` field (flat string, Nextcloud UID) but no structured role references
-
-### What's Missing
-
-1. No `ParticipantsSection.vue` on case detail
-2. No `ResultSection.vue` on case detail
-3. No `AddParticipantDialog.vue` for role assignment
-4. No result selection during case completion flow
-5. No handler reassign UI
-6. Roles not fetched or displayed anywhere
-
----
-
-## MODIFIED Requirements
-
-### Requirement: REQ-ROLE-001 — Role Assignment on Cases (MVP)
-
-**Current state**: Cases only have a flat `assignee` field. No role objects exist in the UI.
-
-**Change**: Add participants section to CaseDetail that creates/reads/deletes role objects filtered by case UUID.
-
-#### Scenario: Assign a participant to a case
-
-- GIVEN a case exists and role types are loaded
-- WHEN the user clicks "Add Participant" and selects a role type and Nextcloud user
-- THEN a role object MUST be created with: `name` (role type name), `roleType` (UUID), `case` (case UUID), `participant` (user UID)
-- AND the participant MUST appear in the Participants section grouped under the role type
-
-#### Scenario: Remove a participant role
-
-- GIVEN a case has an advisor role assigned
-- WHEN the user clicks the remove action on that role
-- THEN the role object MUST be deleted via the store
-- AND the participant MUST disappear from the section
-
----
-
-### Requirement: REQ-ROLE-003 — Handler Assignment Shortcut (MVP)
-
-**Current state**: Handler is set via the `assignee` field on the case info form.
-
-**Change**: Add a "Reassign" action on the handler role that updates both the role participant and the case `assignee` field in one action.
-
-#### Scenario: Reassign handler
-
-- GIVEN a case has a handler role for user A
-- WHEN the coordinator clicks "Reassign" and selects user B
-- THEN the handler role's `participant` MUST be updated to user B
-- AND the case's `assignee` field MUST be updated to user B
-- AND both changes MUST be saved via the store
-
-#### Scenario: Assign first handler
-
-- GIVEN a case has no handler role
-- WHEN the user clicks "Assign Handler" and selects a user
-- THEN a new handler role MUST be created
-- AND the case `assignee` MUST be set to the selected user
-
----
-
-### Requirement: REQ-ROLE-005 — Participant Display on Case Detail (MVP)
-
-**Current state**: No participants section exists.
-
-**Change**: Add a ParticipantsSection component between the info/deadline panels and the tasks section.
-
-#### Scenario: Display participants grouped by role type
-
-- GIVEN a case has roles: handler (Jan), initiator (Petra), advisor (Dr. Bakker)
-- WHEN the user views the case detail
-- THEN the Participants section MUST display all three grouped by role type label
-- AND each participant MUST show their display name (resolved from Nextcloud user)
-- AND the handler MUST have a "Reassign" action
-- AND an "Add Participant" button MUST be visible
-
-#### Scenario: No participants
-
-- GIVEN a case has no role assignments
-- THEN an empty state MUST show with an "Assign Handler" prompt
-
----
-
-### Requirement: REQ-ROLE-006 — Role Validation (MVP)
-
-**Current state**: No validation — roles don't exist in UI yet.
-
-**Change**: Validate role data before saving.
-
-#### Scenario: Required fields
-
-- GIVEN the user submits a role without participant or roleType
-- THEN the store MUST reject with a validation error
-- AND the dialog MUST show the error message
-
----
-
-### Requirement: REQ-RESULT-001 — Case Result Recording (MVP)
-
-**Current state**: CaseDetail has a result prompt overlay when transitioning to final status, but it uses a free-text field.
-
-**Change**: Replace the free-text result input with a result type selector. When the user transitions to a final status, they must select a result type. A result object is created and linked to the case.
-
-#### Scenario: Record result on case completion
-
-- GIVEN a case is being transitioned to a final status
-- AND result types exist for the case's case type
-- WHEN the user selects a result type from the dropdown
-- THEN a result object MUST be created with: `name` (result type name), `case` (case UUID), `resultType` (result type UUID)
-- AND the case `endDate` MUST be set to today
-- AND the case status MUST transition to the final status
-
-#### Scenario: No result types configured
-
-- GIVEN a case type has no result types
-- WHEN the user transitions to a final status
-- THEN the system MUST allow closure without result type selection
-- AND a generic result MUST be recorded with the case closure info
-
-#### Scenario: Attempt second result
-
-- GIVEN a case already has a result
-- WHEN another result creation is attempted
-- THEN the system MUST reject with "Case already has a result"
+# Delta Spec: roles-decisions-mvp
+
+This delta spec scopes the MVP implementation of roles and results from the main `roles-decisions` spec.
+
+## Scope
+
+**In scope (MVP)**: REQ-ROLE-001, REQ-ROLE-003, REQ-ROLE-005, REQ-ROLE-006, REQ-RESULT-001
+
+**Deferred (V1)**: REQ-ROLE-002, REQ-ROLE-004, REQ-RESULT-002, REQ-DECISION-001 through REQ-DECISION-005
+
+---
+
+## Current State
+
+### Existing Code
+
+- **`src/store/store.js`** — Already registers `role`, `result`, `roleType`, `resultType` object types with OpenRegister schema/register IDs
+- **`src/views/cases/CaseDetail.vue`** — Has 6 sections (header, status bar, timeline, info+deadline, tasks, activity). No participants or results sections exist.
+- **`src/store/modules/object.js`** — Full CRUD for any registered object type: `fetchCollection`, `fetchObject`, `saveObject`, `deleteObject`
+- **Case object** — Has `assignee` field (flat string, Nextcloud UID) but no structured role references
+
+### What's Missing
+
+1. No `ParticipantsSection.vue` on case detail
+2. No `ResultSection.vue` on case detail
+3. No `AddParticipantDialog.vue` for role assignment
+4. No result selection during case completion flow
+5. No handler reassign UI
+6. Roles not fetched or displayed anywhere
+
+---
+
+## MODIFIED Requirements
+
+### Requirement: REQ-ROLE-001 — Role Assignment on Cases (MVP)
+
+**Current state**: Cases only have a flat `assignee` field. No role objects exist in the UI.
+
+**Change**: Add participants section to CaseDetail that creates/reads/deletes role objects filtered by case UUID.
+
+#### Scenario: Assign a participant to a case
+
+- GIVEN a case exists and role types are loaded
+- WHEN the user clicks "Add Participant" and selects a role type and Nextcloud user
+- THEN a role object MUST be created with: `name` (role type name), `roleType` (UUID), `case` (case UUID), `participant` (user UID)
+- AND the participant MUST appear in the Participants section grouped under the role type
+
+#### Scenario: Remove a participant role
+
+- GIVEN a case has an advisor role assigned
+- WHEN the user clicks the remove action on that role
+- THEN the role object MUST be deleted via the store
+- AND the participant MUST disappear from the section
+
+---
+
+### Requirement: REQ-ROLE-003 — Handler Assignment Shortcut (MVP)
+
+**Current state**: Handler is set via the `assignee` field on the case info form.
+
+**Change**: Add a "Reassign" action on the handler role that updates both the role participant and the case `assignee` field in one action.
+
+#### Scenario: Reassign handler
+
+- GIVEN a case has a handler role for user A
+- WHEN the coordinator clicks "Reassign" and selects user B
+- THEN the handler role's `participant` MUST be updated to user B
+- AND the case's `assignee` field MUST be updated to user B
+- AND both changes MUST be saved via the store
+
+#### Scenario: Assign first handler
+
+- GIVEN a case has no handler role
+- WHEN the user clicks "Assign Handler" and selects a user
+- THEN a new handler role MUST be created
+- AND the case `assignee` MUST be set to the selected user
+
+---
+
+### Requirement: REQ-ROLE-005 — Participant Display on Case Detail (MVP)
+
+**Current state**: No participants section exists.
+
+**Change**: Add a ParticipantsSection component between the info/deadline panels and the tasks section.
+
+#### Scenario: Display participants grouped by role type
+
+- GIVEN a case has roles: handler (Jan), initiator (Petra), advisor (Dr. Bakker)
+- WHEN the user views the case detail
+- THEN the Participants section MUST display all three grouped by role type label
+- AND each participant MUST show their display name (resolved from Nextcloud user)
+- AND the handler MUST have a "Reassign" action
+- AND an "Add Participant" button MUST be visible
+
+#### Scenario: No participants
+
+- GIVEN a case has no role assignments
+- THEN an empty state MUST show with an "Assign Handler" prompt
+
+---
+
+### Requirement: REQ-ROLE-006 — Role Validation (MVP)
+
+**Current state**: No validation — roles don't exist in UI yet.
+
+**Change**: Validate role data before saving.
+
+#### Scenario: Required fields
+
+- GIVEN the user submits a role without participant or roleType
+- THEN the store MUST reject with a validation error
+- AND the dialog MUST show the error message
+
+---
+
+### Requirement: REQ-RESULT-001 — Case Result Recording (MVP)
+
+**Current state**: CaseDetail has a result prompt overlay when transitioning to final status, but it uses a free-text field.
+
+**Change**: Replace the free-text result input with a result type selector. When the user transitions to a final status, they must select a result type. A result object is created and linked to the case.
+
+#### Scenario: Record result on case completion
+
+- GIVEN a case is being transitioned to a final status
+- AND result types exist for the case's case type
+- WHEN the user selects a result type from the dropdown
+- THEN a result object MUST be created with: `name` (result type name), `case` (case UUID), `resultType` (result type UUID)
+- AND the case `endDate` MUST be set to today
+- AND the case status MUST transition to the final status
+
+#### Scenario: No result types configured
+
+- GIVEN a case type has no result types
+- WHEN the user transitions to a final status
+- THEN the system MUST allow closure without result type selection
+- AND a generic result MUST be recorded with the case closure info
+
+#### Scenario: Attempt second result
+
+- GIVEN a case already has a result
+- WHEN another result creation is attempted
+- THEN the system MUST reject with "Case already has a result"
diff --git a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/tasks.md b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/tasks.md
index e4ada52a..fb1f8bf8 100644
--- a/openspec/changes/archive/2026-02-26-roles-decisions-mvp/tasks.md
+++ b/openspec/changes/archive/2026-02-26-roles-decisions-mvp/tasks.md
@@ -1,36 +1,36 @@
-# Tasks: roles-decisions-mvp
-
-## 1. Participants Section Component
-
-- [x] 1.1 Create `src/views/cases/components/ParticipantsSection.vue` — Self-contained section receiving `caseId` prop. On mount: fetches roles via `objectStore.fetchCollection('role', { '_filters[case]': caseId })` and role types via `objectStore.fetchCollection('roleType', { _limit: 100 })`. Displays roles grouped by role type name. Each role row shows: participant display name (resolved via OCS user details or fallback to UID), role type label, and a remove button (trash icon). The handler role (identified by role type with `genericRole === 'handler'`) shows a "Reassign" button instead of remove. Empty state shows "No participants" with a prominent "Assign Handler" button. Footer has an "Add Participant" button that opens the AddParticipantDialog. Emits `@handler-changed` when handler is reassigned (so CaseDetail can refresh assignee). Follow BEM class naming `participants-section__*`. Use NcButton, NcLoadingIcon, NcEmptyContent.
-
-- [x] 1.2 Create `src/views/cases/components/AddParticipantDialog.vue` — Modal dialog with two fields: (a) Role type selector (NcSelect populated from roleTypes prop), (b) Participant selector (NcSelect populated with Nextcloud users fetched from OCS API `GET /ocs/v2.php/cloud/users/details?format=json&limit=100` with requesttoken header; maps to `{ id: uid, label: displayName }` options). Validates both fields required before enabling "Add" button. On confirm: calls `objectStore.saveObject('role', { name: selectedRoleType.name, roleType: selectedRoleType.id, case: caseId, participant: selectedUser.id })`. Emits `@created` with the new role. Emits `@close` to dismiss. Uses `NcDialog` or `NcModal` for the container.
-
-## 2. Handler Reassignment
-
-- [x] 2.1 Add handler reassign flow to `ParticipantsSection.vue` — When "Reassign" is clicked on the handler role, show an inline NcSelect with the user list (same as AddParticipantDialog). On selection: (a) update the role via `objectStore.saveObject('role', { ...existingRole, participant: newUser })`, (b) update the case via `objectStore.saveObject('case', { id: caseId, assignee: newUser })`, (c) emit `@handler-changed`. If no handler role exists and "Assign Handler" is clicked, open AddParticipantDialog pre-filtered to handler role types.
-
-## 3. Result Section Component
-
-- [x] 3.1 Create `src/views/cases/components/ResultSection.vue` — Simple display component receiving `result` prop (result object or null) and `resultTypes` prop. If result exists: shows result type name, description, and date created. If no result: shows "No result recorded yet" in muted text. Read-only — result creation happens in the status change flow.
-
-## 4. Result Type Selector in Status Change Flow
-
-- [x] 4.1 Update `src/views/cases/CaseDetail.vue` — Replace the free-text `NcTextField` in the result prompt with an `NcSelect` dropdown of result types. Load result types filtered by case type on mount: `objectStore.fetchCollection('resultType', { '_filters[caseType]': caseTypeId, _limit: 100 })`. Store in `resultTypes` data. In `confirmStatusChange()`: (a) create a result object via `objectStore.saveObject('result', { name: selectedResultType.name, case: caseId, resultType: selectedResultType.id })`, (b) set `updateData.result = selectedResultType.name` for backward compat, (c) set `updateData.endDate`. If no result types exist for the case type, fall back to existing free-text field. Add `resultTypes` and `selectedResultType` to data. Import and add `ParticipantsSection` and `ResultSection` components to template.
-
-## 5. Integration — CaseDetail Layout
-
-- [x] 5.1 Update `src/views/cases/CaseDetail.vue` template — Add `` between the info/deadline panels and the tasks section, passing `caseId` prop and handling `@handler-changed` to refresh case data. Add `` below the info panel (or in the info panel area), passing the result object and result types. Import both components.
-
-## 6. Verification
-
-- [ ] 6.1 Verify participants section shows on case detail with grouped roles
-- [ ] 6.2 Verify "Add Participant" dialog opens and creates a role
-- [ ] 6.3 Verify handler "Reassign" updates both role and case assignee
-- [ ] 6.4 Verify "Assign Handler" appears on cases with no handler
-- [ ] 6.5 Verify removing a non-handler role deletes it from the section
-- [ ] 6.6 Verify result type dropdown appears when transitioning to final status
-- [ ] 6.7 Verify result object is created on case completion
-- [ ] 6.8 Verify free-text fallback works when no result types configured
-- [ ] 6.9 Verify result displays in the ResultSection after case closure
-- [ ] 6.10 Verify role validation rejects missing participant or roleType
+# Tasks: roles-decisions-mvp
+
+## 1. Participants Section Component
+
+- [x] 1.1 Create `src/views/cases/components/ParticipantsSection.vue` — Self-contained section receiving `caseId` prop. On mount: fetches roles via `objectStore.fetchCollection('role', { '_filters[case]': caseId })` and role types via `objectStore.fetchCollection('roleType', { _limit: 100 })`. Displays roles grouped by role type name. Each role row shows: participant display name (resolved via OCS user details or fallback to UID), role type label, and a remove button (trash icon). The handler role (identified by role type with `genericRole === 'handler'`) shows a "Reassign" button instead of remove. Empty state shows "No participants" with a prominent "Assign Handler" button. Footer has an "Add Participant" button that opens the AddParticipantDialog. Emits `@handler-changed` when handler is reassigned (so CaseDetail can refresh assignee). Follow BEM class naming `participants-section__*`. Use NcButton, NcLoadingIcon, NcEmptyContent.
+
+- [x] 1.2 Create `src/views/cases/components/AddParticipantDialog.vue` — Modal dialog with two fields: (a) Role type selector (NcSelect populated from roleTypes prop), (b) Participant selector (NcSelect populated with Nextcloud users fetched from OCS API `GET /ocs/v2.php/cloud/users/details?format=json&limit=100` with requesttoken header; maps to `{ id: uid, label: displayName }` options). Validates both fields required before enabling "Add" button. On confirm: calls `objectStore.saveObject('role', { name: selectedRoleType.name, roleType: selectedRoleType.id, case: caseId, participant: selectedUser.id })`. Emits `@created` with the new role. Emits `@close` to dismiss. Uses `NcDialog` or `NcModal` for the container.
+
+## 2. Handler Reassignment
+
+- [x] 2.1 Add handler reassign flow to `ParticipantsSection.vue` — When "Reassign" is clicked on the handler role, show an inline NcSelect with the user list (same as AddParticipantDialog). On selection: (a) update the role via `objectStore.saveObject('role', { ...existingRole, participant: newUser })`, (b) update the case via `objectStore.saveObject('case', { id: caseId, assignee: newUser })`, (c) emit `@handler-changed`. If no handler role exists and "Assign Handler" is clicked, open AddParticipantDialog pre-filtered to handler role types.
+
+## 3. Result Section Component
+
+- [x] 3.1 Create `src/views/cases/components/ResultSection.vue` — Simple display component receiving `result` prop (result object or null) and `resultTypes` prop. If result exists: shows result type name, description, and date created. If no result: shows "No result recorded yet" in muted text. Read-only — result creation happens in the status change flow.
+
+## 4. Result Type Selector in Status Change Flow
+
+- [x] 4.1 Update `src/views/cases/CaseDetail.vue` — Replace the free-text `NcTextField` in the result prompt with an `NcSelect` dropdown of result types. Load result types filtered by case type on mount: `objectStore.fetchCollection('resultType', { '_filters[caseType]': caseTypeId, _limit: 100 })`. Store in `resultTypes` data. In `confirmStatusChange()`: (a) create a result object via `objectStore.saveObject('result', { name: selectedResultType.name, case: caseId, resultType: selectedResultType.id })`, (b) set `updateData.result = selectedResultType.name` for backward compat, (c) set `updateData.endDate`. If no result types exist for the case type, fall back to existing free-text field. Add `resultTypes` and `selectedResultType` to data. Import and add `ParticipantsSection` and `ResultSection` components to template.
+
+## 5. Integration — CaseDetail Layout
+
+- [x] 5.1 Update `src/views/cases/CaseDetail.vue` template — Add `` between the info/deadline panels and the tasks section, passing `caseId` prop and handling `@handler-changed` to refresh case data. Add `` below the info panel (or in the info panel area), passing the result object and result types. Import both components.
+
+## 6. Verification
+
+- [ ] 6.1 Verify participants section shows on case detail with grouped roles
+- [ ] 6.2 Verify "Add Participant" dialog opens and creates a role
+- [ ] 6.3 Verify handler "Reassign" updates both role and case assignee
+- [ ] 6.4 Verify "Assign Handler" appears on cases with no handler
+- [ ] 6.5 Verify removing a non-handler role deletes it from the section
+- [ ] 6.6 Verify result type dropdown appears when transitioning to final status
+- [ ] 6.7 Verify result object is created on case completion
+- [ ] 6.8 Verify free-text fallback works when no result types configured
+- [ ] 6.9 Verify result displays in the ResultSection after case closure
+- [ ] 6.10 Verify role validation rejects missing participant or roleType
diff --git a/openspec/changes/archive/2026-02-26-task-management/.openspec.yaml b/openspec/changes/archive/2026-02-26-task-management/.openspec.yaml
index c1286dbe..bda4da5a 100644
--- a/openspec/changes/archive/2026-02-26-task-management/.openspec.yaml
+++ b/openspec/changes/archive/2026-02-26-task-management/.openspec.yaml
@@ -1,5 +1,5 @@
-change: task-management
-project: procest
-schema: conduction
-created: 2026-02-25
-status: drafting
+change: task-management
+project: procest
+schema: conduction
+created: 2026-02-25
+status: drafting
diff --git a/openspec/changes/archive/2026-02-26-task-management/design.md b/openspec/changes/archive/2026-02-26-task-management/design.md
index cd5f9e6f..cdd01bb3 100644
--- a/openspec/changes/archive/2026-02-26-task-management/design.md
+++ b/openspec/changes/archive/2026-02-26-task-management/design.md
@@ -1,169 +1,169 @@
-# Design: task-management
-
-## Architecture Overview
-
-This is a **frontend-only** change. Tasks are already modeled in OpenRegister (the `task` schema exists and the object type is registered in the Pinia store). The implementation adds Vue components for task management and helper utilities for lifecycle enforcement and date calculations.
-
-```
-┌──────────────────────────────────────────────────┐
-│ New / Modified Vue Components │
-│ │
-│ TaskList.vue — Global task list with filters│
-│ TaskDetail.vue — Task create/edit/view form │
-│ CaseDetail.vue — Enhanced tasks section │
-│ MainMenu.vue — "Tasks" nav item │
-│ App.vue — Route: #/tasks, #/tasks/:id │
-└──────────────┬───────────────────────────────────┘
- │
-┌──────────────▼───────────────────────────────────┐
-│ Helpers (src/utils/) │
-│ │
-│ taskLifecycle.js — Status transitions, allowed │
-│ actions, validation │
-│ taskHelpers.js — Overdue calc, priority sort, │
-│ date formatting │
-└──────────────┬───────────────────────────────────┘
- │
-┌──────────────▼───────────────────────────────────┐
-│ useObjectStore (existing — no changes) │
-│ fetchCollection / fetchObject / saveObject / │
-│ deleteObject — all CRUD via OpenRegister API │
-└──────────────────────────────────────────────────┘
-```
-
-## API Design
-
-No new backend API endpoints. All operations use the existing OpenRegister API:
-
-### `GET /apps/openregister/api/objects/procest/task`
-
-**Query Parameters:**
-- `_limit` — Page size (default 20)
-- `_offset` — Pagination offset
-- `_search` — Full-text search on title
-- `_order` — Sort order, e.g. `{"dueDate":"asc"}`
-- `_filters[status]` — Filter by status
-- `_filters[assignee]` — Filter by assignee UID
-- `_filters[case]` — Filter by parent case UUID
-- `_filters[priority]` — Filter by priority
-
-**Response:**
-```json
-{
- "results": [
- {
- "id": "uuid",
- "title": "Controleer bouwtekeningen",
- "description": "",
- "status": "available",
- "assignee": "jan.devries",
- "case": "case-uuid",
- "dueDate": "2026-03-01T17:00:00Z",
- "priority": "normal",
- "completedDate": null
- }
- ],
- "total": 23,
- "page": 1,
- "pages": 2
-}
-```
-
-### `POST /apps/openregister/api/objects/procest/task`
-
-**Request:**
-```json
-{
- "title": "Controleer bouwtekeningen",
- "case": "case-uuid",
- "status": "available",
- "priority": "normal",
- "assignee": "jan.devries",
- "dueDate": "2026-03-01T17:00:00Z"
-}
-```
-
-### `PUT /apps/openregister/api/objects/procest/task/{id}`
-
-Same body as POST, with `id` in URL path.
-
-### `DELETE /apps/openregister/api/objects/procest/task/{id}`
-
-No body. Returns 200 on success.
-
-## Database Changes
-
-None — OpenRegister handles all storage. The `task` schema is already defined in the repair step.
-
-## Nextcloud Integration
-
-- **Components used**: `NcAppNavigation`, `NcAppNavigationItem`, `NcContent`, `NcAppContent`, `NcButton`, `NcTextField`, `NcSelect`, `NcLoadingIcon`, `NcEmptyContent`, `NcNoteCard` (from `@nextcloud/vue`)
-- **Icons**: Material Design Icons via `vue-material-design-icons` (ClipboardCheck, CalendarAlert, etc.)
-- **No backend controllers** — all frontend
-- **No events/hooks** — no server-side logic
-
-## File Structure
-
-```
-src/
- utils/
- taskLifecycle.js (NEW) — Status transition map, getAllowedTransitions(), validateTransition()
- taskHelpers.js (NEW) — isOverdue(), getOverdueText(), prioritySortWeight(), formatDueDate()
- views/
- tasks/
- TaskList.vue (NEW) — Global task list with filters/search/sort
- TaskDetail.vue (NEW) — Task create/edit/view form with lifecycle actions
- cases/
- CaseDetail.vue (MODIFIED) — Enhanced task section with progress, better sorting, links to task detail
- navigation/
- MainMenu.vue (MODIFIED) — Add "Tasks" navigation item
- App.vue (MODIFIED) — Add routes for #/tasks and #/tasks/:id
-```
-
-## Decisions
-
-### 1. Utility functions vs. Pinia store extension
-
-**Decision**: Separate utility files (`taskLifecycle.js`, `taskHelpers.js`) rather than extending the generic `useObjectStore`.
-
-**Rationale**: The object store is intentionally generic (works for any OpenRegister object type). Task-specific logic like lifecycle validation and overdue calculation belongs in utility functions that components import directly. This keeps the store clean and reusable.
-
-**Alternative considered**: A dedicated `useTaskStore` wrapping `useObjectStore` — rejected because it adds a layer of indirection for simple utility functions.
-
-### 2. Client-side filtering vs. API filtering
-
-**Decision**: Use OpenRegister API `_filters` parameter for all filtering and sorting.
-
-**Rationale**: Server-side filtering is more scalable and consistent with pagination. Client-side filtering would require fetching all tasks upfront, which doesn't scale.
-
-### 3. Status lifecycle enforcement location
-
-**Decision**: Enforce lifecycle transitions in the frontend (disable invalid action buttons) rather than relying on backend validation alone.
-
-**Rationale**: Better UX — users never see confusing error messages from the API. The lifecycle rules are simple and deterministic. The backend schema validation provides a safety net if the frontend is bypassed.
-
-### 4. Hash routing for tasks
-
-**Decision**: Add `#/tasks` and `#/tasks/:id` routes to the existing hash-based router in `App.vue`.
-
-**Rationale**: Consistent with existing pattern (`#/cases`, `#/cases/:id`). No need for vue-router — the app is simple enough for hash-based routing.
-
-## Security Considerations
-
-- All API calls include `requesttoken` (CSRF protection) via existing `_getHeaders()`
-- `OCS-APIREQUEST` header is set, ensuring Nextcloud middleware is active
-- No user-generated HTML is rendered (XSS safe)
-- OpenRegister handles RBAC — frontend doesn't need to implement access control
-
-## NL Design System
-
-- Uses standard Nextcloud Vue components which inherit NL Design System tokens when the `nldesign` app is active
-- Priority badges use CSS variables (no hardcoded colors)
-- Overdue indicators use semantic color variables (`--color-error`, `--color-warning`)
-- All interactive elements have visible focus states (via Nextcloud component defaults)
-
-## Trade-offs
-
-- **No Nextcloud notifications on task assignment**: Would require a backend controller endpoint. Deferred to a future change to keep this frontend-only.
-- **No user existence validation**: Assignee field accepts any string. Validating against Nextcloud users requires a backend API call. Accepted for MVP — users typically know their colleagues' UIDs.
-- **No real-time updates**: If another user modifies a task, the current user won't see it until they refresh or navigate. Acceptable for MVP.
+# Design: task-management
+
+## Architecture Overview
+
+This is a **frontend-only** change. Tasks are already modeled in OpenRegister (the `task` schema exists and the object type is registered in the Pinia store). The implementation adds Vue components for task management and helper utilities for lifecycle enforcement and date calculations.
+
+```
+┌──────────────────────────────────────────────────┐
+│ New / Modified Vue Components │
+│ │
+│ TaskList.vue — Global task list with filters│
+│ TaskDetail.vue — Task create/edit/view form │
+│ CaseDetail.vue — Enhanced tasks section │
+│ MainMenu.vue — "Tasks" nav item │
+│ App.vue — Route: #/tasks, #/tasks/:id │
+└──────────────┬───────────────────────────────────┘
+ │
+┌──────────────▼───────────────────────────────────┐
+│ Helpers (src/utils/) │
+│ │
+│ taskLifecycle.js — Status transitions, allowed │
+│ actions, validation │
+│ taskHelpers.js — Overdue calc, priority sort, │
+│ date formatting │
+└──────────────┬───────────────────────────────────┘
+ │
+┌──────────────▼───────────────────────────────────┐
+│ useObjectStore (existing — no changes) │
+│ fetchCollection / fetchObject / saveObject / │
+│ deleteObject — all CRUD via OpenRegister API │
+└──────────────────────────────────────────────────┘
+```
+
+## API Design
+
+No new backend API endpoints. All operations use the existing OpenRegister API:
+
+### `GET /apps/openregister/api/objects/procest/task`
+
+**Query Parameters:**
+- `_limit` — Page size (default 20)
+- `_offset` — Pagination offset
+- `_search` — Full-text search on title
+- `_order` — Sort order, e.g. `{"dueDate":"asc"}`
+- `_filters[status]` — Filter by status
+- `_filters[assignee]` — Filter by assignee UID
+- `_filters[case]` — Filter by parent case UUID
+- `_filters[priority]` — Filter by priority
+
+**Response:**
+```json
+{
+ "results": [
+ {
+ "id": "uuid",
+ "title": "Controleer bouwtekeningen",
+ "description": "",
+ "status": "available",
+ "assignee": "jan.devries",
+ "case": "case-uuid",
+ "dueDate": "2026-03-01T17:00:00Z",
+ "priority": "normal",
+ "completedDate": null
+ }
+ ],
+ "total": 23,
+ "page": 1,
+ "pages": 2
+}
+```
+
+### `POST /apps/openregister/api/objects/procest/task`
+
+**Request:**
+```json
+{
+ "title": "Controleer bouwtekeningen",
+ "case": "case-uuid",
+ "status": "available",
+ "priority": "normal",
+ "assignee": "jan.devries",
+ "dueDate": "2026-03-01T17:00:00Z"
+}
+```
+
+### `PUT /apps/openregister/api/objects/procest/task/{id}`
+
+Same body as POST, with `id` in URL path.
+
+### `DELETE /apps/openregister/api/objects/procest/task/{id}`
+
+No body. Returns 200 on success.
+
+## Database Changes
+
+None — OpenRegister handles all storage. The `task` schema is already defined in the repair step.
+
+## Nextcloud Integration
+
+- **Components used**: `NcAppNavigation`, `NcAppNavigationItem`, `NcContent`, `NcAppContent`, `NcButton`, `NcTextField`, `NcSelect`, `NcLoadingIcon`, `NcEmptyContent`, `NcNoteCard` (from `@nextcloud/vue`)
+- **Icons**: Material Design Icons via `vue-material-design-icons` (ClipboardCheck, CalendarAlert, etc.)
+- **No backend controllers** — all frontend
+- **No events/hooks** — no server-side logic
+
+## File Structure
+
+```
+src/
+ utils/
+ taskLifecycle.js (NEW) — Status transition map, getAllowedTransitions(), validateTransition()
+ taskHelpers.js (NEW) — isOverdue(), getOverdueText(), prioritySortWeight(), formatDueDate()
+ views/
+ tasks/
+ TaskList.vue (NEW) — Global task list with filters/search/sort
+ TaskDetail.vue (NEW) — Task create/edit/view form with lifecycle actions
+ cases/
+ CaseDetail.vue (MODIFIED) — Enhanced task section with progress, better sorting, links to task detail
+ navigation/
+ MainMenu.vue (MODIFIED) — Add "Tasks" navigation item
+ App.vue (MODIFIED) — Add routes for #/tasks and #/tasks/:id
+```
+
+## Decisions
+
+### 1. Utility functions vs. Pinia store extension
+
+**Decision**: Separate utility files (`taskLifecycle.js`, `taskHelpers.js`) rather than extending the generic `useObjectStore`.
+
+**Rationale**: The object store is intentionally generic (works for any OpenRegister object type). Task-specific logic like lifecycle validation and overdue calculation belongs in utility functions that components import directly. This keeps the store clean and reusable.
+
+**Alternative considered**: A dedicated `useTaskStore` wrapping `useObjectStore` — rejected because it adds a layer of indirection for simple utility functions.
+
+### 2. Client-side filtering vs. API filtering
+
+**Decision**: Use OpenRegister API `_filters` parameter for all filtering and sorting.
+
+**Rationale**: Server-side filtering is more scalable and consistent with pagination. Client-side filtering would require fetching all tasks upfront, which doesn't scale.
+
+### 3. Status lifecycle enforcement location
+
+**Decision**: Enforce lifecycle transitions in the frontend (disable invalid action buttons) rather than relying on backend validation alone.
+
+**Rationale**: Better UX — users never see confusing error messages from the API. The lifecycle rules are simple and deterministic. The backend schema validation provides a safety net if the frontend is bypassed.
+
+### 4. Hash routing for tasks
+
+**Decision**: Add `#/tasks` and `#/tasks/:id` routes to the existing hash-based router in `App.vue`.
+
+**Rationale**: Consistent with existing pattern (`#/cases`, `#/cases/:id`). No need for vue-router — the app is simple enough for hash-based routing.
+
+## Security Considerations
+
+- All API calls include `requesttoken` (CSRF protection) via existing `_getHeaders()`
+- `OCS-APIREQUEST` header is set, ensuring Nextcloud middleware is active
+- No user-generated HTML is rendered (XSS safe)
+- OpenRegister handles RBAC — frontend doesn't need to implement access control
+
+## NL Design System
+
+- Uses standard Nextcloud Vue components which inherit NL Design System tokens when the `nldesign` app is active
+- Priority badges use CSS variables (no hardcoded colors)
+- Overdue indicators use semantic color variables (`--color-error`, `--color-warning`)
+- All interactive elements have visible focus states (via Nextcloud component defaults)
+
+## Trade-offs
+
+- **No Nextcloud notifications on task assignment**: Would require a backend controller endpoint. Deferred to a future change to keep this frontend-only.
+- **No user existence validation**: Assignee field accepts any string. Validating against Nextcloud users requires a backend API call. Accepted for MVP — users typically know their colleagues' UIDs.
+- **No real-time updates**: If another user modifies a task, the current user won't see it until they refresh or navigate. Acceptable for MVP.
diff --git a/openspec/changes/archive/2026-02-26-task-management/proposal.md b/openspec/changes/archive/2026-02-26-task-management/proposal.md
index 0b1a0418..db108526 100644
--- a/openspec/changes/archive/2026-02-26-task-management/proposal.md
+++ b/openspec/changes/archive/2026-02-26-task-management/proposal.md
@@ -1,75 +1,75 @@
-# Proposal: task-management
-
-## Summary
-
-Implement the MVP task management feature for Procest, enabling users to create, assign, track, and complete tasks within cases. Tasks follow the CMMN 1.1 HumanTask lifecycle and are stored as OpenRegister objects. This is a core building block — cases without tasks have no way to distribute and track work.
-
-## Motivation
-
-The current Procest app has a basic stub for tasks in the case detail page (a simple table and inline form) but lacks proper task CRUD, status lifecycle enforcement, a dedicated task list view, filtering, sorting, overdue handling, and priority management. Without a functional task system, case handlers cannot break work into trackable units, assign tasks to colleagues, or monitor progress and deadlines.
-
-## Affected Projects
-
-- [x] Project: `procest` — New task views (list + detail), Pinia store actions for task-specific logic, enhanced case detail task section, navigation updates
-
-## Scope
-
-### In Scope
-
-- Task CRUD via OpenRegister API (create, read, update, delete)
-- CMMN PlanItem status lifecycle (available → active → completed/terminated, available → disabled)
-- Task assignment to Nextcloud users
-- Global task list view with search, filtering (status, assignee, priority), and sorting
-- Case-scoped task list in case detail with completion progress (e.g., "3/5")
-- Task due dates with overdue highlighting (red for overdue, amber for due today)
-- Priority levels (urgent, high, normal, low) with visual badges
-- Task detail/edit view
-- Navigation menu item for Tasks
-- Frontend validation (title required, case required)
-
-### Out of Scope
-
-- Kanban board view (V1 — REQ-TASK-007)
-- Task checklists/sub-items (V1 — REQ-TASK-009)
-- Task dependencies (V1 — REQ-TASK-010)
-- Task templates per case type (V1 — REQ-TASK-011)
-- Automated task creation on status change (Enterprise — REQ-TASK-012)
-- Nextcloud notifications on assignment (deferred — requires backend work)
-- User existence validation on assignment (deferred — requires backend endpoint)
-
-## Approach
-
-This is a **frontend-only** change. All data operations go through the existing OpenRegister API via the generic `useObjectStore`. The `task` object type is already registered in `store.js`. The implementation adds:
-
-1. A dedicated `TaskList.vue` view with filtering/sorting
-2. A `TaskDetail.vue` view for creating/editing tasks
-3. Enhanced task section in `CaseDetail.vue` with progress tracking
-4. Task-specific helper functions for status lifecycle validation, overdue calculation, and priority sorting
-5. Navigation menu entry for "Tasks"
-6. Updated hash-based routing in `App.vue`
-
-No new backend endpoints or database changes required — OpenRegister handles all persistence.
-
-## Capabilities
-
-### New Capabilities
-
-- None (task-management spec already exists)
-
-### Modified Capabilities
-
-- `task-management` — Implementing MVP requirements (REQ-TASK-001 through REQ-TASK-006, REQ-TASK-008, REQ-TASK-013) from the existing spec. No requirement changes, only implementation.
-
-## Cross-Project Dependencies
-
-- **OpenRegister**: Task objects stored in the `procest` register under the `task` schema. The schema must be registered (handled by `InitializeSettings` repair step). No OpenRegister changes needed.
-
-## Rollback Strategy
-
-- All changes are frontend-only Vue components and Pinia store helpers
-- Revert the git commits to restore previous stub behavior
-- No data migration needed — task objects created during use remain in OpenRegister regardless
-
-## Open Questions
-
-- None — the spec is comprehensive and the architecture is straightforward
+# Proposal: task-management
+
+## Summary
+
+Implement the MVP task management feature for Procest, enabling users to create, assign, track, and complete tasks within cases. Tasks follow the CMMN 1.1 HumanTask lifecycle and are stored as OpenRegister objects. This is a core building block — cases without tasks have no way to distribute and track work.
+
+## Motivation
+
+The current Procest app has a basic stub for tasks in the case detail page (a simple table and inline form) but lacks proper task CRUD, status lifecycle enforcement, a dedicated task list view, filtering, sorting, overdue handling, and priority management. Without a functional task system, case handlers cannot break work into trackable units, assign tasks to colleagues, or monitor progress and deadlines.
+
+## Affected Projects
+
+- [x] Project: `procest` — New task views (list + detail), Pinia store actions for task-specific logic, enhanced case detail task section, navigation updates
+
+## Scope
+
+### In Scope
+
+- Task CRUD via OpenRegister API (create, read, update, delete)
+- CMMN PlanItem status lifecycle (available → active → completed/terminated, available → disabled)
+- Task assignment to Nextcloud users
+- Global task list view with search, filtering (status, assignee, priority), and sorting
+- Case-scoped task list in case detail with completion progress (e.g., "3/5")
+- Task due dates with overdue highlighting (red for overdue, amber for due today)
+- Priority levels (urgent, high, normal, low) with visual badges
+- Task detail/edit view
+- Navigation menu item for Tasks
+- Frontend validation (title required, case required)
+
+### Out of Scope
+
+- Kanban board view (V1 — REQ-TASK-007)
+- Task checklists/sub-items (V1 — REQ-TASK-009)
+- Task dependencies (V1 — REQ-TASK-010)
+- Task templates per case type (V1 — REQ-TASK-011)
+- Automated task creation on status change (Enterprise — REQ-TASK-012)
+- Nextcloud notifications on assignment (deferred — requires backend work)
+- User existence validation on assignment (deferred — requires backend endpoint)
+
+## Approach
+
+This is a **frontend-only** change. All data operations go through the existing OpenRegister API via the generic `useObjectStore`. The `task` object type is already registered in `store.js`. The implementation adds:
+
+1. A dedicated `TaskList.vue` view with filtering/sorting
+2. A `TaskDetail.vue` view for creating/editing tasks
+3. Enhanced task section in `CaseDetail.vue` with progress tracking
+4. Task-specific helper functions for status lifecycle validation, overdue calculation, and priority sorting
+5. Navigation menu entry for "Tasks"
+6. Updated hash-based routing in `App.vue`
+
+No new backend endpoints or database changes required — OpenRegister handles all persistence.
+
+## Capabilities
+
+### New Capabilities
+
+- None (task-management spec already exists)
+
+### Modified Capabilities
+
+- `task-management` — Implementing MVP requirements (REQ-TASK-001 through REQ-TASK-006, REQ-TASK-008, REQ-TASK-013) from the existing spec. No requirement changes, only implementation.
+
+## Cross-Project Dependencies
+
+- **OpenRegister**: Task objects stored in the `procest` register under the `task` schema. The schema must be registered (handled by `InitializeSettings` repair step). No OpenRegister changes needed.
+
+## Rollback Strategy
+
+- All changes are frontend-only Vue components and Pinia store helpers
+- Revert the git commits to restore previous stub behavior
+- No data migration needed — task objects created during use remain in OpenRegister regardless
+
+## Open Questions
+
+- None — the spec is comprehensive and the architecture is straightforward
diff --git a/openspec/changes/archive/2026-02-26-task-management/review.md b/openspec/changes/archive/2026-02-26-task-management/review.md
index 674fe48e..e74e4792 100644
--- a/openspec/changes/archive/2026-02-26-task-management/review.md
+++ b/openspec/changes/archive/2026-02-26-task-management/review.md
@@ -1,51 +1,51 @@
-# Review: task-management
-
-## Summary
-- Tasks completed: 16/16 implementation + 0/6 verification (manual testing)
-- GitHub issues: N/A (no plan.json)
-- Spec compliance: **PASS**
-
-## Completeness
-
-All 16 implementation task checkboxes are checked in tasks.md. All implementation files exist and are correctly implemented:
-- `src/utils/taskLifecycle.js` — CMMN lifecycle (TASK_STATUSES, transitions, validation, labels)
-- `src/utils/taskHelpers.js` — Overdue, priority, date helpers (isOverdue, sortTasks, formatDueDate, etc.)
-- `src/views/tasks/TaskList.vue` — Global task list with search, filters, sort, pagination
-- `src/views/tasks/TaskDetail.vue` — Task create/edit/view with lifecycle actions
-- `src/views/cases/CaseDetail.vue` — Enhanced task section with progress "Tasks (X/Y)"
-- `src/navigation/MainMenu.vue` — Tasks nav item with ClipboardCheckOutline icon
-- `src/App.vue` — Routes for #/tasks, #/tasks/{id}, #/tasks/new/{caseId}
-
-## Findings
-
-### CRITICAL
-
-None.
-
-### WARNING
-
-None — all previously identified warnings (partial PUT in transitionTo, missing assignee filter, t() at module evaluation time, dead CSS) were fixed in earlier iterations.
-
-### SUGGESTION
-
-- `searchTimeout` and `assigneeTimeout` in TaskList.vue are at module scope — could share debounce timer across instances (low risk since only one TaskList exists).
-- Consider using `NcDialog` instead of `window.confirm()` for delete confirmation in TaskDetail.vue.
-- `_order` parameter is JSON.stringified — verify consistency with OpenRegister API expectations.
-
-## Requirement-by-Requirement Verification
-
-| Requirement | Status | Notes |
-|-------------|--------|-------|
-| MVP Task CRUD | PASS | Create, read, update, delete all implemented with validation |
-| CMMN Status Lifecycle | PASS | All transitions correct per CMMN 1.1 spec, invalid transitions blocked |
-| Global Task List | PASS | Table with 6 columns, search (300ms debounce), 3 filters, 5 sortable columns, pagination |
-| Case-Scoped Task List | PASS | Progress "Tasks (X/Y)", sortTasks(), clickable rows, "New task" navigation |
-| Overdue Highlighting | PASS | Red text + red left border for overdue, amber for due today, no indicator for completed |
-| Priority Visual Indicators | PASS | Urgent=red, high=orange, normal=none, low=grey — all with text labels (WCAG AA) |
-| Completed/terminated read-only | PASS | isTerminalStatus() disables form + hides transition actions |
-| Navigation + Routing | PASS | #/tasks, #/tasks/{id}, #/tasks/new/{caseId} all routed correctly |
-| Empty states + loading | PASS | NcEmptyContent with clipboard icon, NcLoadingIcon, save button loading state |
-
-## Recommendation
-
-**APPROVE** — 0 critical, 0 warnings. All spec requirements implemented correctly.
+# Review: task-management
+
+## Summary
+- Tasks completed: 16/16 implementation + 0/6 verification (manual testing)
+- GitHub issues: N/A (no plan.json)
+- Spec compliance: **PASS**
+
+## Completeness
+
+All 16 implementation task checkboxes are checked in tasks.md. All implementation files exist and are correctly implemented:
+- `src/utils/taskLifecycle.js` — CMMN lifecycle (TASK_STATUSES, transitions, validation, labels)
+- `src/utils/taskHelpers.js` — Overdue, priority, date helpers (isOverdue, sortTasks, formatDueDate, etc.)
+- `src/views/tasks/TaskList.vue` — Global task list with search, filters, sort, pagination
+- `src/views/tasks/TaskDetail.vue` — Task create/edit/view with lifecycle actions
+- `src/views/cases/CaseDetail.vue` — Enhanced task section with progress "Tasks (X/Y)"
+- `src/navigation/MainMenu.vue` — Tasks nav item with ClipboardCheckOutline icon
+- `src/App.vue` — Routes for #/tasks, #/tasks/{id}, #/tasks/new/{caseId}
+
+## Findings
+
+### CRITICAL
+
+None.
+
+### WARNING
+
+None — all previously identified warnings (partial PUT in transitionTo, missing assignee filter, t() at module evaluation time, dead CSS) were fixed in earlier iterations.
+
+### SUGGESTION
+
+- `searchTimeout` and `assigneeTimeout` in TaskList.vue are at module scope — could share debounce timer across instances (low risk since only one TaskList exists).
+- Consider using `NcDialog` instead of `window.confirm()` for delete confirmation in TaskDetail.vue.
+- `_order` parameter is JSON.stringified — verify consistency with OpenRegister API expectations.
+
+## Requirement-by-Requirement Verification
+
+| Requirement | Status | Notes |
+|-------------|--------|-------|
+| MVP Task CRUD | PASS | Create, read, update, delete all implemented with validation |
+| CMMN Status Lifecycle | PASS | All transitions correct per CMMN 1.1 spec, invalid transitions blocked |
+| Global Task List | PASS | Table with 6 columns, search (300ms debounce), 3 filters, 5 sortable columns, pagination |
+| Case-Scoped Task List | PASS | Progress "Tasks (X/Y)", sortTasks(), clickable rows, "New task" navigation |
+| Overdue Highlighting | PASS | Red text + red left border for overdue, amber for due today, no indicator for completed |
+| Priority Visual Indicators | PASS | Urgent=red, high=orange, normal=none, low=grey — all with text labels (WCAG AA) |
+| Completed/terminated read-only | PASS | isTerminalStatus() disables form + hides transition actions |
+| Navigation + Routing | PASS | #/tasks, #/tasks/{id}, #/tasks/new/{caseId} all routed correctly |
+| Empty states + loading | PASS | NcEmptyContent with clipboard icon, NcLoadingIcon, save button loading state |
+
+## Recommendation
+
+**APPROVE** — 0 critical, 0 warnings. All spec requirements implemented correctly.
diff --git a/openspec/changes/archive/2026-02-26-task-management/specs/task-management/spec.md b/openspec/changes/archive/2026-02-26-task-management/specs/task-management/spec.md
index 7e689492..47e98867 100644
--- a/openspec/changes/archive/2026-02-26-task-management/specs/task-management/spec.md
+++ b/openspec/changes/archive/2026-02-26-task-management/specs/task-management/spec.md
@@ -1,162 +1,162 @@
-# Task Management Delta Specification
-
-## Purpose
-
-Implements the MVP tier of the task management capability as defined in `procest/openspec/specs/task-management/spec.md`. This delta spec captures the subset of requirements being implemented in this change and any implementation-specific clarifications.
-
-## ADDED Requirements
-
-### Requirement: MVP Task CRUD Implementation
-
-The system MUST implement full CRUD operations for tasks using the existing `useObjectStore` Pinia store and OpenRegister API. The frontend MUST validate required fields before submission.
-
-#### Scenario: Create task with frontend validation
-
-- GIVEN the user is on the case detail page for case #2024-042
-- WHEN the user clicks "New task" and submits a form with title "Controleer bouwtekeningen"
-- THEN the system MUST call `saveObject('task', { title, case: caseId, status: 'available', priority: 'normal' })`
-- AND the new task MUST appear in the case's task list without a full page reload
-- AND the task form MUST reset after successful creation
-
-#### Scenario: Reject task creation without title
-
-- GIVEN the user is creating a new task
-- WHEN the user submits the form with an empty title
-- THEN the frontend MUST show an inline validation error "Title is required"
-- AND the form MUST NOT submit to the API
-
-#### Scenario: Edit task inline fields
-
-- GIVEN an existing task "Controleer bouwtekeningen" with status `available`
-- WHEN the user opens the task detail view and changes the description
-- THEN the system MUST call `saveObject('task', updatedData)` with the task's existing ID
-- AND the updated data MUST be reflected in both the task detail and any list views
-
-#### Scenario: Delete task with confirmation
-
-- GIVEN an existing task "Verouderde controle" in case #2024-042
-- WHEN the user clicks delete and confirms the dialog
-- THEN the system MUST call `deleteObject('task', taskId)`
-- AND the task MUST be removed from the case's task list
-
-### Requirement: CMMN Status Lifecycle Enforcement
-
-The frontend MUST enforce the CMMN PlanItem lifecycle transitions. Invalid transitions MUST be prevented in the UI before reaching the API.
-
-#### Scenario: Status transition buttons reflect allowed transitions
-
-- GIVEN a task with status `available`
-- WHEN the task detail view renders
-- THEN the available actions MUST be: "Start" (→ active), "Terminate" (→ terminated), "Disable" (→ disabled)
-- AND "Complete" MUST NOT be available (requires active status first)
-
-#### Scenario: Complete sets completedDate automatically
-
-- GIVEN a task with status `active`
-- WHEN the user clicks "Complete"
-- THEN the system MUST set `status: 'completed'` AND `completedDate` to the current ISO 8601 timestamp
-- AND both fields MUST be sent in a single `saveObject` call
-
-#### Scenario: Completed and terminated tasks are read-only
-
-- GIVEN a task with status `completed` or `terminated`
-- WHEN the task detail view renders
-- THEN all form fields MUST be disabled/read-only
-- AND no status transition buttons MUST be shown
-
-### Requirement: Global Task List View
-
-The system MUST provide a dedicated task list view accessible from the main navigation, with filtering, sorting, and search.
-
-#### Scenario: Navigate to global task list
-
-- GIVEN the user clicks "Tasks" in the navigation menu
-- WHEN the task list view loads
-- THEN the system MUST fetch all tasks via `fetchCollection('task', params)`
-- AND display them in a table with columns: Title, Case, Status, Assignee, Due Date, Priority
-
-#### Scenario: Filter by status
-
-- GIVEN 23 tasks across multiple cases
-- WHEN the user selects status filter "active"
-- THEN the system MUST re-fetch with `_filters[status]=active`
-- AND only active tasks MUST be shown
-
-#### Scenario: Filter by assignee
-
-- GIVEN tasks assigned to multiple users
-- WHEN the user selects assignee filter "jan.devries"
-- THEN the system MUST re-fetch with `_filters[assignee]=jan.devries`
-- AND only Jan's tasks MUST be shown
-
-#### Scenario: Sort by due date
-
-- GIVEN tasks with various due dates
-- WHEN the user clicks the "Due Date" column header
-- THEN tasks MUST be re-fetched with `_order[dueDate]=asc`
-- AND tasks without a due date MUST appear at the end
-
-#### Scenario: Search by title
-
-- GIVEN the user types "bouwtekeningen" in the search field
-- WHEN the search term is applied (debounced 300ms)
-- THEN the system MUST re-fetch with `_search=bouwtekeningen`
-
-### Requirement: Case-Scoped Task List with Progress
-
-The case detail page MUST show tasks for that case with completion progress tracking.
-
-#### Scenario: Task progress indicator
-
-- GIVEN case #2024-042 has 5 tasks, 2 completed
-- WHEN the case detail page renders the tasks section
-- THEN the header MUST show "Tasks (2/5)"
-- AND the progress fraction MUST update when a task is completed
-
-#### Scenario: Default sort order in case task list
-
-- GIVEN case tasks with mixed statuses and priorities
-- WHEN the task list renders
-- THEN tasks MUST be sorted: available/active first (by priority descending, then due date ascending), then completed, then terminated/disabled
-
-### Requirement: Overdue Highlighting
-
-Active/available tasks past their due date MUST be visually highlighted. Completed/terminated/disabled tasks MUST NOT show overdue indicators.
-
-#### Scenario: Overdue task in list view
-
-- GIVEN a task with dueDate "2026-02-20" and status `active`, and today is 2026-02-25
-- WHEN the task renders in any list view
-- THEN the due date cell MUST show "5 days overdue" in red text
-- AND the row MUST have a visual overdue indicator (red left border or background tint)
-
-#### Scenario: Due today indicator
-
-- GIVEN a task with dueDate set to today and status `active`
-- WHEN the task renders
-- THEN the due date cell MUST show "Due today" in amber/orange text
-
-#### Scenario: Completed task not marked overdue
-
-- GIVEN a task with dueDate in the past and status `completed`
-- WHEN the task renders
-- THEN no overdue indicator MUST be shown
-
-### Requirement: Priority Visual Indicators
-
-Tasks MUST display priority badges with consistent color coding across all views.
-
-#### Scenario: Priority badge rendering
-
-- GIVEN a task with priority `urgent`
-- WHEN the task renders in any list view
-- THEN the priority cell MUST show a red "Urgent" badge
-- AND `high` MUST show orange, `normal` no badge, `low` grey
-
-## MODIFIED Requirements
-
-_No existing requirements are being changed — this is an initial implementation of the existing spec._
-
-## REMOVED Requirements
-
-_None._
+# Task Management Delta Specification
+
+## Purpose
+
+Implements the MVP tier of the task management capability as defined in `procest/openspec/specs/task-management/spec.md`. This delta spec captures the subset of requirements being implemented in this change and any implementation-specific clarifications.
+
+## ADDED Requirements
+
+### Requirement: MVP Task CRUD Implementation
+
+The system MUST implement full CRUD operations for tasks using the existing `useObjectStore` Pinia store and OpenRegister API. The frontend MUST validate required fields before submission.
+
+#### Scenario: Create task with frontend validation
+
+- GIVEN the user is on the case detail page for case #2024-042
+- WHEN the user clicks "New task" and submits a form with title "Controleer bouwtekeningen"
+- THEN the system MUST call `saveObject('task', { title, case: caseId, status: 'available', priority: 'normal' })`
+- AND the new task MUST appear in the case's task list without a full page reload
+- AND the task form MUST reset after successful creation
+
+#### Scenario: Reject task creation without title
+
+- GIVEN the user is creating a new task
+- WHEN the user submits the form with an empty title
+- THEN the frontend MUST show an inline validation error "Title is required"
+- AND the form MUST NOT submit to the API
+
+#### Scenario: Edit task inline fields
+
+- GIVEN an existing task "Controleer bouwtekeningen" with status `available`
+- WHEN the user opens the task detail view and changes the description
+- THEN the system MUST call `saveObject('task', updatedData)` with the task's existing ID
+- AND the updated data MUST be reflected in both the task detail and any list views
+
+#### Scenario: Delete task with confirmation
+
+- GIVEN an existing task "Verouderde controle" in case #2024-042
+- WHEN the user clicks delete and confirms the dialog
+- THEN the system MUST call `deleteObject('task', taskId)`
+- AND the task MUST be removed from the case's task list
+
+### Requirement: CMMN Status Lifecycle Enforcement
+
+The frontend MUST enforce the CMMN PlanItem lifecycle transitions. Invalid transitions MUST be prevented in the UI before reaching the API.
+
+#### Scenario: Status transition buttons reflect allowed transitions
+
+- GIVEN a task with status `available`
+- WHEN the task detail view renders
+- THEN the available actions MUST be: "Start" (→ active), "Terminate" (→ terminated), "Disable" (→ disabled)
+- AND "Complete" MUST NOT be available (requires active status first)
+
+#### Scenario: Complete sets completedDate automatically
+
+- GIVEN a task with status `active`
+- WHEN the user clicks "Complete"
+- THEN the system MUST set `status: 'completed'` AND `completedDate` to the current ISO 8601 timestamp
+- AND both fields MUST be sent in a single `saveObject` call
+
+#### Scenario: Completed and terminated tasks are read-only
+
+- GIVEN a task with status `completed` or `terminated`
+- WHEN the task detail view renders
+- THEN all form fields MUST be disabled/read-only
+- AND no status transition buttons MUST be shown
+
+### Requirement: Global Task List View
+
+The system MUST provide a dedicated task list view accessible from the main navigation, with filtering, sorting, and search.
+
+#### Scenario: Navigate to global task list
+
+- GIVEN the user clicks "Tasks" in the navigation menu
+- WHEN the task list view loads
+- THEN the system MUST fetch all tasks via `fetchCollection('task', params)`
+- AND display them in a table with columns: Title, Case, Status, Assignee, Due Date, Priority
+
+#### Scenario: Filter by status
+
+- GIVEN 23 tasks across multiple cases
+- WHEN the user selects status filter "active"
+- THEN the system MUST re-fetch with `_filters[status]=active`
+- AND only active tasks MUST be shown
+
+#### Scenario: Filter by assignee
+
+- GIVEN tasks assigned to multiple users
+- WHEN the user selects assignee filter "jan.devries"
+- THEN the system MUST re-fetch with `_filters[assignee]=jan.devries`
+- AND only Jan's tasks MUST be shown
+
+#### Scenario: Sort by due date
+
+- GIVEN tasks with various due dates
+- WHEN the user clicks the "Due Date" column header
+- THEN tasks MUST be re-fetched with `_order[dueDate]=asc`
+- AND tasks without a due date MUST appear at the end
+
+#### Scenario: Search by title
+
+- GIVEN the user types "bouwtekeningen" in the search field
+- WHEN the search term is applied (debounced 300ms)
+- THEN the system MUST re-fetch with `_search=bouwtekeningen`
+
+### Requirement: Case-Scoped Task List with Progress
+
+The case detail page MUST show tasks for that case with completion progress tracking.
+
+#### Scenario: Task progress indicator
+
+- GIVEN case #2024-042 has 5 tasks, 2 completed
+- WHEN the case detail page renders the tasks section
+- THEN the header MUST show "Tasks (2/5)"
+- AND the progress fraction MUST update when a task is completed
+
+#### Scenario: Default sort order in case task list
+
+- GIVEN case tasks with mixed statuses and priorities
+- WHEN the task list renders
+- THEN tasks MUST be sorted: available/active first (by priority descending, then due date ascending), then completed, then terminated/disabled
+
+### Requirement: Overdue Highlighting
+
+Active/available tasks past their due date MUST be visually highlighted. Completed/terminated/disabled tasks MUST NOT show overdue indicators.
+
+#### Scenario: Overdue task in list view
+
+- GIVEN a task with dueDate "2026-02-20" and status `active`, and today is 2026-02-25
+- WHEN the task renders in any list view
+- THEN the due date cell MUST show "5 days overdue" in red text
+- AND the row MUST have a visual overdue indicator (red left border or background tint)
+
+#### Scenario: Due today indicator
+
+- GIVEN a task with dueDate set to today and status `active`
+- WHEN the task renders
+- THEN the due date cell MUST show "Due today" in amber/orange text
+
+#### Scenario: Completed task not marked overdue
+
+- GIVEN a task with dueDate in the past and status `completed`
+- WHEN the task renders
+- THEN no overdue indicator MUST be shown
+
+### Requirement: Priority Visual Indicators
+
+Tasks MUST display priority badges with consistent color coding across all views.
+
+#### Scenario: Priority badge rendering
+
+- GIVEN a task with priority `urgent`
+- WHEN the task renders in any list view
+- THEN the priority cell MUST show a red "Urgent" badge
+- AND `high` MUST show orange, `normal` no badge, `low` grey
+
+## MODIFIED Requirements
+
+_No existing requirements are being changed — this is an initial implementation of the existing spec._
+
+## REMOVED Requirements
+
+_None._
diff --git a/openspec/changes/archive/2026-02-26-task-management/tasks.md b/openspec/changes/archive/2026-02-26-task-management/tasks.md
index 1f344034..b7765fbc 100644
--- a/openspec/changes/archive/2026-02-26-task-management/tasks.md
+++ b/openspec/changes/archive/2026-02-26-task-management/tasks.md
@@ -1,166 +1,166 @@
-# Tasks: task-management
-
-## 1. Utility Functions
-
-- [x] 1.1 Create task lifecycle utility (`src/utils/taskLifecycle.js`)
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-002`
- - **files**: `src/utils/taskLifecycle.js`
- - **acceptance_criteria**:
- - GIVEN a task status WHEN calling `getAllowedTransitions(status)` THEN it returns only valid CMMN transitions (e.g., `available` → `[active, terminated, disabled]`)
- - GIVEN a transition from `available` to `completed` WHEN calling `validateTransition(from, to)` THEN it returns `false`
- - GIVEN a transition from `active` to `completed` WHEN calling `validateTransition(from, to)` THEN it returns `true`
- - Export `TASK_STATUSES` constant with all five statuses
- - Export `getStatusLabel(status)` returning human-readable Dutch/English labels
- - Export `isTerminalStatus(status)` returning `true` for completed/terminated/disabled
-
-- [x] 1.2 Create task helper utilities (`src/utils/taskHelpers.js`)
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-005`, `#REQ-TASK-013`
- - **files**: `src/utils/taskHelpers.js`
- - **acceptance_criteria**:
- - GIVEN a task with dueDate in the past and status `active` WHEN calling `isOverdue(task)` THEN it returns `true`
- - GIVEN a task with status `completed` and dueDate in the past WHEN calling `isOverdue(task)` THEN it returns `false`
- - GIVEN a task due today WHEN calling `isDueToday(task)` THEN it returns `true`
- - GIVEN a task 5 days overdue WHEN calling `getOverdueText(task)` THEN it returns "5 days overdue"
- - Export `formatDueDate(dateString)` returning formatted date (e.g., "Feb 26")
- - Export `prioritySortWeight(priority)` returning sort weight (urgent=1, high=2, normal=3, low=4)
- - Export `PRIORITY_LEVELS` constant with all four levels and their display config (label, color CSS variable)
- - Export `sortTasks(tasks)` that sorts by status group (active first), then priority, then due date
-
-## 2. Task List View
-
-- [x] 2.1 Create `TaskList.vue` component
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
- - **files**: `src/views/tasks/TaskList.vue`
- - **acceptance_criteria**:
- - GIVEN the user navigates to `#/tasks` WHEN the component mounts THEN it fetches tasks via `fetchCollection('task')` and displays a table
- - Table columns: Title (clickable → task detail), Case (clickable → case detail), Status, Assignee, Due Date, Priority
- - GIVEN 23 tasks WHEN the list renders THEN pagination controls appear (page size 20)
- - GIVEN an overdue task WHEN it renders in the table THEN the due date cell shows red overdue text
- - GIVEN a task with priority `urgent` WHEN it renders THEN a red "Urgent" badge is shown
- - Empty state with NcEmptyContent when no tasks found
-
-- [x] 2.2 Add search functionality to TaskList
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
- - **files**: `src/views/tasks/TaskList.vue`
- - **acceptance_criteria**:
- - GIVEN the search input WHEN the user types "bouwtekeningen" THEN after 300ms debounce the list re-fetches with `_search=bouwtekeningen`
- - GIVEN an active search WHEN the user clears the input THEN all tasks are shown again
-
-- [x] 2.3 Add filter controls to TaskList (status, assignee, priority)
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
- - **files**: `src/views/tasks/TaskList.vue`
- - **acceptance_criteria**:
- - GIVEN filter dropdowns for status, assignee, and priority WHEN the user selects status "active" THEN the list re-fetches with `_filters[status]=active`
- - GIVEN multiple active filters WHEN the user clears one THEN remaining filters stay applied
- - Status filter options: all, available, active, completed, terminated, disabled
-
-- [x] 2.4 Add column sorting to TaskList
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
- - **files**: `src/views/tasks/TaskList.vue`
- - **acceptance_criteria**:
- - GIVEN the user clicks the "Due Date" column header THEN tasks are re-fetched with `_order[dueDate]=asc`
- - GIVEN the user clicks the same column again THEN sort order toggles to `desc`
- - Sortable columns: title, status, assignee, dueDate, priority
-
-## 3. Task Detail View
-
-- [x] 3.1 Create `TaskDetail.vue` component (view/edit mode)
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
- - **files**: `src/views/tasks/TaskDetail.vue`
- - **acceptance_criteria**:
- - GIVEN a task UUID in the URL hash WHEN the component mounts THEN it fetches the task via `fetchObject('task', id)` and populates the form
- - Form fields: title (required), description (textarea), assignee (text input), dueDate (date input), priority (select), status (read-only display)
- - GIVEN a completed/terminated task WHEN the form renders THEN all fields are disabled
- - "Back to list" button navigates to `#/tasks`
- - Parent case link navigates to `#/cases/{caseId}`
-
-- [x] 3.2 Add status transition actions to TaskDetail
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-002`, `#REQ-TASK-008`
- - **files**: `src/views/tasks/TaskDetail.vue`, `src/utils/taskLifecycle.js`
- - **acceptance_criteria**:
- - GIVEN a task with status `available` WHEN the detail renders THEN action buttons show: "Start", "Terminate", "Disable"
- - GIVEN a task with status `active` WHEN the user clicks "Complete" THEN status is set to `completed` and `completedDate` to current ISO timestamp
- - GIVEN a task with status `completed` WHEN the detail renders THEN no action buttons are shown
- - Action buttons use `getAllowedTransitions()` from taskLifecycle.js
-
-- [x] 3.3 Add create mode to TaskDetail (new task)
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
- - **files**: `src/views/tasks/TaskDetail.vue`
- - **acceptance_criteria**:
- - GIVEN URL `#/tasks/new?case=caseUuid` WHEN the component mounts THEN it renders an empty form with the case pre-filled
- - GIVEN the user submits with title "Controleer bouwtekeningen" THEN `saveObject('task', { title, case, status: 'available', priority: 'normal' })` is called
- - GIVEN successful creation THEN the user is navigated to the new task's detail page
- - GIVEN empty title WHEN user clicks Save THEN inline validation error "Title is required" appears
-
-- [x] 3.4 Add delete functionality to TaskDetail
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
- - **files**: `src/views/tasks/TaskDetail.vue`
- - **acceptance_criteria**:
- - GIVEN an existing task that is not in a terminal status WHEN the user clicks "Delete" THEN a confirmation dialog appears
- - GIVEN the user confirms deletion THEN `deleteObject('task', id)` is called and user is navigated to task list
-
-## 4. Case Detail Enhancement
-
-- [x] 4.1 Enhance CaseDetail.vue task section with progress tracking
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-008`
- - **files**: `src/views/cases/CaseDetail.vue`
- - **acceptance_criteria**:
- - GIVEN case #2024-042 has 5 tasks, 2 completed WHEN the section header renders THEN it shows "Tasks (2/5)"
- - Task table shows: title (clickable → `#/tasks/{id}`), status badge, assignee, due date (with overdue formatting), priority badge
- - Default sort: available/active first (by priority desc, then due date asc), then completed, then terminated/disabled
- - "New task" button navigates to `#/tasks/new?case={caseId}` instead of inline form
-
-- [x] 4.2 Replace inline task form in CaseDetail with navigation to TaskDetail
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
- - **files**: `src/views/cases/CaseDetail.vue`
- - **acceptance_criteria**:
- - GIVEN the "New task" button in the case detail WHEN clicked THEN navigate to `#/tasks/new?case={caseId}`
- - Remove the inline `showNewTask` / `newTask` form data and `createTask` method
- - Task rows are clickable and navigate to `#/tasks/{taskId}`
-
-## 5. Navigation and Routing
-
-- [x] 5.1 Add "Tasks" item to MainMenu.vue
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
- - **files**: `src/navigation/MainMenu.vue`
- - **acceptance_criteria**:
- - GIVEN the navigation menu WHEN it renders THEN a "Tasks" item appears between "Cases" and the end of the list
- - The item uses `ClipboardCheckOutline` (or similar) icon from vue-material-design-icons
- - GIVEN the user is on `#/tasks` WHEN the menu renders THEN the Tasks item is highlighted as active
-
-- [x] 5.2 Add task routes to App.vue
- - **spec_ref**: design.md (routing section)
- - **files**: `src/App.vue`
- - **acceptance_criteria**:
- - GIVEN URL `#/tasks` WHEN the app renders THEN `TaskList` component is shown
- - GIVEN URL `#/tasks/new` WHEN the app renders THEN `TaskDetail` in create mode is shown
- - GIVEN URL `#/tasks/{uuid}` WHEN the app renders THEN `TaskDetail` with that task loaded is shown
- - Import `TaskList` and `TaskDetail` components
-
-## 6. Styling and Polish
-
-- [x] 6.1 Add priority badge and overdue indicator styles
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-005`, `#REQ-TASK-006`
- - **files**: `src/views/tasks/TaskList.vue`, `src/views/tasks/TaskDetail.vue`
- - **acceptance_criteria**:
- - Priority badges: urgent (red bg), high (orange bg), normal (no badge), low (grey bg)
- - All badge colors use CSS variables (`--color-error` for red, `--color-warning` for orange, `--color-text-maxcontrast` for grey)
- - Overdue text styled in `--color-error`, "Due today" in `--color-warning`
- - Priority and overdue indicators include text labels (not color-only — WCAG AA)
-
-- [x] 6.2 Add empty states and loading indicators
- - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
- - **files**: `src/views/tasks/TaskList.vue`, `src/views/tasks/TaskDetail.vue`
- - **acceptance_criteria**:
- - GIVEN no tasks exist WHEN the task list renders THEN NcEmptyContent with "No tasks found" and a clipboard icon is shown
- - GIVEN tasks are loading WHEN the list renders THEN NcLoadingIcon is shown
- - GIVEN a task is being saved WHEN the save button is clicked THEN the button shows a loading state
-
-## Verification
-
-- [x] All tasks checked off
-- [ ] Manual testing: create, edit, complete, delete tasks in a case
-- [ ] Manual testing: global task list with filters, search, sort
-- [ ] Manual testing: overdue and priority indicators display correctly
-- [ ] Manual testing: status lifecycle transitions are enforced (invalid buttons disabled)
-- [ ] Manual testing: case detail shows task progress "X/Y"
+# Tasks: task-management
+
+## 1. Utility Functions
+
+- [x] 1.1 Create task lifecycle utility (`src/utils/taskLifecycle.js`)
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-002`
+ - **files**: `src/utils/taskLifecycle.js`
+ - **acceptance_criteria**:
+ - GIVEN a task status WHEN calling `getAllowedTransitions(status)` THEN it returns only valid CMMN transitions (e.g., `available` → `[active, terminated, disabled]`)
+ - GIVEN a transition from `available` to `completed` WHEN calling `validateTransition(from, to)` THEN it returns `false`
+ - GIVEN a transition from `active` to `completed` WHEN calling `validateTransition(from, to)` THEN it returns `true`
+ - Export `TASK_STATUSES` constant with all five statuses
+ - Export `getStatusLabel(status)` returning human-readable Dutch/English labels
+ - Export `isTerminalStatus(status)` returning `true` for completed/terminated/disabled
+
+- [x] 1.2 Create task helper utilities (`src/utils/taskHelpers.js`)
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-005`, `#REQ-TASK-013`
+ - **files**: `src/utils/taskHelpers.js`
+ - **acceptance_criteria**:
+ - GIVEN a task with dueDate in the past and status `active` WHEN calling `isOverdue(task)` THEN it returns `true`
+ - GIVEN a task with status `completed` and dueDate in the past WHEN calling `isOverdue(task)` THEN it returns `false`
+ - GIVEN a task due today WHEN calling `isDueToday(task)` THEN it returns `true`
+ - GIVEN a task 5 days overdue WHEN calling `getOverdueText(task)` THEN it returns "5 days overdue"
+ - Export `formatDueDate(dateString)` returning formatted date (e.g., "Feb 26")
+ - Export `prioritySortWeight(priority)` returning sort weight (urgent=1, high=2, normal=3, low=4)
+ - Export `PRIORITY_LEVELS` constant with all four levels and their display config (label, color CSS variable)
+ - Export `sortTasks(tasks)` that sorts by status group (active first), then priority, then due date
+
+## 2. Task List View
+
+- [x] 2.1 Create `TaskList.vue` component
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
+ - **files**: `src/views/tasks/TaskList.vue`
+ - **acceptance_criteria**:
+ - GIVEN the user navigates to `#/tasks` WHEN the component mounts THEN it fetches tasks via `fetchCollection('task')` and displays a table
+ - Table columns: Title (clickable → task detail), Case (clickable → case detail), Status, Assignee, Due Date, Priority
+ - GIVEN 23 tasks WHEN the list renders THEN pagination controls appear (page size 20)
+ - GIVEN an overdue task WHEN it renders in the table THEN the due date cell shows red overdue text
+ - GIVEN a task with priority `urgent` WHEN it renders THEN a red "Urgent" badge is shown
+ - Empty state with NcEmptyContent when no tasks found
+
+- [x] 2.2 Add search functionality to TaskList
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
+ - **files**: `src/views/tasks/TaskList.vue`
+ - **acceptance_criteria**:
+ - GIVEN the search input WHEN the user types "bouwtekeningen" THEN after 300ms debounce the list re-fetches with `_search=bouwtekeningen`
+ - GIVEN an active search WHEN the user clears the input THEN all tasks are shown again
+
+- [x] 2.3 Add filter controls to TaskList (status, assignee, priority)
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
+ - **files**: `src/views/tasks/TaskList.vue`
+ - **acceptance_criteria**:
+ - GIVEN filter dropdowns for status, assignee, and priority WHEN the user selects status "active" THEN the list re-fetches with `_filters[status]=active`
+ - GIVEN multiple active filters WHEN the user clears one THEN remaining filters stay applied
+ - Status filter options: all, available, active, completed, terminated, disabled
+
+- [x] 2.4 Add column sorting to TaskList
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
+ - **files**: `src/views/tasks/TaskList.vue`
+ - **acceptance_criteria**:
+ - GIVEN the user clicks the "Due Date" column header THEN tasks are re-fetched with `_order[dueDate]=asc`
+ - GIVEN the user clicks the same column again THEN sort order toggles to `desc`
+ - Sortable columns: title, status, assignee, dueDate, priority
+
+## 3. Task Detail View
+
+- [x] 3.1 Create `TaskDetail.vue` component (view/edit mode)
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
+ - **files**: `src/views/tasks/TaskDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN a task UUID in the URL hash WHEN the component mounts THEN it fetches the task via `fetchObject('task', id)` and populates the form
+ - Form fields: title (required), description (textarea), assignee (text input), dueDate (date input), priority (select), status (read-only display)
+ - GIVEN a completed/terminated task WHEN the form renders THEN all fields are disabled
+ - "Back to list" button navigates to `#/tasks`
+ - Parent case link navigates to `#/cases/{caseId}`
+
+- [x] 3.2 Add status transition actions to TaskDetail
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-002`, `#REQ-TASK-008`
+ - **files**: `src/views/tasks/TaskDetail.vue`, `src/utils/taskLifecycle.js`
+ - **acceptance_criteria**:
+ - GIVEN a task with status `available` WHEN the detail renders THEN action buttons show: "Start", "Terminate", "Disable"
+ - GIVEN a task with status `active` WHEN the user clicks "Complete" THEN status is set to `completed` and `completedDate` to current ISO timestamp
+ - GIVEN a task with status `completed` WHEN the detail renders THEN no action buttons are shown
+ - Action buttons use `getAllowedTransitions()` from taskLifecycle.js
+
+- [x] 3.3 Add create mode to TaskDetail (new task)
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
+ - **files**: `src/views/tasks/TaskDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN URL `#/tasks/new?case=caseUuid` WHEN the component mounts THEN it renders an empty form with the case pre-filled
+ - GIVEN the user submits with title "Controleer bouwtekeningen" THEN `saveObject('task', { title, case, status: 'available', priority: 'normal' })` is called
+ - GIVEN successful creation THEN the user is navigated to the new task's detail page
+ - GIVEN empty title WHEN user clicks Save THEN inline validation error "Title is required" appears
+
+- [x] 3.4 Add delete functionality to TaskDetail
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
+ - **files**: `src/views/tasks/TaskDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN an existing task that is not in a terminal status WHEN the user clicks "Delete" THEN a confirmation dialog appears
+ - GIVEN the user confirms deletion THEN `deleteObject('task', id)` is called and user is navigated to task list
+
+## 4. Case Detail Enhancement
+
+- [x] 4.1 Enhance CaseDetail.vue task section with progress tracking
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-008`
+ - **files**: `src/views/cases/CaseDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN case #2024-042 has 5 tasks, 2 completed WHEN the section header renders THEN it shows "Tasks (2/5)"
+ - Task table shows: title (clickable → `#/tasks/{id}`), status badge, assignee, due date (with overdue formatting), priority badge
+ - Default sort: available/active first (by priority desc, then due date asc), then completed, then terminated/disabled
+ - "New task" button navigates to `#/tasks/new?case={caseId}` instead of inline form
+
+- [x] 4.2 Replace inline task form in CaseDetail with navigation to TaskDetail
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-001`
+ - **files**: `src/views/cases/CaseDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN the "New task" button in the case detail WHEN clicked THEN navigate to `#/tasks/new?case={caseId}`
+ - Remove the inline `showNewTask` / `newTask` form data and `createTask` method
+ - Task rows are clickable and navigate to `#/tasks/{taskId}`
+
+## 5. Navigation and Routing
+
+- [x] 5.1 Add "Tasks" item to MainMenu.vue
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
+ - **files**: `src/navigation/MainMenu.vue`
+ - **acceptance_criteria**:
+ - GIVEN the navigation menu WHEN it renders THEN a "Tasks" item appears between "Cases" and the end of the list
+ - The item uses `ClipboardCheckOutline` (or similar) icon from vue-material-design-icons
+ - GIVEN the user is on `#/tasks` WHEN the menu renders THEN the Tasks item is highlighted as active
+
+- [x] 5.2 Add task routes to App.vue
+ - **spec_ref**: design.md (routing section)
+ - **files**: `src/App.vue`
+ - **acceptance_criteria**:
+ - GIVEN URL `#/tasks` WHEN the app renders THEN `TaskList` component is shown
+ - GIVEN URL `#/tasks/new` WHEN the app renders THEN `TaskDetail` in create mode is shown
+ - GIVEN URL `#/tasks/{uuid}` WHEN the app renders THEN `TaskDetail` with that task loaded is shown
+ - Import `TaskList` and `TaskDetail` components
+
+## 6. Styling and Polish
+
+- [x] 6.1 Add priority badge and overdue indicator styles
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-005`, `#REQ-TASK-006`
+ - **files**: `src/views/tasks/TaskList.vue`, `src/views/tasks/TaskDetail.vue`
+ - **acceptance_criteria**:
+ - Priority badges: urgent (red bg), high (orange bg), normal (no badge), low (grey bg)
+ - All badge colors use CSS variables (`--color-error` for red, `--color-warning` for orange, `--color-text-maxcontrast` for grey)
+ - Overdue text styled in `--color-error`, "Due today" in `--color-warning`
+ - Priority and overdue indicators include text labels (not color-only — WCAG AA)
+
+- [x] 6.2 Add empty states and loading indicators
+ - **spec_ref**: `procest/openspec/specs/task-management/spec.md#REQ-TASK-004`
+ - **files**: `src/views/tasks/TaskList.vue`, `src/views/tasks/TaskDetail.vue`
+ - **acceptance_criteria**:
+ - GIVEN no tasks exist WHEN the task list renders THEN NcEmptyContent with "No tasks found" and a clipboard icon is shown
+ - GIVEN tasks are loading WHEN the list renders THEN NcLoadingIcon is shown
+ - GIVEN a task is being saved WHEN the save button is clicked THEN the button shows a loading state
+
+## Verification
+
+- [x] All tasks checked off
+- [ ] Manual testing: create, edit, complete, delete tasks in a case
+- [ ] Manual testing: global task list with filters, search, sort
+- [ ] Manual testing: overdue and priority indicators display correctly
+- [ ] Manual testing: status lifecycle transitions are enforced (invalid buttons disabled)
+- [ ] Manual testing: case detail shows task progress "X/Y"
diff --git a/openspec/changes/archive/2026-03-03-complete-l10n/.openspec.yaml b/openspec/changes/archive/2026-03-03-complete-l10n/.openspec.yaml
new file mode 100644
index 00000000..ed9e94a6
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-complete-l10n/.openspec.yaml
@@ -0,0 +1,3 @@
+schema: conduction
+created: 2026-03-03
+status: archived
diff --git a/openspec/changes/archive/2026-03-03-complete-l10n/design.md b/openspec/changes/archive/2026-03-03-complete-l10n/design.md
new file mode 100644
index 00000000..aa81768e
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-complete-l10n/design.md
@@ -0,0 +1,96 @@
+# Design: Complete l10n
+
+## Context
+
+Procest has `l10n/en.json` and `l10n/nl.json` with 55 keys each. The codebase uses ~175 unique keys via `t('procest', '...')` across 25+ files (views, utils, services). ~120 keys are missing. Missing keys cause fallback to the key string (often English), so Dutch users see mixed Dutch/English.
+
+**User-reported**: With Nextcloud set to Nederlands, Procest displays almost entirely in English. This is caused by the incomplete l10n list (fallback to key), not by locale-loading. Exploration report: `exploration.md`.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Add all missing keys to en.json and nl.json
+- Ensure valid JSON and correct placeholder syntax
+- No code changes — l10n files only
+
+**Non-Goals:**
+- New languages
+- Changing existing translations
+- Refactoring t() calls
+
+## File Map
+
+### Modified Files
+
+| File | Changes |
+|------|---------|
+| `l10n/en.json` | Add ~120 missing keys with English text |
+| `l10n/nl.json` | Add ~120 missing keys with Dutch translations |
+
+### Unchanged Files
+
+| File | Reason |
+|------|--------|
+| All `src/` files | No code changes (loadTranslations fix reverted — caused empty text) |
+| `l10n/*.pot` (if any) | Not used for this change |
+
+## Design Decisions
+
+### DD-01: Source of Truth for Keys
+
+**Decision**: Extract keys from code via grep/search of `t('procest', '...')` patterns.
+
+**Rationale**: The code is the source of truth for which strings need translation. Adding keys not used in code would create orphan entries.
+
+### DD-02: Placeholder Syntax
+
+**Decision**: Preserve `{name}` placeholders exactly in both en and nl. Only translate the surrounding text.
+
+**Rationale**: Nextcloud's t() uses these for interpolation. Changing `{days}` to `{dagen}` would break the substitution. Example: `"{days} days overdue"` → nl: `"{days} dagen te laat"`.
+
+### DD-03: Translation Approach
+
+**Decision**: Add keys in batches by area (navigation, dashboard, cases, tasks, case types, validation, utilities). Use the exploration analysis as the reference list.
+
+**Rationale**: Organized batches reduce errors and make review easier. The exploration already produced a categorized list.
+
+## Key Categories (from exploration.md)
+
+1. **Navigation & layout** — Track and manage tasks, Documentation, Case Types, Configuration, Register and schema settings
+2. **Dashboard** — New Case, New Task, Open Cases, Overdue, Completed This Month, My Tasks, Cases by Status, KPI strings (+{n} today, avg {days} days, etc.), welcome messages
+3. **Overdue panel** — Overdue Cases, No overdue cases, View all overdue
+4. **Cases** — Case Information, Identifier, Handler, Result, Participants, Add Participant, Assign Handler, Reassign, etc.
+5. **Case detail & extension** — Closed on {date}, Extend Deadline, Status changed from/to, Updated: {fields}, delete confirmations
+6. **Case types (admin)** — Draft, Published, Publish, Unpublish, Configure case types, Statuses, delete/confirm dialogs
+7. **Status types tab** — Save the case type first..., Drag to reorder, Final status, Notify initiator, Add Status Type, validation errors
+8. **Tasks** — Due today, Completed on {date}, Case: {id}, Title is required, Create task, etc.
+9. **My Work** — Show completed, All caught up!, Due this week, Upcoming, No deadline, Completed, All, CASE, TASK
+10. **Result & activity** — Result, Type: {type}, Deadline & Timing, Extension: allowed (+{period}), Activity, Add note
+11. **Validation** — {field} is required, four ISO duration variants (P56D, P42D, P28D, generic), Valid from/until
+12. **Duration & relative time** — 1 year, {n} years, 1 day, {n} days, just now, yesterday, {days} days ago, Due tomorrow, {days} days remaining
+13. **Task lifecycle** — Available, Active, Terminated, Disabled, Start, Complete, Terminate, Disable
+14. **Priority (capitalized)** — Urgent, High, Normal, Low
+15. **Confidentiality & origin** — Internal, External, Public, Restricted, Case sensitive, Confidential, etc.
+16. **Case type fields** — Purpose, Trigger, Subject, Processing deadline, Origin, Responsible unit, etc.
+17. **User settings** — Procest settings, No settings available yet
+18. **Add participant** — Role type, Select role type..., Participant, Select user..., Failed to add participant
+
+## Risks / Trade-offs
+
+- **[Risk] Translation quality** → Dutch translations may need native review. Mitigation: Use consistent terminology (e.g., "zaak" for case, "taak" for task) matching existing nl.json.
+- **[Trade-off] Large file** → en.json and nl.json will grow from 55 to ~175 entries. Acceptable; standard for localized apps.
+
+## Exploration Notes
+
+- **OverduePanel**: Uses `Overdue Cases`, `No overdue cases`, `View all overdue` — added to categories.
+- **ISO duration**: Four distinct keys (generic + P56D, P42D, P28D). Add all.
+- **Priority**: Both lowercase (en.json) and capitalized (taskHelpers) keys exist; add capitalized Urgent, High, Normal, Low.
+- **QuickStatusDropdown**: `Change status` (no ellipsis) vs `Change status...` elsewhere — different keys.
+
+## Post-Apply: Rebuild Required
+
+After updating l10n files, run `npm run build` in the procest app directory. Nextcloud Vue apps compile assets at build time; l10n changes do not take effect until the app is rebuilt. Users may also need to hard refresh (Ctrl+Shift+R) or clear browser cache.
+
+## Open Questions
+
+None — exploration complete; `exploration.md` is the reference list.
diff --git a/openspec/changes/archive/2026-03-03-complete-l10n/exploration.md b/openspec/changes/archive/2026-03-03-complete-l10n/exploration.md
new file mode 100644
index 00000000..4125fd58
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-complete-l10n/exploration.md
@@ -0,0 +1,378 @@
+# Exploration: Complete l10n
+
+**Date**: 2026-03-03
+**Scope**: Extract all `t('procest', '...')` keys from codebase, compare with l10n files, produce definitive missing-key list.
+
+---
+
+## User-Reported Symptom & Diagnosis
+
+**Symptom**: Nextcloud language is set to Nederlands, but the Procest app displays almost entirely in English.
+
+**Cause**: Incomplete l10n list — not a locale-loading bug. Nextcloud correctly loads `l10n/nl.json` when the user's language is Dutch. However, when `t('procest', 'key')` is called and the key is missing from `nl.json`, Nextcloud falls back to the key string itself (which is typically the English source text). With only ~55 keys in `nl.json` and ~120 keys used in code, most UI strings fall back to English.
+
+**Fix**: Add all missing keys to `nl.json` with Dutch translations. No code changes required.
+
+### If Dutch still doesn't show after applying l10n
+
+**Cause 1**: Nextcloud Vue apps require a rebuild after l10n changes; browser/Nextcloud may cache old assets.
+
+**Cause 2 (investigated, reverted)**: Tried importing `t`, `n`, `loadTranslations` from `@nextcloud/l10n` and calling `loadTranslations('procest', callback)` before mount. This caused **all text to display empty** — reverted. The root cause for Dutch not showing remains unclear; may require Nextcloud/server-side investigation (locale injection, app template).
+
+**Steps**:
+1. Run `npm run build` in the procest app directory
+3. Hard refresh the browser (Ctrl+Shift+R) or clear cache
+4. Optionally: disable and re-enable the app (`occ app:disable procest` then `occ app:enable procest`) to clear Nextcloud app cache
+
+---
+
+## Summary
+
+| Metric | Value |
+|--------|-------|
+| Keys in en.json | 55 |
+| Unique keys used in code | ~175 |
+| Missing keys | ~120 |
+| Files with t() calls | 25+ (views, utils, services) |
+
+---
+
+## Key Sources
+
+### Views (Vue components)
+- `MainMenu.vue` — navigation
+- `Dashboard.vue`, `KpiCards.vue`, `StatusChart.vue`, `MyWorkPreview.vue`, `ActivityFeed.vue`, `OverduePanel.vue`
+- `CaseList.vue`, `CaseDetail.vue`, `CaseCreateDialog.vue`
+- `TaskList.vue`, `TaskDetail.vue`, `TaskCreateDialog.vue`
+- `MyWork.vue`
+- `CaseTypeList.vue`, `CaseTypeDetail.vue`, `StatusesTab.vue`
+- `Settings.vue`, `UserSettings.vue`
+- Case components: `ResultSection`, `AddParticipantDialog`, `DeadlinePanel`, `ActivityTimeline`, `ParticipantsSection`, `QuickStatusDropdown`
+
+### Utils
+- `dashboardHelpers.js` — formatRelativeTime, getMyWorkItems
+- `caseHelpers.js` — getCaseOverdueText, formatDeadlineCountdown
+- `caseValidation.js` — validateCaseCreate, validateCaseUpdate
+- `caseTypeValidation.js` — validateCaseType, validateForPublish, getFieldLabel
+- `durationHelpers.js` — formatDuration, getDurationError
+- `taskHelpers.js` — PRIORITY_LABELS, getOverdueText
+- `taskLifecycle.js` — STATUS_LABELS, ACTION_LABELS
+
+### Services
+- `taskApi.js` — formatTaskForDisplay
+
+---
+
+## Missing Keys (Categorized)
+
+### 1. Navigation & layout
+- `Track and manage tasks`
+- `Documentation`
+- `Case Types`
+- `Configuration`
+- `Register and schema settings`
+
+### 2. Dashboard
+- `New Case`
+- `New Task`
+- `Refresh dashboard`
+- `Open Cases`
+- `Overdue`
+- `Completed This Month`
+- `My Tasks`
+- `Cases by Status`
+- `No open cases`
+- `My Work`
+- `No items assigned to you`
+- `View all my work`
+- `View all activity`
+- `Recent Activity`
+- `No recent activity`
+- `Retry`
+- `0 today`
+- `+{n} today`
+- `action needed`
+- `all on track`
+- `no data`
+- `{n} due today`
+- `none due today`
+- `avg {days} days`
+- `Failed to load dashboard data`
+- `Welcome to Procest! Get started by creating your first case type in Settings.`
+- `Welcome to Procest! Get started by creating your first case or task using the buttons above.`
+
+### 3. Overdue panel
+- `Overdue Cases`
+- `No overdue cases`
+- `View all overdue`
+
+### 4. Cases
+- `Manage cases and workflows`
+- `Case`
+- `Case Information`
+- `Case type`
+- `Identifier`
+- `Handler`
+- `Assign handler...`
+- `Start date`
+- `Result`
+- `Result (required)`
+- `Change status...`
+- `Select result type...`
+- `Confirm`
+- `Participants`
+- `No participants assigned`
+- `Assign Handler`
+- `Reassign`
+- `Reassign handler to:`
+- `Unknown`
+- `Remove this participant?`
+- `New Case`
+- `New Task`
+- `Create case`
+- `Create task`
+- `Select a case type...`
+- `Enter case title...`
+- `Not set`
+- `Initial status`
+- `Calculated deadline`
+- `Case created with type '{type}'`
+
+### 5. Case detail & extension
+- `Closed on {date}`
+- `This will extend the deadline by {period}.`
+- `Extend Deadline`
+- `Reason`
+- `Why is an extension needed?`
+- `Extend deadline`
+- `Please select a result type`
+- `Result is required when closing a case`
+- `Status changed from '{from}' to '{to}'`
+- `Status changed to '{status}'`
+- `Updated: {fields}`
+- `Are you sure you want to delete this case?`
+- `This case has {count} linked tasks. Are you sure you want to delete it?`
+- `Deadline extended from {old} to {new}. Reason: {reason}`
+- `No reason provided`
+
+### 6. Case types (admin)
+- `Configure case types`
+- `Draft`
+- `Published`
+- `Set as default`
+- `Delete`
+- `Only published case types can be set as default`
+- `Cannot delete: active cases are using this type`
+- `Failed to delete case type`
+- `{from} — (no end)`
+- `This will delete the case type and all {count} status types. Continue?`
+- `Delete case type "{title}"?`
+- `Failed to delete status type "{name}"`
+- `New Case Type`
+- `Case Type`
+- `Publish`
+- `Unpublish`
+- `Cannot publish:`
+- `Saved successfully`
+- `General`
+- `Statuses`
+- `Please fix the validation errors`
+- `Failed to save case type`
+- `Unpublishing this case type will prevent new cases from being created. Existing cases will continue to function. Continue?`
+
+### 7. Status types tab
+- `Save the case type first before adding status types.`
+- `Drag to reorder`
+- `Final`
+- `Notify`
+- `Name`
+- `Order`
+- `Final status`
+- `Notify initiator`
+- `Notification text`
+- `No status types defined. Add at least one to publish this case type.`
+- `Add Status Type`
+- `Name *`
+- `Order *`
+- `Add`
+- `Status type name is required`
+- `Order is required`
+- `A status type with this order already exists`
+- `Failed to add status type`
+- `Failed to save`
+- `At least one status type must be marked as final`
+- `Delete status type "{name}"?`
+- `Failed to delete status type`
+
+### 8. Tasks
+- `Due today`
+- `Back to list`
+- `New task`
+- `Task`
+- `Completed on {date}`
+- `Case: {id}`
+- `Username`
+- `Title is required`
+- `Are you sure you want to delete this task?`
+- `New Task`
+- `Enter task title...`
+- `Optional description...`
+- `Select priority`
+- `Select due date`
+- `Username (optional)`
+- `Link to a case (optional)`
+- `Create task`
+- `Change status` (no ellipsis — QuickStatusDropdown)
+
+### 9. My Work
+- `Show completed`
+- `Cases and tasks assigned to you will appear here`
+- `All caught up!`
+- `All your items are completed`
+- `Due this week`
+- `Upcoming`
+- `No deadline`
+- `Completed`
+- `All`
+- `Cases`
+- `Tasks`
+- `CASE`
+- `TASK`
+
+### 10. Result & activity
+- `Result`
+- `No result recorded yet`
+- `Type: {type}`
+- `Deadline & Timing`
+- `Started`
+- `Deadline`
+- `Processing time`
+- `Days elapsed`
+- `Extension: allowed (+{period})`
+- `Extension: already extended`
+- `Extension: not allowed`
+- `Request Extension`
+- `Activity`
+- `Add a note...`
+- `Add note`
+- `No activity yet`
+
+### 11. Validation & utilities
+- `{field} is required`
+- `Title is required`
+- `Case type is required`
+- `Case type '{name}' is a draft and cannot be used to create cases`
+- `Case type is not yet valid (valid from {date})`
+- `Case type has expired (valid until {date})`
+- `Must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks, P2M for 2 months)`
+- `Must be a valid ISO 8601 duration (e.g., P56D)`
+- `Must be a valid ISO 8601 duration (e.g., P42D)`
+- `Must be a valid ISO 8601 duration (e.g., P28D)`
+- `Extension period is required when extension is allowed`
+- `'Valid until' must be after 'Valid from'`
+- `Missing required fields: {fields}`
+- `'Valid from' date must be set`
+- `At least one status type must be defined`
+- `At least one status type must be marked as final`
+
+### 12. Duration & relative time
+- `1 year`
+- `{n} years`
+- `1 month`
+- `{n} months`
+- `1 week`
+- `{n} weeks`
+- `1 day`
+- `{n} days`
+- `1 day overdue`
+- `{days} days overdue`
+- `Due today`
+- `Due tomorrow`
+- `{days} days remaining`
+- `{days} days`
+- `just now`
+- `{min} min ago`
+- `{hours} hours ago`
+- `yesterday`
+- `{days} days ago`
+
+### 13. Task lifecycle
+- `Available`
+- `Active`
+- `Completed`
+- `Terminated`
+- `Disabled`
+- `Start`
+- `Complete`
+- `Terminate`
+- `Disable`
+
+### 14. Priority (capitalized)
+- `Urgent`
+- `High`
+- `Normal`
+- `Low`
+
+### 15. Confidentiality & origin
+- `Internal`
+- `External`
+- `Public`
+- `Restricted`
+- `Case sensitive`
+- `Confidential`
+- `Highly confidential`
+- `Secret`
+- `Top secret`
+
+### 16. Case type fields (getFieldLabel)
+- `Purpose`
+- `Trigger`
+- `Subject`
+- `Processing deadline`
+- `Origin`
+- `Confidentiality`
+- `Responsible unit`
+- `Extension period`
+- `Service target`
+- `Valid until`
+
+### 17. User settings
+- `Procest settings`
+- `General`
+- `No settings available yet`
+- `User settings will appear here in a future update.`
+
+### 18. Add participant
+- `Add Participant`
+- `Role type`
+- `Select role type...`
+- `Participant`
+- `Select user...`
+- `Failed to add participant`
+
+---
+
+## Placeholder Rules
+
+- **Preserve exactly**: `{field}`, `{count}`, `{days}`, `{n}`, `{name}`, `{title}`, `{from}`, `{to}`, `{period}`, `{date}`, `{user}`, `{type}`, `{old}`, `{new}`, `{reason}`, `{fields}`, `{min}`, `{hours}`
+- **Escaped quotes**: Keys like `Status changed from '{from}' to '{to}'` keep the quotes in the key; the placeholders are interpolated.
+
+---
+
+## Notes
+
+1. **Duplicate keys**: Some keys appear in both en.json and code with different casing (e.g. "New case" vs "New Case"). The code uses exact keys — use the key as it appears in the t() call.
+2. **ISO duration**: Four distinct keys exist (generic + P56D, P42D, P28D variants). Add all four.
+3. **Priority**: en.json has lowercase `low`, `normal`, `high`, `urgent`; taskHelpers uses `Urgent`, `High`, `Normal`, `Low`. Both sets are needed.
+4. **OverduePanel**: Uses `Overdue Cases`, `No overdue cases`, `View all overdue` — not in original design.
+5. **QuickStatusDropdown**: Uses `Change status` (no ellipsis) vs `Change status...` elsewhere.
+
+---
+
+## Verification
+
+After adding keys:
+1. `en.json` and `nl.json` must have identical key sets
+2. No trailing commas in JSON
+3. Placeholders unchanged in both en and nl
+4. Spot-check: Dutch locale shows Dutch strings for dashboard, case detail, case type admin, task forms
diff --git a/openspec/changes/archive/2026-03-03-complete-l10n/proposal.md b/openspec/changes/archive/2026-03-03-complete-l10n/proposal.md
new file mode 100644
index 00000000..27169631
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-complete-l10n/proposal.md
@@ -0,0 +1,55 @@
+# Proposal: Complete l10n
+
+## Summary
+
+Add all missing translation keys to the Procest app's l10n files (`l10n/en.json` and `l10n/nl.json`). Currently 55 keys exist; ~120 keys used in the codebase are missing (exploration: 25+ files across views, utils, services). Dutch users see untranslated English strings for many UI elements. This change completes the localization so the app displays correctly in both English and Dutch.
+
+## Problem
+
+The Procest app uses `t('procest', '...')` for user-facing strings throughout the codebase. When a key is missing from the active locale's JSON file, Nextcloud falls back to the key itself (often English). For Dutch users, this means many labels, messages, placeholders, and error texts appear in English instead of Dutch.
+
+**User-reported symptom**: When Nextcloud language is set to Nederlands, the Procest app still displays almost entirely in English. This is caused by the incomplete l10n list — not by a locale-loading bug. With ~55 keys in `nl.json` and ~120 keys used in code, most strings fall back to the (English) key.
+
+The admin-settings spec requires: "All labels, error messages, validation messages, and placeholder text MUST support English and Dutch localization."
+
+## Scope — MVP
+
+**In scope:**
+- Add all missing translation keys to `l10n/en.json` (English as source)
+- Add Dutch translations for all keys to `l10n/nl.json`
+- Preserve placeholder syntax (`{field}`, `{count}`, `{days}`, etc.) in both files
+- Cover: navigation, dashboard, cases, tasks, case types, validation, utilities, user settings
+
+**Out of scope:**
+- Additional languages (beyond en/nl)
+- Changing existing translations (only adding missing ones)
+- Extracting new strings from code (only add keys already used via t())
+
+## Approach
+
+1. Use exploration report (`exploration.md`) as the definitive list of missing keys — already extracted from 25+ files (views, utils, services)
+2. Add missing keys to `en.json` in batches by category (see design Key Categories)
+3. Add corresponding Dutch translations to `nl.json`, preserving placeholder syntax
+4. Verify JSON syntax and that en.json and nl.json have identical key sets
+5. **Rebuild**: Run `npm run build` — Nextcloud Vue apps must be rebuilt after l10n changes for translations to take effect
+
+## Capabilities
+
+### New Capabilities
+
+- **localization**: Complete English and Dutch translations for all user-facing strings in the Procest app. Ensures compliance with admin-settings REQ (localization) and improves UX for Dutch users.
+
+## Impact
+
+- **Files**: `l10n/en.json`, `l10n/nl.json`
+- **Backend**: None
+- **Dependencies**: Nextcloud l10n system, existing t() calls in code
+
+## Dependencies
+
+- admin-settings spec (localization requirement)
+- Existing codebase (no code changes — only l10n file updates)
+
+## Post-Completion Note
+
+Manual Dutch verification failed — Dutch texts are not displayed when Nextcloud is set to Nederlands. L10n files are complete; a follow-up change will address Dutch display (locale/translation loading). This change is ready to archive.
diff --git a/openspec/changes/archive/2026-03-03-complete-l10n/tasks.md b/openspec/changes/archive/2026-03-03-complete-l10n/tasks.md
new file mode 100644
index 00000000..3c64ac83
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-complete-l10n/tasks.md
@@ -0,0 +1,57 @@
+# Tasks: Complete l10n
+
+**Reference**: Use `exploration.md` as the definitive list of ~120 missing keys. Keys must match exact form used in code.
+
+## 1. Add Missing Keys to en.json (by category)
+
+- [x] 1.1 Navigation & layout (Track and manage tasks, Documentation, Case Types, Configuration, Register and schema settings)
+- [x] 1.2 Dashboard (New Case, New Task, Open Cases, Overdue, KPI strings, welcome messages, Retry, Failed to load dashboard data)
+- [x] 1.3 Overdue panel (Overdue Cases, No overdue cases, View all overdue)
+- [x] 1.4 Cases (Manage cases and workflows, Case Information, Identifier, Handler, Participants, Add Participant, Assign Handler, Reassign, etc.)
+- [x] 1.5 Case detail & extension (Closed on {date}, Extend Deadline, Status changed from/to, Updated: {fields}, delete confirmations)
+- [x] 1.6 Case types admin (Draft, Published, Publish, Unpublish, Configure case types, Statuses, delete/confirm dialogs)
+- [x] 1.7 Status types tab (Save the case type first..., Drag to reorder, Final status, Notify initiator, Add Status Type, validation errors)
+- [x] 1.8 Tasks (Due today, Completed on {date}, Case: {id}, Title is required, Create task, Change status, etc.)
+- [x] 1.9 My Work (Show completed, All caught up!, Due this week, Upcoming, No deadline, Completed, All, CASE, TASK)
+- [x] 1.10 Result & activity (Result, Type: {type}, Deadline & Timing, Extension strings, Activity, Add note)
+- [x] 1.11 Validation (four ISO duration variants, {field} is required, Valid from/until, case type validation messages)
+- [x] 1.12 Duration & relative time (1 year, {n} years, 1 day, {n} days, just now, yesterday, {days} days ago, Due tomorrow, {days} days remaining)
+- [x] 1.13 Task lifecycle (Available, Active, Terminated, Disabled, Start, Complete, Terminate, Disable)
+- [x] 1.14 Priority capitalized (Urgent, High, Normal, Low)
+- [x] 1.15 Confidentiality & case type fields (Internal, External, Purpose, Trigger, Subject, etc.)
+- [x] 1.16 User settings & Add participant (Procest settings, No settings available yet, Role type, Select role type..., etc.)
+
+## 2. Add Dutch Translations to nl.json
+
+- [x] 2.1 Add nl translations for all keys added in step 1 — match existing nl.json style (zaak, taak, etc.)
+- [x] 2.2 Preserve placeholder syntax ({field}, {count}, {days}, {n}, {name}, {title}, {from}, {to}, {period}, {date}, {user}, {type}, {old}, {new}, {reason}, {fields}, {min}, {hours}) in Dutch strings
+
+## 3. Translation Loading Fix (reverted — caused all text to show empty)
+
+- [x] 3.0 ~~Import `t`, `n`, `loadTranslations` from `@nextcloud/l10n`~~ — REVERTED: loadTranslations approach caused all text to display empty; restored original global t/n
+
+## 4. Rebuild (required for l10n to take effect)
+
+- [x] 4.1 Run `npm run build` in the procest app directory — Nextcloud Vue apps must be rebuilt after l10n changes (on Windows, if `NODE_ENV=production` fails, use `npx webpack --config webpack.config.js --progress` or run from WSL)
+- [x] 4.2 Hard refresh browser (Ctrl+Shift+R) or clear cache after rebuild
+- [x] 4.3 Optionally: `occ app:disable procest` then `occ app:enable procest` to clear Nextcloud app cache
+
+## 5. Verify
+
+- [x] 5.1 Validate JSON syntax (no trailing commas, valid escaping)
+- [x] 5.2 Verify key count: en.json and nl.json have identical keys (302 total)
+- [x] 5.3 Spot-check: switch Nextcloud to Dutch — **FAILED**: Dutch texts are not shown (app displays English regardless of locale)
+
+## Verification Tasks
+
+- [x] V-auto Automated: `node openspec/verify-l10n.js` — JSON valid, key sync, placeholders, code coverage
+- [x] V01 Manual test: Set Nextcloud language to Dutch, verify dashboard labels in Dutch — **FAILED**: Dutch not displayed
+- [x] V02 Manual test: Verify case detail, case type admin, and task forms show Dutch labels — **FAILED**: Dutch not displayed
+- [x] V03 Manual test: Verify error messages and validation text appear in Dutch — **FAILED**: Dutch not displayed
+- [x] V04 Manual test: Verify Overdue panel, My Work, and relative time strings in Dutch — **FAILED**: Dutch not displayed
+
+---
+
+## Archive Note
+
+**Status**: Archived. L10n keys added (302 total); automated verification passed. Manual Dutch verification failed — Dutch texts are not displayed in the app. A **follow-up change** will be created to fix Dutch display (locale/translation loading on the server or app side). This change completes the l10n file updates only.
diff --git a/openspec/changes/archive/2026-03-03-complete-l10n/verify-report.md b/openspec/changes/archive/2026-03-03-complete-l10n/verify-report.md
new file mode 100644
index 00000000..ef099971
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-complete-l10n/verify-report.md
@@ -0,0 +1,59 @@
+# Verification Report: Complete l10n
+
+**Date**: 2026-03-03
+**Change**: complete-l10n (archived)
+**Status**: PARTIAL — automated passed, manual Dutch verification failed
+
+---
+
+## Automated Checks
+
+| Check | Result |
+|-------|--------|
+| en.json valid JSON | OK |
+| nl.json valid JSON | OK |
+| en.json and nl.json identical keys | OK (302 total) |
+| Placeholders preserved in nl.json | OK (36 keys with placeholders) |
+| All keys used in code exist in l10n | OK (275 keys) |
+
+---
+
+## Fixes Applied During Verify
+
+18 keys were found in code but missing from l10n. Added to both en.json and nl.json:
+
+- `{days} days`
+- `No tasks yet`
+- `Case Type Management`
+- `Manage case types and their configurations`
+- `Case type schema`
+- `Status type schema`
+- `Initiator action`
+- `Handler action`
+- `e.g., P56D (56 days)`
+- `e.g., P42D (42 days)`
+- `e.g., P28D (28 days)`
+- `Extension allowed`
+- `Publication required`
+- `Publication text`
+- `Reference process`
+- `Keywords`
+- `Comma-separated keywords`
+- `Valid from`
+
+---
+
+## Manual Verification (Failed)
+
+Dutch texts are not displayed in the app when Nextcloud language is set to Nederlands. All manual verification tasks failed. A follow-up change will be created to fix Dutch display.
+
+- [x] V01: Set Nextcloud language to Dutch, verify dashboard labels in Dutch — **FAILED**
+- [x] V02: Verify case detail, case type admin, and task forms show Dutch labels — **FAILED**
+- [x] V03: Verify error messages and validation text appear in Dutch — **FAILED**
+- [x] V04: Verify Overdue panel, My Work, and relative time strings in Dutch — **FAILED**
+
+---
+
+## Verification Script
+
+Run `node openspec/verify-l10n.js` from the procest app root to re-run automated checks.
diff --git a/openspec/changes/archive/2026-03-03-fix-dutch-display/.openspec.yaml b/openspec/changes/archive/2026-03-03-fix-dutch-display/.openspec.yaml
new file mode 100644
index 00000000..ed9e94a6
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-dutch-display/.openspec.yaml
@@ -0,0 +1,3 @@
+schema: conduction
+created: 2026-03-03
+status: archived
diff --git a/openspec/changes/archive/2026-03-03-fix-dutch-display/design.md b/openspec/changes/archive/2026-03-03-fix-dutch-display/design.md
new file mode 100644
index 00000000..f6bf282d
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-dutch-display/design.md
@@ -0,0 +1,125 @@
+# Design: Fix Displaying Dutch Language
+
+## Context
+
+The Procest app uses `Vue.mixin({ methods: { t, n } })` so `t` and `n` are available in all components. These are global functions — not imported from `@nextcloud/l10n`. The `@nextcloud/l10n` library uses `window._oc_l10n_registry_translations[appId]` for translations and `document.documentElement.dataset.locale` for locale.
+
+**Current flow**: `t('procest', key)` → `getAppTranslations('procest')` → `window._oc_l10n_registry_translations['procest']` → lookup or fallback to key.
+
+**Problem**: Dutch translations exist in `l10n/nl.json` (302 keys) but are not displayed. Either (1) registry is empty for app; (2) registry has wrong locale; (3) locale is always 'en'.
+
+**Failed attempt**: `loadTranslations('procest', callback)` before mount caused all text empty — suggests registration timing or wrong bundle format.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Dutch texts display when Nextcloud language is Nederlands
+- No regression: English still works when locale is en
+- Minimal changes to app code
+
+**Non-Goals:**
+- Changing l10n file content (complete-l10n already done)
+- New languages
+
+## File Map
+
+### Files to Investigate / Modify
+
+| File | Role |
+|------|------|
+| `src/main.js` | Entry point; may need to load/register translations before Vue mount |
+| `src/settings.js` | Admin settings entry; same consideration |
+| `templates/index.php` | If exists: app template; Nextcloud may inject l10n here |
+| `appinfo/info.xml` | App metadata; no l10n config expected |
+
+### Unchanged Files
+
+| File | Reason |
+|------|--------|
+| `l10n/en.json`, `l10n/nl.json` | Complete; no changes |
+| All Vue components | Use t() correctly; no changes |
+
+## Root Cause Hypotheses
+
+### H1: Server does not inject app translations
+
+Nextcloud may inject core translations but not app-specific ones. For app pages (TemplateResponse), the app may need to load its own l10n via `loadTranslations` or the template must include a script that registers them.
+
+**Check**: Inspect the rendered HTML for the Procest page; look for `window._oc_l10n_registry_translations` or `OC.L10n` scripts. Compare with a working Nextcloud app (e.g. Files, Calendar).
+
+### H2: App template missing or does not load l10n
+
+Procest uses `TemplateResponse(Application::APP_ID, 'index')`. If template does not exist, Nextcloud may use a default that does not load app l10n. If template exists, it may need to explicitly load `l10n/{locale}.json`.
+
+**Check**: Does `templates/index.php` exist? If not, Nextcloud falls back to a default — what does it include?
+
+### H3: loadTranslations approach — wrong usage or timing
+
+The previous attempt used `loadTranslations('procest', callback)` before mount. It caused empty text. Possible causes:
+- Callback ran before Vue mount but `t` was already bound to a stale/empty registry
+- `register()` was called with wrong format (e.g. `bundle.translations` vs `bundle`)
+- `t` from global scope vs `t` from @nextcloud/l10n — different implementations?
+
+**Check**: `loadTranslations` fetches `l10n/{locale}.json` and calls `register(appName, result.translations)`. The nl.json format is `{ "translations": { "key": "value" } }`. So `result.translations` is correct. Verify `getLocale()` returns 'nl' when Nextcloud is Dutch. If locale is 'en', `loadTranslations` resolves immediately without fetching — that's correct.
+
+**Fix attempt**: Import `t`, `n` from `@nextcloud/l10n` and use them in the mixin (not globals). Ensure `loadTranslations` completes and registers before any component renders. The empty-text bug may have been caused by using global `t` before registration — the global might come from a different source (e.g. OC.L10n) that expects different registration.
+
+### H4: Locale not set on document
+
+`getLocale()` returns `document.documentElement.dataset.locale || 'en'`. If Nextcloud does not set `data-locale` on the HTML element for app pages, locale would always be 'en'.
+
+**Check**: In browser DevTools, when Nextcloud is Dutch: `document.documentElement.dataset.locale` and `document.documentElement.lang`.
+
+## Design Decisions
+
+### DD-01: Prefer app-side fix over server-side
+
+**Decision**: First try fixing in the app (main.js, settings.js) by loading and registering translations before mount. Only if that fails, investigate server-side (app template, Nextcloud core).
+
+**Rationale**: App-side changes are simpler to ship and don't require Nextcloud core changes. Many Nextcloud apps use `loadTranslations` successfully.
+
+### DD-02: Import t, n from @nextcloud/l10n
+
+**Decision**: Use `import { t, n, loadTranslations } from '@nextcloud/l10n'` and pass these to `Vue.mixin`. Do not rely on global `t`/`n`.
+
+**Rationale**: The global `t`/`n` may come from Nextcloud core or another source. Using the package ensures we use the same implementation that `loadTranslations` registers with. This may fix the empty-text bug (mismatch between global and @nextcloud/l10n registry).
+
+### DD-03: Async load before mount
+
+**Decision**: Call `loadTranslations('procest', () => { /* mount Vue */ })` and only mount Vue in the callback after translations are loaded.
+
+**Rationale**: Ensures translations are in `window._oc_l10n_registry_translations['procest']` before any component calls `t()`. For locale 'en', `loadTranslations` resolves immediately (no fetch). For 'nl', it fetches and registers.
+
+## Implementation Sketch
+
+```javascript
+// main.js
+import Vue from 'vue'
+import { PiniaVuePlugin } from 'pinia'
+import { translate as t, translatePlural as n, loadTranslations } from '@nextcloud/l10n'
+import pinia from './pinia.js'
+import router from './router/index.js'
+import App from './App.vue'
+import '@conduction/nextcloud-vue/css/index.css'
+
+Vue.mixin({ methods: { t, n } })
+
+loadTranslations('procest', () => {
+ new Vue({
+ pinia,
+ router,
+ render: h => h(App),
+ }).$mount('#content')
+})
+```
+
+## Risks / Trade-offs
+
+- **[Risk] loadTranslations empty-text bug recurs** → If the fix was using global vs imported t, this should work. If not, need to debug: log `getLocale()`, `hasAppTranslations('procest')`, and registry contents before mount.
+- **[Trade-off] Slight delay before mount** → For non-en locales, one extra network request. Acceptable; translations are small.
+
+## Open Questions
+
+1. Does Procest have a `templates/index.php`? If not, what template does Nextcloud use?
+2. What does `document.documentElement.dataset.locale` show when Nextcloud is Dutch?
+3. If loadTranslations + import t/n still fails, what does a working Nextcloud app (e.g. deck, files) do?
diff --git a/openspec/changes/archive/2026-03-03-fix-dutch-display/exploration.md b/openspec/changes/archive/2026-03-03-fix-dutch-display/exploration.md
new file mode 100644
index 00000000..1c18d860
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-dutch-display/exploration.md
@@ -0,0 +1,119 @@
+# Exploration: Fix Displaying Dutch Language
+
+**Date**: 2026-03-03
+**Scope**: Identify why Dutch translations are not displayed when Nextcloud language is Nederlands; fix translation loading.
+
+---
+
+## Context from complete-l10n (archived)
+
+- **L10n files**: `l10n/en.json` and `l10n/nl.json` have 302 keys each; Dutch translations are complete
+- **Symptom**: User sets Nextcloud to Nederlands; Procest app still shows English
+- **Failed fix**: `loadTranslations('procest', callback)` before mount caused **all text to display empty** — reverted
+- **Current**: App uses `Vue.mixin({ methods: { t, n } })` with global `t` and `n`
+
+---
+
+## Technical Background
+
+### @nextcloud/l10n
+
+- **getLocale()**: `document.documentElement.dataset.locale || 'en'`
+- **Translations**: `window._oc_l10n_registry_translations[appId]`
+- **loadTranslations(appName, callback)**: Fetches `l10n/{locale}.json`, parses, calls `register(appName, result.translations)`. For locale 'en', resolves immediately without fetch.
+- **register(appName, bundle)**: Sets `window._oc_l10n_registry_translations[appId] = bundle`
+
+### Procest app flow
+
+1. DashboardController returns `TemplateResponse(Application::APP_ID, 'index')`
+2. Nextcloud renders `templates/index.php` (exists): `Util::addScript($appId, $appId . '-main')` + ``
+3. Page loads `procest-main.js` (webpack bundle)
+4. main.js: `Vue.mixin({ methods: { t, n } })` then `new Vue(...).$mount('#content')`
+5. Components call `this.t('procest', key)` → `t('procest', key)` (from mixin)
+
+**Template finding**: `templates/index.php` does not explicitly load l10n. It only adds the main script. Nextcloud core may inject app l10n automatically when rendering the page — or it may not for app pages. 1.2/1.3 will tell us.
+
+### Where do global t, n come from?
+
+The app does not import `t` or `n`. They are used as globals. Nextcloud core typically injects these via the page. For app pages, the core may inject `OC.L10n` or similar. The `@nextcloud/l10n` package exports `t`, `n` that use `window._oc_l10n_registry_translations`. If the core injects a different `t` that uses a different registry, there could be a mismatch.
+
+---
+
+## Root Cause Hypotheses
+
+| ID | Hypothesis | How to verify |
+|----|------------|---------------|
+| H1 | Server does not inject app translations for Procest | Inspect rendered HTML; check for l10n script tags |
+| H2 | App template missing or does not load l10n | Check if `templates/index.php` exists; check default template |
+| H3 | Global t vs @nextcloud/l10n t mismatch | Import t from @nextcloud/l10n; use loadTranslations before mount |
+| H4 | document.documentElement.dataset.locale not set | DevTools: inspect `document.documentElement.dataset` when locale is Dutch |
+| H5 | loadTranslations callback timing — Vue used stale t | Ensure t from @nextcloud/l10n; mount only after callback |
+
+---
+
+## Investigation Checklist
+
+- [x] Check if `templates/index.php` exists in Procest — **YES** (adds procest-main.js, div#content)
+- [x] In browser (Nextcloud set to Dutch): `document.documentElement.dataset.locale` — **returns "nl"** ✓
+- [x] In browser: `window._oc_l10n_registry_translations` — **procest is undefined** (server does not inject app l10n)
+- [ ] Compare with working apps — optional; root cause confirmed
+- [ ] Try: import t,n from @nextcloud/l10n + loadTranslations before mount; verify no empty text
+
+---
+
+## Investigation Results (2026-03-03)
+
+| Check | Result | Meaning |
+|-------|--------|---------|
+| 1.2 Locale | `"nl"` | Nextcloud correctly sets locale; user language is Dutch |
+| 1.3 Registry | `procest` undefined | **Root cause**: Nextcloud server does NOT inject Procest's translations into `window._oc_l10n_registry_translations` |
+
+**Conclusion**: The server does not load Procest's l10n for app pages. The app must load its own translations. Use `loadTranslations('procest', callback)` before mount, and import `t`, `n` from `@nextcloud/l10n` (not globals). Proceed to implement fix (section 2).
+
+**Why previous loadTranslations caused empty text**: The app used global `t`/`n` (from Nextcloud core). Those may use a different registry or expect server-injected data. By importing `t`/`n` from `@nextcloud/l10n` and passing them to the mixin, we use the same implementation that `loadTranslations` registers with. Mount only after the callback so the registry is populated before any component renders.
+
+---
+
+## How to Inspect Locale and Registry (1.2, 1.3)
+
+**1.2 Locale**: Open Procest with Nextcloud set to Nederlands. In DevTools Console:
+```javascript
+document.documentElement.dataset.locale // expect "nl" or "nl_NL"
+document.documentElement.lang // expect "nl"
+```
+
+**1.3 Registry**: In DevTools Console:
+```javascript
+window._oc_l10n_registry_translations
+```
+Expand the object. Check if `procest` exists and has Dutch strings. If missing/empty, server is not injecting app l10n.
+
+---
+
+## Reference Apps (1.4)
+
+| App | URL | Pattern |
+|-----|-----|---------|
+| Deck | https://github.com/nextcloud/deck/blob/main/src/main.js | `import { translate, translatePlural } from '@nextcloud/l10n'`; `Vue.prototype.t = translate`; no loadTranslations |
+| Files | nextcloud/server/apps/files | Check src for l10n usage |
+
+---
+
+## Empty Text Bug — Root Cause (2026-03-03)
+
+**Symptom**: After loadTranslations fix, `window._oc_l10n_registry_translations.procest` has Dutch translations, but frontend shows no words.
+
+**Root cause**: `@nextcloud/l10n` exports `translate` and `translatePlural`, NOT `t` and `n`. The import `{ t, n, loadTranslations }` was resolving `t` and `n` to `undefined`. The Vue mixin received `methods: { t: undefined, n: undefined }`, so `this.t()` in components failed (undefined is not a function).
+
+**Fix**: Use the correct import alias:
+```javascript
+import { translate as t, translatePlural as n, loadTranslations } from '@nextcloud/l10n'
+```
+
+---
+
+## References
+
+- complete-l10n exploration: `archive/2026-03-03-complete-l10n/exploration.md`
+- @nextcloud/l10n: https://nextcloud-libraries.github.io/nextcloud-l10n/
+- Nextcloud translations: https://docs.nextcloud.com/server/latest/developer_manual/basics/translations.html
diff --git a/openspec/changes/archive/2026-03-03-fix-dutch-display/proposal.md b/openspec/changes/archive/2026-03-03-fix-dutch-display/proposal.md
new file mode 100644
index 00000000..f86f3cd8
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-dutch-display/proposal.md
@@ -0,0 +1,54 @@
+# Proposal: Fix Displaying Dutch Language
+
+## Summary
+
+Fix the Procest app so that Dutch translations are displayed when the user's Nextcloud language is set to Nederlands. The l10n files (`l10n/en.json`, `l10n/nl.json`) are complete with 302 keys (complete-l10n change, archived). However, the app displays English regardless of locale. This change addresses the translation loading/locale injection so Dutch texts appear correctly.
+
+## Problem
+
+**User-reported symptom**: When Nextcloud language is set to Nederlands, the Procest app still displays almost entirely in English.
+
+**Context**: The complete-l10n change added all missing keys to both l10n files. Automated verification passed (JSON valid, keys in sync, placeholders preserved). Manual Dutch verification failed — Dutch texts are not shown.
+
+**Known facts**:
+- `l10n/nl.json` contains 302 keys with correct Dutch translations
+- The app uses `t('procest', '...')` throughout; `t` and `n` are provided via `Vue.mixin({ methods: { t, n } })` (globals)
+- A previous attempt to use `loadTranslations('procest', callback)` from `@nextcloud/l10n` before mount caused **all text to display empty** — reverted
+- Root cause is unclear: may be server-side (locale injection, app template) or app-side (translation registration timing)
+
+## Scope — MVP
+
+**In scope:**
+- Investigate and fix why Dutch translations are not displayed
+- Ensure `t('procest', key)` returns Dutch when user locale is Nederlands
+- Preserve existing l10n files (no changes to en.json/nl.json content)
+
+**Out of scope:**
+- Adding new translation keys (already done in complete-l10n)
+- Additional languages beyond en/nl
+- Changing translation strings
+
+## Approach
+
+1. **Investigate** how Nextcloud injects locale and translations for app pages (TemplateResponse, app template, `document.documentElement.dataset.locale`, `window._oc_l10n_registry_translations`)
+2. **Identify** whether the app template loads app l10n, or if the app must load it itself
+3. **Fix** translation loading: either (a) ensure server injects app translations; (b) use `@nextcloud/l10n` correctly (debug why loadTranslations caused empty text); or (c) register translations before Vue mount with correct timing
+4. **Verify** manually: set Nextcloud to Nederlands, confirm dashboard and key views show Dutch
+
+## Capabilities
+
+### Modified Capabilities
+
+- **localization**: Dutch translations will actually display when user language is Nederlands. Completes the localization story started in complete-l10n.
+
+## Impact
+
+- **Frontend**: Likely `src/main.js`, `src/settings.js` (translation loading); possibly app template if server-side
+- **Backend**: Possibly app template (templates/index.php) if Nextcloud requires explicit l10n injection
+- **Dependencies**: @nextcloud/l10n, Nextcloud core translation system
+
+## Dependencies
+
+- complete-l10n (archived) — l10n files are complete; this change fixes display
+- admin-settings spec (localization requirement)
+- Nextcloud core (locale, app template)
diff --git a/openspec/changes/archive/2026-03-03-fix-dutch-display/tasks.md b/openspec/changes/archive/2026-03-03-fix-dutch-display/tasks.md
new file mode 100644
index 00000000..107cd997
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-dutch-display/tasks.md
@@ -0,0 +1,31 @@
+# Tasks: Fix Displaying Dutch Language
+
+## 1. Investigate
+
+- [x] 1.1 Check if `templates/index.php` exists — **YES**, exists at `templates/index.php` (adds procest-main.js, div#content)
+- [x] 1.2 In browser (Nextcloud set to Nederlands): inspect locale — **returns "nl"** ✓
+- [x] 1.3 In browser: inspect translation registry — **procest is undefined** (server does not inject app l10n)
+- [x] 1.4 Compare with working Nextcloud apps — optional; root cause confirmed
+
+**Root cause**: Server does not inject Procest's l10n. App must load its own via `loadTranslations`. Proceed to section 2.
+
+## 2. Implement Fix
+
+- [x] 2.1 Import `translate as t`, `translatePlural as n`, `loadTranslations` from `@nextcloud/l10n` in `src/main.js`
+- [x] 2.2 Wrap Vue mount in `loadTranslations('procest', () => { ... })` callback
+- [x] 2.3 Pass imported `t`, `n` to `Vue.mixin({ methods: { t, n } })` (not globals)
+- [x] 2.4 Apply same pattern to `src/settings.js` if it uses t/n
+- [x] 2.5 If loadTranslations causes empty text: **FIXED** — import was wrong: use `translate as t, translatePlural as n` (package exports `translate`/`translatePlural`, not `t`/`n`)
+
+## 3. Verify
+
+- [x] 3.1 Set Nextcloud language to Nederlands; open Procest app
+- [x] 3.2 Verify dashboard labels (Dashboard, New Case, New Task, etc.) show Dutch
+- [x] 3.3 Verify case list, case detail, task forms show Dutch labels
+- [x] 3.4 Verify English still works when locale is en
+- [x] 3.5 Rebuild app (`npm run build`); hard refresh browser
+
+## 4. Fallback (if app-side fix fails)
+
+- [ ] 4.1 Create or update `templates/index.php` to explicitly load app l10n (if Nextcloud supports this)
+- [ ] 4.2 Document findings for next investigation cycle
diff --git a/openspec/changes/archive/2026-03-03-fix-dutch-display/verify-report.md b/openspec/changes/archive/2026-03-03-fix-dutch-display/verify-report.md
new file mode 100644
index 00000000..82a838c4
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-dutch-display/verify-report.md
@@ -0,0 +1,34 @@
+# Verification Report: Fix Displaying Dutch Language
+
+**Date**: 2026-03-03
+**Change**: fix-dutch-display (archived)
+**Status**: PASSED — automated and manual checks complete
+
+---
+
+## Automated Checks
+
+| Check | Result |
+|-------|--------|
+| en.json valid JSON | OK |
+| nl.json valid JSON | OK |
+| en.json and nl.json identical keys | OK (302 total) |
+| Placeholders preserved in nl.json | OK (36 keys) |
+| All keys used in code exist in l10n | OK (275 keys) |
+
+---
+
+## Manual Verification (fix-dutch-display)
+
+| Task | Status |
+|------|--------|
+| V01: Set Nextcloud to Nederlands; verify dashboard labels show Dutch | OK |
+| V02: Verify case list, case detail, task forms show Dutch | OK |
+| V03: Verify English still works when locale is en | OK |
+
+---
+
+## Verification Scripts
+
+- **L10n (automated)**: `node openspec/verify-l10n.js` from procest app root
+- **Manual**: Set Nextcloud language, open Procest, confirm Dutch/English display
diff --git a/openspec/changes/archive/2026-03-03-fix-settings-bug/.openspec.yaml b/openspec/changes/archive/2026-03-03-fix-settings-bug/.openspec.yaml
new file mode 100644
index 00000000..ed9e94a6
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-settings-bug/.openspec.yaml
@@ -0,0 +1,3 @@
+schema: conduction
+created: 2026-03-03
+status: archived
diff --git a/openspec/changes/archive/2026-03-03-fix-settings-bug/design.md b/openspec/changes/archive/2026-03-03-fix-settings-bug/design.md
new file mode 100644
index 00000000..e67fa064
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-settings-bug/design.md
@@ -0,0 +1,80 @@
+# Design: Fix Settings Bug
+
+## Context
+
+The Procest app uses `MainMenu.vue` for navigation. The footer contains `NcAppNavigationSettings`, which renders a button (showing "Instellingen" when Nextcloud is in Dutch) and a dropdown with two items: "Case Types" (→ `/case-types`) and "Configuration" (→ `/settings`). Both routes render `AdminRoot.vue` — the same page with Configuration and Case Type Management sections. The dropdown is redundant and the button label uses Nextcloud's locale instead of the app's.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Single "Settings" button in the footer that navigates directly to the admin page (one click, no dropdown)
+- Button label uses `t('procest', 'Settings')` for proper app l10n
+- Remove redundant navigation options
+
+**Non-Goals:**
+- Changes to AdminRoot, Configuration, or Case Type Management content
+- Backend changes
+
+## File Map
+
+### Modified Files
+
+| File | Changes |
+|------|---------|
+| `src/navigation/MainMenu.vue` | Replace NcAppNavigationSettings (dropdown) with single NcAppNavigationItem in footer; use t('procest', 'Settings') |
+| `l10n/en.json` | Add "Settings" translation if missing |
+| `l10n/nl.json` | Add "Instellingen" (or "Settings") for Dutch if missing |
+| `src/router/index.js` | Remove `/case-types` route (optional — keeps URL tidy; or keep for backwards compatibility) |
+
+### Unchanged Files
+
+| File | Reason |
+|------|--------|
+| `src/views/settings/AdminRoot.vue` | No changes |
+| All other views | No changes |
+
+## Design Decisions
+
+### DD-01: Single NcAppNavigationItem in Footer
+
+**Decision**: Replace `NcAppNavigationSettings` with a single `NcAppNavigationItem` in the footer slot.
+
+**Rationale**: NcAppNavigationSettings creates a dropdown. With one destination, a dropdown adds an extra click. A direct NcAppNavigationItem gives one-click access. The NcAppNavigation footer slot accepts arbitrary content; NcAppNavigationItem can be used there.
+
+**Implementation**: Put one NcAppNavigationItem with `:to="{ name: 'Settings' }"` and `:name="t('procest', 'Settings')"` in the footer template.
+
+### DD-02: Keep or Remove /case-types Route
+
+**Decision**: Remove the `/case-types` route from the router; use only `/settings`.
+
+**Rationale**: Both routes render AdminRoot. Having two URLs for the same page is confusing. External links or bookmarks to `/case-types` would break — but this is a small app; such links are unlikely. Simpler to have one canonical URL.
+
+**Alternative**: Keep both routes and redirect `/case-types` → `/settings` for backwards compatibility. Prefer removal for simplicity unless backwards compatibility is required.
+
+### DD-03: Localization
+
+**Decision**: Use `t('procest', 'Settings')` for the button label. Add "Settings" to l10n files.
+
+**Rationale**: Procest has l10n/en.json and l10n/nl.json. The `t()` function uses the app's translation files. "Settings" in English; "Instellingen" in Dutch. Ensures the label matches the app's locale.
+
+## Component Change
+
+```
+Before:
+ NcAppNavigationSettings (button: "Instellingen" / framework locale)
+ ├── NcAppNavigationItem "Case Types" → /case-types (AdminRoot)
+ └── NcAppNavigationItem "Configuration" → /settings (AdminRoot)
+
+After:
+ NcAppNavigationItem "Settings" → /settings (AdminRoot)
+ (single item in footer, no dropdown)
+```
+
+## Risks / Trade-offs
+
+- **[Risk] NcAppNavigation footer slot structure** → The footer may expect NcAppNavigationSettings specifically. Mitigation: Check Nextcloud Vue docs; if needed, use NcAppNavigationSettings with one item and pass `:name="t('procest', 'Settings')"` to override the button label.
+- **[Trade-off] Removing /case-types** → Any existing links to `/case-types` would 404. Low risk for a settings page.
+
+## Open Questions
+
+None — the fix is straightforward.
diff --git a/openspec/changes/archive/2026-03-03-fix-settings-bug/proposal.md b/openspec/changes/archive/2026-03-03-fix-settings-bug/proposal.md
new file mode 100644
index 00000000..c4d859eb
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-settings-bug/proposal.md
@@ -0,0 +1,45 @@
+# Proposal: Fix Settings Bug
+
+## Summary
+
+Fix two related UI bugs in the Procest app navigation: (1) the settings button shows "Instellingen" (Dutch) instead of using proper localization; (2) the settings dropdown has two options — "Case Types" and "Configuration" — that both navigate to the same page (AdminRoot). Replace with a single "Settings" button that goes directly to the admin page.
+
+## Problem
+
+1. **Wrong language**: The settings button in the app footer shows "Instellingen" (Dutch) when it should use the app's locale (English by default, or user's language via l10n).
+2. **Redundant dropdown**: Clicking the settings button opens a dropdown with "Case Types" and "Configuration". Both options route to the same component (AdminRoot at `/settings` and `/case-types`). Users must click twice to reach settings; one direct button is sufficient.
+
+## Scope — MVP
+
+**In scope:**
+- Replace the settings dropdown with a single "Settings" button that navigates directly to the admin page
+- Use proper localization: `t('procest', 'Settings')` so the label respects the app's l10n (en.json, nl.json)
+- Remove the redundant `/case-types` route or consolidate routing (both currently render AdminRoot)
+
+**Out of scope:**
+- New features or requirements
+- Changes to the admin page content (Configuration + Case Type Management sections remain as-is)
+
+## Approach
+
+1. In `MainMenu.vue`: Replace `NcAppNavigationSettings` (dropdown with two items) with a single `NcAppNavigationItem` in the footer that links directly to the Settings route
+2. Use `t('procest', 'Settings')` for the button label
+3. Add "Settings" to `l10n/en.json` and `l10n/nl.json` if not present
+4. Remove the redundant `/case-types` route from the router (or keep for backwards compatibility but remove from nav)
+
+## Capabilities
+
+### Modified Capabilities
+
+- **admin-settings**: UI/navigation fix — single settings entry point, proper localization. No spec changes.
+
+## Impact
+
+- **Frontend**: `src/navigation/MainMenu.vue`, `src/router/index.js`, `l10n/en.json`, `l10n/nl.json`
+- **Backend**: None
+- **Dependencies**: Nextcloud Vue (NcAppNavigationItem), existing l10n
+
+## Dependencies
+
+- admin-settings spec (navigation is part of admin access)
+- Nextcloud Vue components
diff --git a/openspec/changes/archive/2026-03-03-fix-settings-bug/specs/admin-settings/spec.md b/openspec/changes/archive/2026-03-03-fix-settings-bug/specs/admin-settings/spec.md
new file mode 100644
index 00000000..4aef562e
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-settings-bug/specs/admin-settings/spec.md
@@ -0,0 +1,36 @@
+# Delta Spec: Admin Settings — Settings Navigation Bug Fix
+
+**Source**: `openspec/specs/admin-settings/spec.md`
+**Scope**: UI/navigation fix — single settings button, proper localization. No spec changes to admin-settings requirements.
+
+---
+
+## ADDED Requirements (implicit from fix)
+
+### REQ-NAV-001: Settings Navigation [MVP]
+
+The app MUST provide a single, clearly labeled entry point to the admin settings page from the main navigation.
+
+#### Scenario: Settings button in navigation footer
+- **WHEN** the user views the Procest app
+- **THEN** a "Settings" (or localized equivalent) button MUST appear in the navigation footer
+- **AND** clicking it MUST navigate directly to the admin settings page (one click, no dropdown)
+
+#### Scenario: Settings label uses app locale
+- **WHEN** the app locale is English
+- **THEN** the settings button MUST display "Settings"
+- **WHEN** the app locale is Dutch
+- **THEN** the settings button MUST display "Instellingen" (or the Dutch translation)
+
+#### Scenario: No redundant navigation options
+- **WHEN** the user clicks the settings button
+- **THEN** the system MUST NOT show a dropdown with multiple options that lead to the same page
+- **AND** the user MUST reach the admin page with a single click
+
+---
+
+## EXCLUDED Requirements
+
+| Req | Description | Reason |
+|-----|-------------|--------|
+| REQ-ADMIN-001 through REQ-ADMIN-016 | Admin settings content and behavior | This change only fixes navigation; admin page content unchanged |
diff --git a/openspec/changes/archive/2026-03-03-fix-settings-bug/tasks.md b/openspec/changes/archive/2026-03-03-fix-settings-bug/tasks.md
new file mode 100644
index 00000000..a5769850
--- /dev/null
+++ b/openspec/changes/archive/2026-03-03-fix-settings-bug/tasks.md
@@ -0,0 +1,16 @@
+# Tasks: Fix Settings Bug
+
+## 1. Navigation Fix [MVP]
+
+- [x] 1.1 Replace NcAppNavigationSettings with single NcAppNavigationItem in MainMenu.vue footer — use `:name="t('procest', 'Settings')"` and `:to="{ name: 'Settings' }"`, include Cog icon
+- [x] 1.2 Add "Settings" to l10n/en.json and "Instellingen" to l10n/nl.json (if not already present)
+
+## 2. Route Cleanup
+
+- [x] 2.1 Remove `/case-types` route from router (or add redirect to `/settings` for backwards compatibility)
+
+## Verification Tasks
+
+- [x] V01 Manual test: Settings button appears in footer with correct label (English: "Settings", Dutch: "Instellingen")
+- [x] V02 Manual test: Single click on Settings navigates to admin page (no dropdown)
+- [x] V03 Manual test: Admin page shows both Configuration and Case Type Management sections
diff --git a/openspec/changes/archive/2026-03-17-fix-settings-url-bug/.openspec.yaml b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/.openspec.yaml
new file mode 100644
index 00000000..219e2a04
--- /dev/null
+++ b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-03-13
diff --git a/openspec/changes/archive/2026-03-17-fix-settings-url-bug/design.md b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/design.md
new file mode 100644
index 00000000..ffb9774b
--- /dev/null
+++ b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/design.md
@@ -0,0 +1,63 @@
+## Approach
+
+Replace hardcoded API URL strings in Pinia stores with `generateUrl()` from `@nextcloud/router`. This function is already a dependency of the project and produces the correct Nextcloud-routed path (e.g., `/index.php/apps/procest/api/settings` or `/apps/procest/api/settings` depending on the server's URL rewrite config). It is the standard pattern used by all other Nextcloud apps.
+
+No PHP changes are needed. The backend routes in `lib/AppInfo/Application.php` are correctly registered. The bug is purely on the client side.
+
+## Implementation
+
+### Import
+
+Add the import to the top of each affected store file:
+
+```js
+import { generateUrl } from '@nextcloud/router'
+```
+
+### URL Replacements
+
+**`src/store/modules/settings.js`** — 2 occurrences:
+
+```js
+// BEFORE
+fetch('/apps/procest/api/settings', ...)
+
+// AFTER
+fetch(generateUrl('/apps/procest/api/settings'), ...)
+```
+
+Both the `fetchSettings()` action (GET) and the `saveSettings()` action (POST) use this URL.
+
+**`src/store/modules/zgwMapping.js`** — 3 occurrences:
+
+```js
+// BEFORE
+fetch('/apps/procest/api/zgw-mappings', ...)
+fetch(`/apps/procest/api/zgw-mappings/${resourceKey}`, ...)
+fetch(`/apps/procest/api/zgw-mappings/${resourceKey}/reset`, ...)
+
+// AFTER
+fetch(generateUrl('/apps/procest/api/zgw-mappings'), ...)
+fetch(generateUrl(`/apps/procest/api/zgw-mappings/${resourceKey}`), ...)
+fetch(generateUrl(`/apps/procest/api/zgw-mappings/${resourceKey}/reset`), ...)
+```
+
+### Why `@nextcloud/router` and not hardcoding `/index.php/`
+
+Hardcoding `/index.php/` would fix the immediate issue but would break in environments where Nextcloud's Apache/nginx mod_rewrite is configured to strip `index.php` (the default in most production setups). `generateUrl()` always produces the correct path for the current server configuration.
+
+## Files Affected
+
+| File | Change |
+|------|--------|
+| `src/store/modules/settings.js` | Add import + fix 2 fetch URLs |
+| `src/store/modules/zgwMapping.js` | Add import + fix 3 fetch URLs |
+
+## Testing
+
+After the fix, verify:
+1. Browser console shows no 404 on `/apps/procest/api/settings` at page load
+2. Dashboard renders KPI cards and data (not a blank screen)
+3. Case list and task list show data (not just "No items found")
+4. Admin settings form shows the current register/schema configuration (not empty fields)
+5. ZGW mapping table loads correctly on the admin settings page
diff --git a/openspec/changes/archive/2026-03-17-fix-settings-url-bug/plan.json b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/plan.json
new file mode 100644
index 00000000..c977dc4a
--- /dev/null
+++ b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/plan.json
@@ -0,0 +1,61 @@
+{
+ "change": "fix-settings-url-bug",
+ "project": "procest",
+ "repo": "ConductionNL/procest",
+ "created": "2026-03-17",
+ "tracking_issue": 13,
+ "tasks": [
+ {
+ "id": 1,
+ "title": "Fix settings.js fetch URLs",
+ "description": "Replace hardcoded /apps/procest/api/settings paths with generateUrl() from @nextcloud/router in fetchSettings() and saveSettings().",
+ "github_issue": 14,
+ "status": "done",
+ "spec_ref": "openspec/changes/fix-settings-url-bug/specs/openregister-integration/spec.md#REQ-OREG-014",
+ "acceptance_criteria": [
+ "@nextcloud/router imported at top of file",
+ "Both fetch calls in settings.js use generateUrl()",
+ "No hardcoded /apps/procest/api/settings strings remain in the file"
+ ],
+ "files_likely_affected": [
+ "src/store/modules/settings.js"
+ ],
+ "labels": ["openspec", "fix-settings-url-bug"]
+ },
+ {
+ "id": 2,
+ "title": "Fix zgwMapping.js fetch URLs",
+ "description": "Replace all 3 hardcoded /apps/procest/api/zgw-mappings paths with generateUrl() from @nextcloud/router.",
+ "github_issue": 15,
+ "status": "done",
+ "spec_ref": "openspec/changes/fix-settings-url-bug/specs/openregister-integration/spec.md#REQ-OREG-014",
+ "acceptance_criteria": [
+ "@nextcloud/router imported at top of file",
+ "All 3 fetch calls in zgwMapping.js use generateUrl()",
+ "No hardcoded /apps/procest/api/zgw-mappings strings remain in the file"
+ ],
+ "files_likely_affected": [
+ "src/store/modules/zgwMapping.js"
+ ],
+ "labels": ["openspec", "fix-settings-url-bug"]
+ },
+ {
+ "id": 3,
+ "title": "Verify fix works end-to-end",
+ "description": "After Tasks 1 and 2 are applied, verify the cascade failure is resolved by testing in a browser.",
+ "github_issue": 16,
+ "status": "done",
+ "spec_ref": "openspec/changes/fix-settings-url-bug/specs/openregister-integration/spec.md#REQ-OREG-014",
+ "acceptance_criteria": [
+ "Browser console shows no 404 on /apps/procest/api/settings on page load",
+ "Browser console shows no 'Object type X is not registered' errors",
+ "Dashboard renders (KPI cards visible, not blank screen)",
+ "Case list shows data or meaningful empty state",
+ "Admin settings form shows register/schema IDs (not empty fields)",
+ "ZGW mapping table loads on admin settings page"
+ ],
+ "files_likely_affected": [],
+ "labels": ["openspec", "fix-settings-url-bug"]
+ }
+ ]
+}
diff --git a/openspec/changes/archive/2026-03-17-fix-settings-url-bug/proposal.md b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/proposal.md
new file mode 100644
index 00000000..fafe9ab4
--- /dev/null
+++ b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/proposal.md
@@ -0,0 +1,26 @@
+## Why
+
+The Procest frontend calls internal API endpoints using hardcoded paths (e.g., `/apps/procest/api/settings`) that bypass Nextcloud's URL routing, causing HTTP 404 errors on every page load. This single bug cascades into complete app failure: all Pinia object stores fail to register their types, making every data fetch (cases, tasks, case types, statuses, ZGW mappings) throw "Object type X is not registered". The app is functionally broken for all users in all environments. Identified by automated browser testing across 5 test perspectives.
+
+## What Changes
+
+- Replace all hardcoded `/apps/procest/api/...` paths in Pinia stores with `generateUrl('/apps/procest/api/...')` from `@nextcloud/router`
+- Affected stores: `settings.js` (2 occurrences) and `zgwMapping.js` (3 occurrences)
+- No PHP changes required — the backend routes are correctly registered; only the frontend URL construction is broken
+
+## Capabilities
+
+### New Capabilities
+
+_(none — this is a bug fix)_
+
+### Modified Capabilities
+
+- `openregister-integration`: The requirement for how frontend stores construct API URLs is being tightened. All internal Procest API calls MUST use `generateUrl()` from `@nextcloud/router` to produce correct Nextcloud-routed paths. Hardcoded paths are prohibited.
+
+## Impact
+
+- **Fixes**: All 5 hardcoded fetch URLs in `src/store/modules/settings.js` and `src/store/modules/zgwMapping.js`
+- **Unblocks**: Dashboard KPI cards, case list, task list, admin settings form — everything that depends on `initializeStores()` succeeding
+- **No breaking changes**: The API contracts are unchanged; only the client-side URL construction changes
+- **No PHP changes**: `SettingsController` and `ZgwMappingController` routes are correctly defined
diff --git a/openspec/changes/archive/2026-03-17-fix-settings-url-bug/specs/openregister-integration/spec.md b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/specs/openregister-integration/spec.md
new file mode 100644
index 00000000..ba24dda8
--- /dev/null
+++ b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/specs/openregister-integration/spec.md
@@ -0,0 +1,34 @@
+# Delta Spec: openregister-integration
+
+## Change: fix-settings-url-bug
+
+### MODIFIED Requirements
+
+#### REQ-OREG-014: Frontend API URL Construction
+
+**Status**: MODIFIED (was implicit/unspecified, now explicit)
+
+**Old behaviour**: Internal Procest API URLs were constructed as hardcoded string literals (e.g., `'/apps/procest/api/settings'`), which fail on Nextcloud installations where `index.php` is not rewritten.
+
+**New requirement**: All frontend `fetch()` calls to internal Procest API endpoints MUST construct URLs using `generateUrl()` from `@nextcloud/router`:
+
+```js
+import { generateUrl } from '@nextcloud/router'
+
+// Correct
+fetch(generateUrl('/apps/procest/api/settings'), { ... })
+fetch(generateUrl(`/apps/procest/api/zgw-mappings/${key}`), { ... })
+
+// Prohibited
+fetch('/apps/procest/api/settings', { ... })
+fetch(`/apps/procest/api/zgw-mappings/${key}`, { ... })
+```
+
+**Rationale**: `generateUrl()` produces the correct URL for the current Nextcloud server configuration regardless of whether `mod_rewrite` / `index.php` stripping is enabled. Hardcoded paths produce 404 errors on standard Nextcloud installations.
+
+**Scope**: This requirement applies to ALL Pinia stores making calls to Procest's own backend controllers (`SettingsController`, `ZgwMappingController`). It does NOT apply to calls to OpenRegister's API (`/index.php/apps/openregister/api/...`), which are handled separately.
+
+**Acceptance criteria**:
+- GIVEN a Procest app page load WHEN `fetchSettings()` is called THEN the network request goes to the correct URL (no 404)
+- GIVEN a Procest admin settings page WHEN ZGW mappings are fetched THEN the network request goes to the correct URL (no 404)
+- GIVEN the fix is applied WHEN the app initializes THEN `initializeStores()` completes without "Object type X is not registered" errors
diff --git a/openspec/changes/archive/2026-03-17-fix-settings-url-bug/tasks.md b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/tasks.md
new file mode 100644
index 00000000..783edca4
--- /dev/null
+++ b/openspec/changes/archive/2026-03-17-fix-settings-url-bug/tasks.md
@@ -0,0 +1,53 @@
+# Tasks: fix-settings-url-bug
+
+## Task 1: Fix settings.js fetch URLs
+
+**File**: `src/store/modules/settings.js`
+
+**What**:
+1. Add `import { generateUrl } from '@nextcloud/router'` at the top of the file
+2. In `fetchSettings()`: replace `fetch('/apps/procest/api/settings', ...)` with `fetch(generateUrl('/apps/procest/api/settings'), ...)`
+3. In `saveSettings()`: replace `fetch('/apps/procest/api/settings', ...)` with `fetch(generateUrl('/apps/procest/api/settings'), ...)`
+
+**Spec ref**: `openspec/changes/fix-settings-url-bug/specs/openregister-integration/spec.md#REQ-OREG-014`
+
+**Acceptance criteria**:
+- [x] `@nextcloud/router` imported at top of file
+- [x] Both fetch calls in `settings.js` use `generateUrl()`
+- [x] No hardcoded `/apps/procest/api/settings` strings remain in the file
+
+---
+
+## Task 2: Fix zgwMapping.js fetch URLs
+
+**File**: `src/store/modules/zgwMapping.js`
+
+**What**:
+1. Add `import { generateUrl } from '@nextcloud/router'` at the top of the file
+2. Fix the 3 fetch calls:
+ - List all mappings: `fetch('/apps/procest/api/zgw-mappings', ...)` → `fetch(generateUrl('/apps/procest/api/zgw-mappings'), ...)`
+ - Get/update single mapping: `fetch('/apps/procest/api/zgw-mappings/${resourceKey}', ...)` → `fetch(generateUrl('/apps/procest/api/zgw-mappings/${resourceKey}'), ...)`
+ - Reset mapping: `fetch('/apps/procest/api/zgw-mappings/${resourceKey}/reset', ...)` → `fetch(generateUrl('/apps/procest/api/zgw-mappings/${resourceKey}/reset'), ...)`
+
+**Spec ref**: `openspec/changes/fix-settings-url-bug/specs/openregister-integration/spec.md#REQ-OREG-014`
+
+**Acceptance criteria**:
+- [x] `@nextcloud/router` imported at top of file
+- [x] All 3 fetch calls in `zgwMapping.js` use `generateUrl()`
+- [x] No hardcoded `/apps/procest/api/zgw-mappings` strings remain in the file
+
+---
+
+## Task 3: Verify fix works end-to-end
+
+**What**: After applying Tasks 1 and 2, verify the fix resolves the cascade failure.
+
+**Acceptance criteria**:
+- [x] Browser console shows no 404 on `/apps/procest/api/settings` on page load
+- [x] Browser console shows no "Object type X is not registered" errors
+- [x] Dashboard renders (KPI cards visible, not blank screen)
+- [x] Case list shows data or meaningful empty state (not "No items found" due to API failure)
+- [x] Admin settings form shows register/schema IDs (not empty fields)
+- [x] ZGW mapping table loads on admin settings page
+
+> **Note**: Tasks 1 and 2 were already implemented before this change was spec'd. Verification (Task 3) requires a running Nextcloud instance.
diff --git a/openspec/changes/zgw-business-rules-compliance/proposal.md b/openspec/changes/zgw-business-rules-compliance/proposal.md
index 06aeab8c..bb18176a 100644
--- a/openspec/changes/zgw-business-rules-compliance/proposal.md
+++ b/openspec/changes/zgw-business-rules-compliance/proposal.md
@@ -1,40 +1,40 @@
-## Why
-
-Procest exposes ZGW-compliant APIs (Zaken, Catalogi, Documenten, Besluiten) on top of OpenRegister, but currently fails ~56 out of 353 business rule assertions in the VNG Newman test suite. Full compliance is required for production use by Dutch municipalities. The business rules enforce data integrity, authorization, side effects (zaak closing/reopening, archive derivation), and cross-register consistency that are mandatory per the VNG ZGW standard.
-
-Additionally, current ZGW endpoints are significantly slower than expected (~2-5s per request vs the expected ~180ms) due to manual enrichment logic instead of leveraging OpenRegister's optimized property inversion and search methods. Performance must be addressed alongside correctness.
-
-## What Changes
-
-- **zrc-007a**: Fix eindstatus detection — set zaak `einddatum` when the statustype with the highest `volgnummer` is created (fallback when `isEindstatus` is not explicitly set)
-- **zrc-007b**: Set `indicatieGebruiksrecht` on all linked informatieobjecten when zaak closes
-- **zrc-007q**: Validate all linked informatieobjecten have `indicatieGebruiksrecht` set before allowing eindstatus creation
-- **zrc-008c**: Check `zaken.heropenen` scope before allowing reopening of a closed zaak
-- **zrc-010**: Fix communicatiekanaal validation error codes (`bad-url` vs `invalid-resource`)
-- **zrc-013a**: Fix hoofdzaak not-found error code (`does-not-exist` instead of `no_match`)
-- **zrc-015**: Validate productenOfDiensten are a subset of zaaktype's allowed products
-- **zrc-016/018/019/020**: Cross-validate sub-resource types belong to zaak's zaaktype
-- **zrc-021**: Derive archiefactiedatum from resultaattype's brondatumArchiefprocedure
-- **zrc-002**: Enforce identificatie + bronorganisatie uniqueness
-- **zrc-005b/023h**: Cascade-delete ObjectInformatieObject when ZaakInformatieObject or zaak is deleted
-- **zrc-009**: Derive vertrouwelijkheidaanduiding from zaaktype without template leakage
-- **zrc-006**: Filter zaken results based on consumer's authorized zaaktypen and vertrouwelijkheidaanduiding
-- **Performance**: Refactor enrichment logic to use OpenRegister's optimized property inversion and search methods instead of manual cross-register lookups
-
-## Capabilities
-
-### New Capabilities
-- `zgw-business-rules`: ZRC/ZTC/DRC/BRC business rule validation and enrichment — covers all VNG-numbered rules (zrc-001 through zrc-023, brc-001 through brc-006, drc-001 through drc-003, ztc-001 through ztc-010)
-- `zgw-endpoint-performance`: Optimize ZGW endpoint response times by leveraging OpenRegister's property inversion, eager loading, and optimized search instead of manual N+1 cross-register lookups
-
-### Modified Capabilities
-- `procest-case-management`: ZGW zaak lifecycle side effects (closing, reopening, archive derivation) are tightened to match VNG standard exactly
-- `procest-object-store`: Cross-register sync (OIO creation/deletion) and cascade delete behavior changes
-
-## Impact
-
-- **Code**: `ZrcController.php`, `ZgwZrcRulesService.php`, `ZgwRulesBase.php`, `ZgwBusinessRulesService.php`, `ZgwService.php`, `ZgwZtcRulesService.php`, `ZgwDrcRulesService.php`, `ZgwBrcRulesService.php`
-- **APIs**: All ZGW endpoints (`/api/zgw/{zaken,catalogi,documenten,besluiten}/v1/*`) — validation responses and side effects change
-- **Dependencies**: OpenRegister ObjectService (property inversion, optimized search), OpenRegister AuthorizationService (scope checking)
-- **Testing**: Newman business rules collection must reach 353/353 assertions passing (0 failures)
-- **Performance target**: Average endpoint response time under 200ms (currently 2-5s)
+## Why
+
+Procest exposes ZGW-compliant APIs (Zaken, Catalogi, Documenten, Besluiten) on top of OpenRegister, but currently fails ~56 out of 353 business rule assertions in the VNG Newman test suite. Full compliance is required for production use by Dutch municipalities. The business rules enforce data integrity, authorization, side effects (zaak closing/reopening, archive derivation), and cross-register consistency that are mandatory per the VNG ZGW standard.
+
+Additionally, current ZGW endpoints are significantly slower than expected (~2-5s per request vs the expected ~180ms) due to manual enrichment logic instead of leveraging OpenRegister's optimized property inversion and search methods. Performance must be addressed alongside correctness.
+
+## What Changes
+
+- **zrc-007a**: Fix eindstatus detection — set zaak `einddatum` when the statustype with the highest `volgnummer` is created (fallback when `isEindstatus` is not explicitly set)
+- **zrc-007b**: Set `indicatieGebruiksrecht` on all linked informatieobjecten when zaak closes
+- **zrc-007q**: Validate all linked informatieobjecten have `indicatieGebruiksrecht` set before allowing eindstatus creation
+- **zrc-008c**: Check `zaken.heropenen` scope before allowing reopening of a closed zaak
+- **zrc-010**: Fix communicatiekanaal validation error codes (`bad-url` vs `invalid-resource`)
+- **zrc-013a**: Fix hoofdzaak not-found error code (`does-not-exist` instead of `no_match`)
+- **zrc-015**: Validate productenOfDiensten are a subset of zaaktype's allowed products
+- **zrc-016/018/019/020**: Cross-validate sub-resource types belong to zaak's zaaktype
+- **zrc-021**: Derive archiefactiedatum from resultaattype's brondatumArchiefprocedure
+- **zrc-002**: Enforce identificatie + bronorganisatie uniqueness
+- **zrc-005b/023h**: Cascade-delete ObjectInformatieObject when ZaakInformatieObject or zaak is deleted
+- **zrc-009**: Derive vertrouwelijkheidaanduiding from zaaktype without template leakage
+- **zrc-006**: Filter zaken results based on consumer's authorized zaaktypen and vertrouwelijkheidaanduiding
+- **Performance**: Refactor enrichment logic to use OpenRegister's optimized property inversion and search methods instead of manual cross-register lookups
+
+## Capabilities
+
+### New Capabilities
+- `zgw-business-rules`: ZRC/ZTC/DRC/BRC business rule validation and enrichment — covers all VNG-numbered rules (zrc-001 through zrc-023, brc-001 through brc-006, drc-001 through drc-003, ztc-001 through ztc-010)
+- `zgw-endpoint-performance`: Optimize ZGW endpoint response times by leveraging OpenRegister's property inversion, eager loading, and optimized search instead of manual N+1 cross-register lookups
+
+### Modified Capabilities
+- `procest-case-management`: ZGW zaak lifecycle side effects (closing, reopening, archive derivation) are tightened to match VNG standard exactly
+- `procest-object-store`: Cross-register sync (OIO creation/deletion) and cascade delete behavior changes
+
+## Impact
+
+- **Code**: `ZrcController.php`, `ZgwZrcRulesService.php`, `ZgwRulesBase.php`, `ZgwBusinessRulesService.php`, `ZgwService.php`, `ZgwZtcRulesService.php`, `ZgwDrcRulesService.php`, `ZgwBrcRulesService.php`
+- **APIs**: All ZGW endpoints (`/api/zgw/{zaken,catalogi,documenten,besluiten}/v1/*`) — validation responses and side effects change
+- **Dependencies**: OpenRegister ObjectService (property inversion, optimized search), OpenRegister AuthorizationService (scope checking)
+- **Testing**: Newman business rules collection must reach 353/353 assertions passing (0 failures)
+- **Performance target**: Average endpoint response time under 200ms (currently 2-5s)
diff --git a/openspec/config.yaml b/openspec/config.yaml
index 28fd5a0a..615b954e 100644
--- a/openspec/config.yaml
+++ b/openspec/config.yaml
@@ -1,55 +1,58 @@
-schema: conduction
-
-context: |
- Project: Procest
- Repo: ConductionNL/procest
- Type: Nextcloud App (PHP backend + Vue 2 frontend)
- Description: Case management (zaakgericht werken) for Nextcloud — manages cases, tasks, statuses, roles, results, decisions
- Key components: Cases, Tasks, Statuses, Roles, Results, Decisions, Dashboard
- Database: PostgreSQL (via OpenRegister's ObjectService)
- Mount path: /var/www/html/custom_apps/procest
-
- Standards:
- Primary: CMMN 1.1 (OMG) + Schema.org for data model
- Semantic: Schema.org JSON-LD type annotations
- API mapping: ZGW APIs (Zaken, Besluiten, Catalogi) for Dutch government
- Supplementary: BPMN 2.0 (task patterns), DMN (decision logic)
-
- Architecture:
- Pattern: Thin client — Procest owns no database tables
- Data layer: OpenRegister (JSON object storage with schema validation)
- Frontend: Vue 2.7 + Pinia stores querying OpenRegister API directly
- Backend: Minimal — SettingsController + ConfigurationService for register setup
- Nextcloud reuse: Calendar (IManager), Contacts (IManager), Files (IRootFolder),
- Activity (IManager), Talk (IBroker), Comments (ICommentsManager)
- Not reusing Deck: No PHP API, board/stack model doesn't fit case lifecycle
-
- Sister app: Pipelinq (CRM) — sends cases via request-to-case conversion
- Shared specs: See ../openspec/specs/ for cross-project conventions
- Project guidelines: See ../project.md for workspace-wide standards
- Architecture: See docs/ARCHITECTURE.md for standards research and data model decisions
- Features: See docs/FEATURES.md for competitive analysis and feature roadmap
-
-rules:
- proposal:
- - Reference shared nextcloud-app spec for app structure requirements
- - Reference docs/ARCHITECTURE.md for data model and standards decisions
- - Consider impact on Pipelinq (request-to-case bridge)
- - Reference docs/FEATURES.md for feature tier (MVP/V1/Enterprise)
- specs:
- - Use CMMN concepts for case lifecycle (CasePlanModel, HumanTask, Milestone)
- - Use Schema.org type annotations for all entities (schema:Project, schema:Action, etc.)
- - Include ZGW mapping column where applicable (Zaak, Rol, Besluit, etc.)
- - Specify which Nextcloud OCP interfaces are used for integration features
- - Reference FEATURES.md tier for each requirement (MVP/V1/Enterprise)
- design:
- - Uses OpenRegister API directly from frontend (no own backend CRUD)
- - Register config in lib/Settings/procest_register.json (OpenAPI 3.0.0 format)
- - Imported via ConfigurationService::importFromApp() in repair step
- - Reference Nextcloud OCP interfaces for all platform integrations
- - Case tasks are OpenRegister objects, NOT Deck cards
- tasks:
- - Tag tasks with feature tier (MVP/V1/Enterprise)
- - Test with OpenRegister to verify schema validation works
- - Verify Nextcloud integration features with actual OCP interfaces
- - Test request-to-case bridge with Pipelinq
+schema: conduction
+
+context: |
+ Project: Procest
+ Repo: ConductionNL/procest
+ Type: Nextcloud App (PHP backend + Vue 2 frontend)
+ Description: Case management (zaakgericht werken) for Nextcloud — manages cases, tasks, statuses, roles, results, decisions
+ Key components: Cases, Tasks, Statuses, Roles, Results, Decisions, Dashboard
+ Database: PostgreSQL (via OpenRegister's ObjectService)
+ Mount path: /var/www/html/custom_apps/procest
+
+ Standards:
+ Primary: CMMN 1.1 (OMG) + Schema.org for data model
+ Semantic: Schema.org JSON-LD type annotations
+ API mapping: ZGW APIs (Zaken, Besluiten, Catalogi) for Dutch government
+ Supplementary: BPMN 2.0 (task patterns), DMN (decision logic)
+
+ Architecture:
+ Pattern: Thin client — Procest owns no database tables
+ Data layer: OpenRegister (JSON object storage with schema validation)
+ Frontend: Vue 2.7 + Pinia stores querying OpenRegister API directly
+ Backend: Minimal — SettingsController + ConfigurationService for register setup
+ Nextcloud reuse: Calendar (IManager), Contacts (IManager), Files (IRootFolder),
+ Activity (IManager), Talk (IBroker), Comments (ICommentsManager)
+ Not reusing Deck: No PHP API, board/stack model doesn't fit case lifecycle
+
+ Sister app: Pipelinq (CRM) — sends cases via request-to-case conversion
+ Shared specs: See ../openspec/specs/ for cross-project conventions
+ Project guidelines: See ../project.md for workspace-wide standards
+ Architecture: See docs/ARCHITECTURE.md for standards research and data model decisions
+ Features: See docs/FEATURES.md for competitive analysis and feature roadmap
+
+rules:
+ proposal:
+ - Reference shared nextcloud-app spec for app structure requirements
+ - Reference docs/ARCHITECTURE.md for data model and standards decisions
+ - Consider impact on Pipelinq (request-to-case bridge)
+ - Reference docs/FEATURES.md for feature tier (MVP/V1/Enterprise)
+ specs:
+ - Use CMMN concepts for case lifecycle (CasePlanModel, HumanTask, Milestone)
+ - Use Schema.org type annotations for all entities (schema:Project, schema:Action, etc.)
+ - Include ZGW mapping column where applicable (Zaak, Rol, Besluit, etc.)
+ - Specify which Nextcloud OCP interfaces are used for integration features
+ - Reference FEATURES.md tier for each requirement (MVP/V1/Enterprise)
+ design:
+ - Uses OpenRegister API directly from frontend (no own backend CRUD)
+ - Register config in lib/Settings/procest_register.json (OpenAPI 3.0.0 format)
+ - Imported via ConfigurationService::importFromApp() in repair step
+ - Reference Nextcloud OCP interfaces for all platform integrations
+ - Case tasks are OpenRegister objects, NOT Deck cards
+ tasks:
+ - Tag tasks with feature tier (MVP/V1/Enterprise)
+ - Test with OpenRegister to verify schema validation works
+ - Verify Nextcloud integration features with actual OCP interfaces
+ - Test request-to-case bridge with Pipelinq
+ review:
+ - Cross-reference shared specs (nextcloud-app, api-patterns, nl-design, docker)
+ - Flag spec deviations with WARNING and justification
diff --git a/openspec/feature-counsel-report.md b/openspec/feature-counsel-report.md
index eb8d4d1e..85a4c317 100644
--- a/openspec/feature-counsel-report.md
+++ b/openspec/feature-counsel-report.md
@@ -1,6 +1,6 @@
-# Feature Counsel Report: Procest
+# Feature Counsel Report: procest
-**Date:** 2026-02-25
+**Date:** 2026-03-16
**Method:** 8-persona feature advisory analysis against OpenSpec specifications
**Personas:** Henk Bakker, Fatima El-Amrani, Sem de Jong, Noor Yilmaz, Annemarie de Vries, Mark Visser, Priya Ganpat, Jan-Willem van der Berg
@@ -8,68 +8,94 @@
## Executive Summary
-Procest has a strong foundation as an internal case management tool for municipal case handlers, with solid data modeling (CMMN 1.1, Schema.org) and a clean architectural separation via OpenRegister. However, across all 8 personas, the most striking gap is the complete absence of a citizen-facing interface -- 4 of 8 personas independently flagged this as their top priority. The system is built entirely for ambtenaren (civil servants) while ignoring the citizens and businesses whose cases are being managed. Beyond this fundamental gap, three cross-cutting themes emerged: (1) the API layer lacks standards compliance (no OpenAPI spec, no ZGW endpoints, no NLGov API Design Rules adherence), blocking municipal adoption and third-party integration; (2) accessibility and plain language requirements are underspecified despite WCAG AA claims -- no minimum font sizes, touch targets, B1 language level, or dark mode; and (3) security/compliance features are decorative rather than enforceable -- confidentiality levels do not restrict access, retention periods are not enforced, and audit logs cannot be exported.
+Procest is a technically sound case management system with strong CMMN/ZGW foundations and a solid data architecture built on OpenRegister. However, all 8 personas independently identified the same critical gap: **Procest is built entirely for internal case handlers and administrators — not for citizens or business owners who are the actual subjects of the cases**. The legally required Citizen Portal (Mijn Zaken, Wmebv) is planned but completely unspecified. Beyond this central gap, four key themes emerged: (1) missing bulk operations and export features that block real-world usage, (2) critical security and compliance gaps (audit log export, multi-tenancy isolation, ENSIA evidence), (3) absent OpenAPI specification blocking all integrators, and (4) insufficient accessibility enforcement (no concrete WCAG AA targets, no plain-Dutch mandate). The foundation is excellent; the gaps are mostly where internal processing meets the outside world — but several are legally blocking.
---
-## Consensus Features (suggested by 3+ personas)
+## Overall Results
+
+| Persona | Missing Features | Improvement Suggestions | Compliance Gaps |
+|---------|-----------------|-------------------------|-----------------|
+| Henk Bakker | 10 | 10 | 8 |
+| Fatima El-Amrani | 8 | 10 | 6 |
+| Sem de Jong | 8 | 8 | 7 |
+| Noor Yilmaz | 10 | 10 | 9 |
+| Annemarie de Vries | 12 | 10 | 10 |
+| Mark Visser | 10 | 10 | 6 |
+| Priya Ganpat | 10 | 10 | 10 |
+| Jan-Willem van der Berg | 10 | 10 | 7 |
+| **Total unique features** | **~40 distinct** | **~35 distinct** | **~25 distinct** |
+
+---
+
+## Consensus Features (suggested by 4+ personas)
| # | Feature | Suggested by | Priority | Impact |
|---|---------|-------------|----------|--------|
-| 1 | **Citizen portal / "Mijn Zaken"** | Henk, Fatima, Mark, Jan-Willem | MUST | Citizens and businesses have no way to track their case status. Legally required under Wmebv/Awb. 4/8 personas flagged this as their #1 need. |
-| 2 | **Plain language B1 Dutch** | Henk, Fatima, Jan-Willem | MUST | All citizen-facing text must be at B1 reading level. Specs use jargon like "Besluitvorming", "CasePlanModel", "P56D" that excludes 2.5M low-literate Dutch adults. |
-| 3 | **Bulk operations on list views** | Mark, Sem, Annemarie | SHOULD | No spec defines bulk reassign, bulk status change, or bulk delete. Essential for real-world case volumes. |
-| 4 | **Proper URL-based routing (not hash)** | Sem, Mark, Annemarie | MUST | Hash-based routing (#/cases/123) breaks deep-linking, bookmarking, sharing, and browser back/forward. 3 personas independently flagged this. |
-| 5 | **Email/SMS notifications** | Henk, Mark, Jan-Willem | SHOULD | Only Nextcloud internal notifications are specified. Citizens and external users need email/SMS to know when their case status changes. |
-| 6 | **Data retention enforcement** | Noor, Mark, Annemarie | SHOULD | Retention periods are defined on result types but never enforced. Violates Archiefwet. Need automated retention checks and destruction workflows. |
-| 7 | **CSV/Excel export** | Mark, Noor | MUST | No export capability anywhere in the specs. Needed for management reporting, WBSO, ENSIA compliance evidence, and data portability (AVG Art. 20). |
-| 8 | **OpenAPI 3.0 specification** | Priya, Annemarie | MUST | No machine-readable API documentation. Table stakes for any government API in 2026. Blocks all third-party integration. |
-| 9 | **ZGW API compatibility layer** | Priya, Annemarie | MUST | ZGW mapping exists only in documentation tables, not as actual endpoints. Municipalities cannot integrate without real ZGW Zaken/Catalogi/Besluiten API endpoints. |
-| 10 | **NLGov API Design Rules v2** | Priya, Annemarie | MUST | Error format, pagination, versioning, and URL patterns do not comply. Mandatory for Dutch government APIs per Forum Standaardisatie. |
+| 1 | **Citizen Portal (Mijn Zaken)** — visual status tracker for citizens | ALL 8 | MUST | Legal requirement (Wmebv); 0% citizen-facing functionality today |
+| 2 | **Bulk Operations** — select → reassign, status-change, delete, export | Mark, Annemarie, Priya, Jan-Willem, Sem | MUST | Current single-record UX blocks real-world municipal workflows |
+| 3 | **CSV/Excel/PDF Export** on all list views + audit trail | Mark, Annemarie, Noor, Jan-Willem, Priya | MUST | Required for compliance audits, WOO/ENSIA, reporting |
+| 4 | **Email/SMS Notifications** for case status changes | Fatima, Jan-Willem, Mark, Henk, Noor | SHOULD | Citizens and handlers both need proactive updates |
+| 5 | **Audit Log Export** (filterable CSV/PDF, admin page) | Noor, Annemarie, Mark, Priya | MUST | BIO2 A.8, NIS 2.5, ENSIA self-evaluation (annual) |
+| 6 | **GEMMA-Standard Case Type Templates** (import pack) | Annemarie, Mark, Jan-Willem, Henk | SHOULD | Reduces deployment from 2 days to 2 hours for 342 municipalities |
+| 7 | **Plain Dutch (B1 level)** + `publicLabel` on StatusType | Henk, Fatima, Jan-Willem, Mark | MUST | "Besluitvorming" excludes large portions of population |
+| 8 | **Published OpenAPI 3.0 specification** | Priya, Annemarie, Mark | MUST | Blocks all third-party integrations and ZGW API adoption |
+| 9 | **Multi-tenancy org isolation** — explicit test scenarios + RBAC matrix | Noor, Mark, Annemarie | CRITICAL | Shared Nextcloud instances are standard in municipalities |
+| 10 | **Help/Contact on every page** (phone + email, max 2 clicks) | Henk, Fatima, Jan-Willem | HIGH | No support path mentioned anywhere in current specs |
+| 11 | **Concrete WCAG AA requirements** (min 18px text, 48×48px buttons, 4.5:1 contrast) | Henk, Fatima, Sem, Annemarie | MUST | Vague "WCAG AA" claims without measurements are not compliance |
+| 12 | **publiccode.yml** in repository root | Annemarie, Mark | MUST | Required for GEMMA Softwarecatalogus listing |
---
## Per-Persona Highlights
### Henk Bakker (Elderly Citizen, 78)
-- **Top need**: Citizen portal with simple status tracking ("Stap 2 van 4")
-- **Key missing feature**: Minimum font sizes (16px+), touch targets (44x44px), zoom support (200%)
-- **Quote**: "Ik wil gewoon weten hoe het met mijn vergunning staat, zonder dat ik hoef te bellen."
+- **Can Henk use this?** NO — Procest is an internal handler tool. No citizen interface exists.
+- **Top need**: Citizen Portal with large buttons (48×48px min), plain Dutch, phone number visible on every page
+- **Key missing feature**: Any citizen-facing component; also: back-navigation on all views, help/support contact everywhere, confirmation messages ("Opgeslagen!")
+- **Quote**: *"Ik zie allerlei technische termen en knoppen die ik niet begrijp. Waar kan ik zien hoe het met mijn aanvraag gaat? En als ik er niet uitkom, wie kan ik dan bellen?"*
### Fatima El-Amrani (Low-Literate Migrant, 52)
-- **Top need**: Visual progress indicators with icons, not text labels
-- **Key missing feature**: Multi-language support (Arabic, Turkish) and RTL CSS
-- **Quote**: "I recognize apps by their icons, not by their names. Give me a folder icon for cases, a checkmark for tasks."
+- **Can Fatima use this?** NO — no citizen-facing interface; handler UI is text-heavy and jargon-laden
+- **Top need**: Visual status tracker with icons + colors (not text), mobile-first, 44×44px touch targets
+- **Key missing feature**: WhatsApp/SMS notifications; audio/read-aloud for case status; Arabic/Darija language option
+- **Quote**: *"Te veel woorden. Ik begrijp niet wat 'Besluitvorming' betekent. Laat me gewoon een vinkje of een kruis zien — klaar of niet klaar."*
### Sem de Jong (Young Digital Native, 22)
-- **Top need**: Dark mode, keyboard shortcuts, command palette (Cmd+K)
-- **Key missing feature**: Global search across all entity types
-- **Quote**: "Hash URLs in 2026? I can't share a filtered view with my colleague. Use proper URL paths."
+- **Can Sem use this?** YES with frustration — handler UI works but feels dated
+- **Top need**: Dark mode, URL state persistence, keyboard shortcuts (Cmd+K), toast notifications
+- **Key missing feature**: URL-persisted filters/sort; skeleton loading states; undo toasts; performance budgets (LCP, CLS)
+- **Quote**: *"Als ik een gefilterde zakenlijst refreshed en de filters verdwijnen, stop ik ermee. Dit is 2026 — URL state is non-negotiable."*
### Noor Yilmaz (Municipal CISO, 36)
-- **Top need**: Exportable audit logs with IP/session data for ENSIA compliance
-- **Key missing feature**: Enforceable confidentiality levels (not just decorative metadata)
-- **Quote**: "A 'geheim' case visible to all users with generic RBAC is a data breach waiting to happen."
+- **Can Noor deploy this?** NO — critical security/compliance gaps block procurement
+- **Top need**: Exportable audit logs, multi-tenancy isolation proof, session management controls
+- **Key missing feature**: ENSIA evidence export; dedicated audit log admin page with filter/export; cross-org isolation test scenarios
+- **Quote**: *"ENSIA evaluatie loopt elk jaar van juli tot december. Zonder exporteerbare auditlogs en bewijs van organisatie-isolatie kan ik dit systeem niet goedkeuren voor aanschaf."*
### Annemarie de Vries (VNG Standards Architect, 38)
-- **Top need**: ZGW API endpoints and publiccode.yml for GEMMA Softwarecatalogus listing
-- **Key missing feature**: Pre-built case type templates for small municipalities
-- **Quote**: "Without ZGW endpoints and publiccode.yml, I cannot recommend Procest to any municipality."
+- **Can Annemarie recommend this?** NO — missing GEMMA alignment, publiccode.yml, Wmebv citizen portal
+- **Top need**: GEMMA_ALIGNMENT.md, Common Ground 5-layer statement, formal ZGW OpenAPI specs
+- **Key missing feature**: GEMMA component mapping; pre-built GEMMA case type templates; Citizen Portal spec; EUPL license
+- **Quote**: *"Het systeem is technisch goed doordacht, maar VNG kan het niet aanbevelen aan 342 gemeenten zonder expliciete GEMMA-mapping, publiccode.yml en een gespecificeerde Mijn Zaken portal — dat is immers wettelijk verplicht."*
### Mark Visser (MKB Software Vendor, 48)
-- **Top need**: CSV/Excel export on every list view for business reporting
-- **Key missing feature**: External portal for tracking submitted cases as a vendor/initiator
-- **Quote**: "I cannot run a business on a system I cannot get data out of. Export is a non-starter blocker."
+- **Can Mark sell this?** NOT YET — missing bulk ops, templates, and citizen portal block sales
+- **Top need**: Bulk case operations (checkbox select → reassign/status-change/export); GEMMA template pack
+- **Key missing feature**: Bulk operations; pre-configured Dutch case type templates; SLA tracking; team workload view
+- **Quote**: *"De eerste vraag die mijn klant stelt is: 'Kan ik dit exporteren naar Excel?' Ik had geen antwoord. Dit is een dealbreaker."*
-### Priya Ganpat (ZZP Developer, 34)
-- **Top need**: Published OpenAPI spec and webhook/event system for integrations
-- **Key missing feature**: RFC 7807 error format and rate limit documentation
-- **Quote**: "I opened DevTools and I can see the requests, but there is no Swagger UI, no try-it-out. I have to read source code."
+### Priya Ganpat (ZZP Developer / Integrator, 34)
+- **Can Priya integrate with this?** NOT YET — no OpenAPI spec, no webhooks, no sandbox
+- **Top need**: Published OpenAPI 3.0 spec; webhook events; RFC 7807 error format; sandbox environment
+- **Key missing feature**: OpenAPI spec; webhook/event system; cursor-based pagination; rate limit docs; idempotency keys
+- **Quote**: *"Ik kan geen integratie bouwen zonder machine-leesbare API-documentatie. Ik ben nu broncode aan het lezen om endpoints te ontdekken — dat kost mij 3× zoveel tijd."*
### Jan-Willem van der Berg (Small Business Owner, 55)
-- **Top need**: Contact information (phone/email) accessible from every screen
-- **Key missing feature**: Simple case status tracker modeled after PostNL package tracking
-- **Quote**: "Dit is een systeem voor ambtenaren. Waar ben IK in dit verhaal?"
+- **Can Jan-Willem use this?** NO — zero citizen-facing components
+- **Top need**: Citizen portal in plain Dutch; automatic email notifications; contact info visible everywhere
+- **Key missing feature**: Any citizen interface; phone number in the UI; plain-Dutch status summaries; pre-filled forms with known business data
+- **Quote**: *"Dit systeem is gemaakt voor de gemeente, niet voor ondernemers zoals ik. Waar zie ik wanneer mijn vergunning klaar is? En als ik een vraag heb — wie bel ik dan?"*
---
@@ -79,153 +105,149 @@ Procest has a strong foundation as an internal case management tool for municipa
| # | Feature | Personas | Priority | Notes |
|---|---------|----------|----------|-------|
-| 1 | Minimum 16px base font, 44x44px touch targets | Henk, Fatima | MUST | No concrete sizing specs despite WCAG AA claims |
-| 2 | B1 Dutch language level for all user-facing text | Henk, Fatima, Jan-Willem | MUST | Specs use compound words like "vertrouwelijkheidaanduiding" (9 syllables) |
-| 3 | Icons alongside ALL text labels | Henk, Fatima | SHOULD | Navigation, status indicators, buttons, filter options need paired icons |
-| 4 | Dark mode / prefers-color-scheme support | Sem | SHOULD | No dark mode variants specified for any color references |
-| 5 | RTL (right-to-left) CSS support | Fatima | SHOULD | Prerequisite for Arabic language support; use CSS logical properties |
-| 6 | Multi-language: Arabic, Turkish, Frisian | Fatima, Annemarie | SHOULD | Only English/Dutch specified; excludes 600K+ residents |
-| 7 | prefers-reduced-motion support | Sem | SHOULD | Kanban drag, status transitions, loading animations need motion query respect |
-| 8 | 200% zoom support verified | Henk | MUST | No spec mentions zoom behavior |
-| 9 | Focus management for SPA navigation | Sem | SHOULD | After view transitions, focus must move to new content, not stay on invisible elements |
-| 10 | Visible "Terug" (back) button on all detail pages | Henk | SHOULD | Users fear using browser back button; need explicit in-app navigation |
+| 1 | Citizen Portal with visual design (icons, colors, minimal text, mobile-first) | Henk, Fatima, Jan-Willem, Mark | MUST | Legal requirement (Wmebv); primary touchpoint for low-literate users |
+| 2 | Concrete WCAG AA requirements: min 18px body, 48×48px buttons, 4.5:1 contrast | Henk, Fatima, Sem, Annemarie | MUST | Current "WCAG AA" claims have no measurable targets |
+| 3 | B1 plain-Dutch mandate + `publicLabel` field on StatusType | Henk, Fatima, Jan-Willem | MUST | Internal "Besluitvorming" → citizen-facing "Uw aanvraag wordt beoordeeld" |
+| 4 | Icon + text label pairs on all status indicators (no color-only signaling) | Henk, Fatima, Sem | MUST | Color-blind and low-vision users need redundant coding |
+| 5 | 44×44px minimum touch targets on all interactive elements | Fatima | MUST | WCAG 2.5.5; small-phone users (5-inch screens) need tappable elements |
+| 6 | Keyboard alternative MUST (not MAY) for drag-and-drop interactions | Henk, Sem | MUST | WCAG 2.1.1 Keyboard; specs currently say "MAY" |
+| 7 | Help/support contact (phone + email) on every page, max 2 clicks | Henk, Fatima, Jan-Willem | MUST | No support contact mentioned anywhere in current specs |
+| 8 | Audio / read-aloud support for case status (citizen portal) | Fatima | COULD | Enables WhatsApp voice workflow; high-impact for low-literacy |
+| 9 | SMS/WhatsApp notifications for status changes | Fatima, Henk, Jan-Willem | SHOULD | Citizens don't check websites; proactive push required |
+| 10 | RTL layout support using CSS logical properties | Fatima | COULD | Future-proofing for Arabic/Darija; prevents later rewrite |
+| 11 | `prefers-reduced-motion` CSS support for all animations | Sem | SHOULD | Vestibular disorder accessibility |
+| 12 | Session timeout warning with extension option | Henk | SHOULD | WCAG 2.2.1 Timing Adjustable |
### Security & Compliance
| # | Feature | Personas | Priority | Standard |
|---|---------|----------|----------|----------|
-| 1 | Audit log export (CSV/PDF) | Noor, Mark | MUST | BIO2 12.4.1, ENSIA evidence |
-| 2 | IP/session/user-agent in audit trail | Noor | MUST | BIO2 12.4.1 source identification |
-| 3 | Enforceable confidentiality levels | Noor | MUST | ISO 27002:2022 clause 5.10 |
-| 4 | Soft-delete with retention-aware destruction | Noor, Priya | SHOULD | Archiefwet, data integrity |
-| 5 | Data retention enforcement automation | Noor, Annemarie, Mark | SHOULD | Archiefwet 1995 |
-| 6 | Permission overview / access matrix | Noor | SHOULD | ISO 27002:2022 clause 8.3 |
-| 7 | Admin action audit logging | Noor | SHOULD | Config changes need same audit rigor as case operations |
-| 8 | DPIA documentation / privacy-by-design section | Noor, Annemarie | SHOULD | AVG article 35 |
-| 9 | Failed authentication logging | Noor | SHOULD | BIO2 9.4.2 |
-| 10 | Four-eyes principle for critical transitions | Noor | COULD | ISO 27002:2022 clause 5.4 |
+| 1 | Audit log export (CSV/PDF, filterable by date/user/action/entity) | Noor, Annemarie, Priya | MUST | BIO2 A.8, NIS Directive 2.5, ENSIA |
+| 2 | ENSIA evidence export (dated summary: case counts, access changes, role distributions) | Noor, Annemarie | MUST | ENSIA self-evaluation (annual Jul–Dec) |
+| 3 | Multi-tenancy org isolation — explicit test scenarios + RBAC matrix per tenant | Noor, Mark, Annemarie | CRITICAL | AVG Art. 32, NIS Directive 2.14 |
+| 4 | Session management: configurable idle timeout (default 30 min) + active sessions page | Noor | SHOULD | ISO 27002:2022 A.6.2.3, BIO2 A.10.2 |
+| 5 | Re-authentication for high-risk operations (status→final, result recording) | Noor | SHOULD | ISO 27002 best practice |
+| 6 | Admin activity in audit trail (case type config changes with before/after snapshots) | Noor, Annemarie | MUST | Change management, BIO2 |
+| 7 | PII minimization in lists: show initials, full names only on detail views | Noor | SHOULD | AVG data minimization principle |
+| 8 | Confidentiality change audit with approval trail | Noor, Annemarie | SHOULD | AVG Art. 32 |
+| 9 | GDPR data subject rights: personal data export + deletion/redaction | Mark, Annemarie | MEDIUM | AVG Art. 17, 20 |
+| 10 | Encryption at rest documentation (Nextcloud-level vs. Procest-level) | Noor | SHOULD | AVG Art. 32, BIO2 A.8.2.3 |
+| 11 | Segregation of duties — prevent same user from being handler + decision maker | Noor | SHOULD | BIO2 A.7.1.3 |
+| 12 | Security.md: data classification, encryption, vulnerability disclosure | Annemarie | SHOULD | Common Ground security requirements |
### API & Developer Experience
| # | Feature | Personas | Priority | Notes |
|---|---------|----------|----------|-------|
-| 1 | Published OpenAPI 3.0 specification | Priya, Annemarie | MUST | No machine-readable API docs exist |
-| 2 | ZGW API compatibility layer | Priya, Annemarie | MUST | Mapping exists in docs only, not as endpoints |
-| 3 | RFC 7807 Problem Details error format | Priya, Annemarie | MUST | NLGov API Design Rules mandate |
-| 4 | Webhook/event notification system | Priya | MUST | Real-time integration; no polling |
-| 5 | API versioning strategy | Priya, Annemarie | MUST | No version segment in current URL pattern |
-| 6 | Rate limit documentation + headers | Priya | SHOULD | Rate limiting exists but is undocumented |
-| 7 | Allowed transitions in API responses | Priya | SHOULD | Expose `_allowedTransitions` to avoid client-side state machine |
-| 8 | `_expand` query parameter for eager loading | Priya | SHOULD | Solve N+1 problem server-side, not just frontend caching |
-| 9 | Cursor-based pagination option | Priya | SHOULD | Offset pagination breaks during concurrent modifications |
-| 10 | Health check endpoint | Priya | SHOULD | `/api/health` for monitoring and CI pipelines |
-| 11 | Bulk/batch API endpoint | Priya, Mark | SHOULD | Migration and batch operations need multi-record support |
-| 12 | JSON-LD `@context` in responses | Priya, Annemarie | COULD | Schema.org types declared but not in API output |
+| 1 | Published OpenAPI 3.0 specification at well-known URL | Priya, Annemarie, Mark | MUST | Blocks all third-party integrations; schema defs already exist |
+| 2 | Webhook/event notifications (case status changed, deadline, task completed) | Priya, Noor, Mark | MUST | Polling doesn't scale; citizen portals need real-time events |
+| 3 | RFC 7807 Problem Details error format (type, title, status, detail, instance) | Priya, Annemarie | SHOULD | NLGov API Design Rules mandate; enables programmatic error handling |
+| 4 | Rate limiting headers + documentation | Priya | SHOULD | Required for production-grade integrations |
+| 5 | Sandbox / test environment with demo data | Priya, Mark | MUST | Municipalities won't test against production data |
+| 6 | Cursor-based pagination option (not just offset/limit) | Priya | SHOULD | Large datasets; consistent iteration under concurrent data changes |
+| 7 | Bulk API endpoints (batch update/reassign) | Priya, Mark | SHOULD | Prevents N+1 API call patterns from client code |
+| 8 | Eager-loading query parameter (`?_embed=caseType,status`) | Priya | SHOULD | Reduces N+1 queries; improves list view performance significantly |
+| 9 | Concurrency control (ETags + If-Match headers) | Priya | SHOULD | Prevents lost-update bugs in concurrent editing |
+| 10 | API versioning strategy + breaking change policy | Priya, Annemarie | SHOULD | Without this, integrations break silently on updates |
+| 11 | Idempotency keys (via `Idempotency-Key` header) | Priya | SHOULD | Prevents duplicate cases on client retry |
+| 12 | ZGW API formal OpenAPI specs for active changes (zgw-*-api) | Annemarie, Priya | MUST | Active changes in progress; need complete formal specs before merge |
### UX & Performance
| # | Feature | Personas | Priority | Notes |
|---|---------|----------|----------|-------|
-| 1 | URL-based routing (replace hash routing) | Sem, Mark, Annemarie | MUST | Bookmarking, sharing, browser back/forward |
-| 2 | Global search / command palette (Cmd+K) | Sem | SHOULD | Unified search across cases, tasks, decisions |
-| 3 | Keyboard shortcuts for power users | Sem | SHOULD | N=new case, T=new task, G+D=go dashboard |
-| 4 | Undo-via-toast instead of confirmation dialogs | Sem | SHOULD | Modern UX pattern; less flow interruption |
-| 5 | Skeleton/shimmer loading on all views | Sem | SHOULD | Only dashboard specifies loading states |
-| 6 | Inline editing on list views | Sem, Mark | SHOULD | Avoid opening detail page for single-field changes |
-| 7 | Optimistic UI for all mutations | Sem | SHOULD | Only kanban drag specifies optimistic updates |
-| 8 | Auto-refresh dashboard with staleness indicator | Sem | SHOULD | Manual refresh is outdated UX |
-| 9 | Saved filters / bookmarkable filtered views | Mark, Sem | SHOULD | Persist filter state in URL and localStorage |
-| 10 | Configurable page size (20/50/100) | Mark, Sem | COULD | 20 items too few for power users |
-| 11 | Print-friendly case overview | Henk, Mark | COULD | For physical records and meetings |
-| 12 | Empty state illustrations | Sem | COULD | Friendly, not just text |
+| 1 | URL state persistence for all filters/sort/pagination | Sem, Mark, Priya | MUST | Share filtered views; bookmarks; browser back/forward |
+| 2 | Dark mode support (CSS variables + `prefers-color-scheme`) | Sem | SHOULD | Government handlers work long shifts; table-stakes in 2026 |
+| 3 | Keyboard shortcuts: Cmd+K command palette, Escape closes modals | Sem | SHOULD | Power user productivity; Escape also required by WCAG 2.1.2 |
+| 4 | Skeleton loading states — defined anatomy per card type | Sem | SHOULD | No blank screens during data fetch |
+| 5 | Toast notifications (bottom-right, auto-dismiss, undo action) | Sem | SHOULD | Replace blocking modal dialogs for save/delete confirmations |
+| 6 | Undo (5-second window) for destructive actions | Sem | SHOULD | Standard 2026 web UX pattern |
+| 7 | Performance budget: LCP <1.2s, CLS <0.1, dashboard load <2s | Sem | SHOULD | Core Web Vitals; applicable to government network conditions |
+| 8 | Copy-to-clipboard for case IDs, task URLs | Sem | COULD | Quick wins for power users |
+| 9 | Case health score / visual indicator (green/yellow/red) | Mark, Sem | SHOULD | At-a-glance risk identification in case lists |
+| 10 | Team workload view (all handlers' queues, not just "My Work") | Mark, Noor | SHOULD | Team leads need allocation visibility |
+| 11 | Upcoming deadlines panel (30-day forecast, team-wide) | Mark, Annemarie | SHOULD | Proactive workload planning prevents overdue spikes |
+| 12 | Visible focus indicators (2px outline) on all interactive elements | Sem | MUST | WCAG AA keyboard accessibility |
### Standards & Interoperability
| # | Feature | Personas | Priority | Standard |
|---|---------|----------|----------|----------|
-| 1 | publiccode.yml | Annemarie | MUST | Standard for Public Code; GEMMA listing prerequisite |
-| 2 | NLGov API Design Rules v2 compliance | Annemarie, Priya | MUST | Forum Standaardisatie mandatory standard |
-| 3 | GEMMA reference component mapping | Annemarie | SHOULD | Position in municipal application landscape |
-| 4 | Common Ground 5-layer documentation | Annemarie | SHOULD | Document which layer each component belongs to |
-| 5 | FSC (Federated Service Connectivity) readiness | Annemarie | SHOULD | Inter-organizational API communication |
-| 6 | EUPL-1.2 license consideration | Annemarie | SHOULD | EC recommended; many procurement frameworks require it |
-| 7 | Notificatiecomponent / Abonnementen API | Annemarie | COULD | ZGW event-driven integration |
-| 8 | DigiD / eHerkenning integration path | Jan-Willem, Annemarie | COULD | Required if citizen portal is built |
-| 9 | e-Depot / archiving integration | Annemarie | SHOULD | Archiefwet compliance for closed cases |
-| 10 | ZGW enum values for confidentiality levels | Annemarie | SHOULD | Use `zaakvertrouwelijk` not `case_sensitive` |
+| 1 | GEMMA_ALIGNMENT.md — explicit component mapping | Annemarie, Mark | MUST | GEMMA Softwarecatalogus prerequisite |
+| 2 | publiccode.yml in repository root | Annemarie | MUST | Standard for Public Code; GEMMA Softwarecatalogus |
+| 3 | Common Ground 5-layer architecture statement | Annemarie | MUST | CG adoption requirement; currently implicit |
+| 4 | EUPL-1.2 license (or AGPL compatibility statement) | Annemarie | SHOULD | VNG preferred license; AGPL creates procurement friction |
+| 5 | Deployment guide for municipalities (Docker, HA, multi-tenant scenarios) | Annemarie, Mark | SHOULD | 342 municipalities need operational documentation |
+| 6 | NLGov API Design Rules v2 compliance declaration | Annemarie, Priya | MUST | Required for Dutch government APIs |
+| 7 | WCAG AA conformance matrix with audit schedule | Annemarie | SHOULD | Validate "WCAG AA" claims with real evidence |
+| 8 | i18n scope: date formats (DD-MM-YYYY), time zones, legal term glossary | Annemarie, Henk | SHOULD | Consistency across municipalities |
### Business & Workflow
| # | Feature | Personas | Priority | Notes |
|---|---------|----------|----------|-------|
-| 1 | Citizen portal / "Mijn Zaken" | Henk, Fatima, Mark, Jan-Willem | MUST | 4 personas; legally required (Wmebv) |
-| 2 | CSV/Excel export on all list views | Mark, Noor | MUST | Reporting, compliance evidence, data portability |
-| 3 | Bulk operations (reassign, status change, delete) | Mark, Sem, Annemarie | SHOULD | Essential for real-world case volumes |
-| 4 | Email/SMS notifications for external users | Mark, Henk, Jan-Willem | SHOULD | Nextcloud-only notifications exclude non-Nextcloud users |
-| 5 | Contact info (phone/email) on every page | Henk, Jan-Willem | SHOULD | Government accessibility fundamental |
-| 6 | Pre-built case type templates | Annemarie, Jan-Willem | SHOULD | Lower barrier for small municipalities |
-| 7 | Organisation-scoped dashboard | Mark | SHOULD | Business owners need company-wide view |
-| 8 | Configurable case identifier format per type | Mark, Annemarie | SHOULD | Prefix like "VRG-2026-042" for recognition |
-| 9 | Help/FAQ system with contextual guidance | Henk, Jan-Willem | SHOULD | No help system specified anywhere |
-| 10 | Data import/migration from existing zaaksystemen | Annemarie | SHOULD | Adoption impossible without migration path |
+| 1 | Bulk operations: case select → reassign / status-change / delete / export | Mark, Priya, Annemarie, Jan-Willem, Sem | MUST | #1 handler friction; 60% time saving estimate |
+| 2 | GEMMA-standard case type templates (Omgevingsvergunning, Subsidieaanvraag, Klacht) | Annemarie, Mark, Jan-Willem, Henk | SHOULD | Import pack with statuses, deadlines, roles, docs, retention rules |
+| 3 | SLA / servicenorm tracking per case type (% within target) | Mark, Annemarie | SHOULD | Primary municipal governance KPI; data exists, not surfaced |
+| 4 | Automatic handler notification on case status change | Mark | SHOULD | Currently only initiators are notified; handlers miss reassignments |
+| 5 | Workload balancing suggestion on task assignment | Annemarie, Mark | SHOULD | Shows handler's current load when assigning |
+| 6 | Extension reason mandatory + audit logged | Mark, Annemarie | SHOULD | Legal audits require documented reasons for deadline extensions |
+| 7 | Pre-publication checklist for case types | Annemarie, Mark | SHOULD | Guards against incomplete configuration in production |
+| 8 | Case type expiry warning (admin alert 30 days before `validUntil`) | Mark | COULD | Prevents stale case types silently affecting new cases |
+| 9 | Business-day deadline calculations (`P40BD` vs `P56D`) | Priya, Annemarie | SHOULD | ZGW deadlines are often in working days |
+| 10 | Status transition flowchart visible in UI | Mark, Henk | SHOULD | Handlers don't know which statuses they can jump to |
+| 11 | Pre-filled forms with known contact/business data | Jan-Willem | SHOULD | Reduces re-entry friction |
+| 12 | Save-draft / resume incomplete applications | Jan-Willem | COULD | Businesses need time to gather required documents |
---
-## Recommended Actions
-
-### MUST (blocking for key user groups)
-
-1. **Build a citizen portal ("Mijn Zaken")** — 4 of 8 personas identified this as their #1 need. Citizens and businesses have no way to track case status. This is not just a UX gap; under the Wmebv and Awb, citizens have a legal right to follow their case progress digitally. Model it after PostNL package tracking: simple steps, visual progress, plain language, and a phone number.
-
-2. **Publish OpenAPI 3.0 specification and adopt NLGov API Design Rules v2** — Without a machine-readable API contract, no developer can build integrations without reading source code. Without NLGov compliance (RFC 7807 errors, standard pagination, versioning), the API fails procurement evaluation at every municipality. This is table stakes for Dutch government APIs.
-
-3. **Implement ZGW API compatibility layer** — The specs claim ZGW mapping but no actual ZGW endpoint exists. At minimum, provide read-only Zaken, Catalogi, and Besluiten API endpoints. Without this, Procest cannot participate in the municipal zaaksysteem-keten and Annemarie cannot recommend it.
+## Recommendations
-4. **Add publiccode.yml** — Required for GEMMA Softwarecatalogus listing, developer.overheid.nl, and Standard for Public Code compliance. A 30-minute task with massive visibility impact.
+### CRITICAL (fix immediately — blocks deployment)
-5. **Replace hash-based routing with proper URL-based routing** — 3 personas independently flagged this as a fundamental limitation. Hash URLs break bookmarking, sharing, deep-linking from other systems, and browser back/forward. Migrate to vue-router with history mode.
+1. **Spec and implement the Citizen Portal (Mijn Zaken)** — Wmebv legally requires this. All 8 personas identified it. Move from "Planned" to active specification immediately. The spec must address: simplified visual UI in B1 Dutch, status timeline with `publicLabel`, document download, contact handler, email/SMS notifications, authentication path, and Pipelinq integration. *(Affects: ALL 8)*
-6. **Add CSV/Excel export on all list views** — No export capability exists anywhere. Blocks business reporting, ENSIA compliance evidence, data portability (AVG Art. 20), and management oversight.
+2. **Define and test multi-tenancy org isolation** — Add explicit test scenarios proving Gemeente A users cannot see Gemeente B cases. Document RBAC matrix per role. Without this, the product cannot be deployed to municipalities sharing Nextcloud infrastructure. *(Affects: Noor, Mark, Annemarie)*
-### SHOULD (significant improvement for multiple personas)
+3. **Implement audit log export with admin UI** — Dedicated Audit Logs page with date/user/action filtering and CSV/PDF export. Required for ENSIA self-evaluation (annual July–December). Without this, no municipality can pass BIO2/ENSIA compliance review. *(Affects: Noor, Annemarie)*
-1. **Mandate B1 Dutch language level for all citizen-facing text** — Specs use jargon ("Besluitvorming", "vertrouwelijkheidaanduiding", "CasePlanModel") that excludes 2.5M low-literate Dutch adults. Add a cross-cutting NFR for B1 plain language.
+### HIGH (fix before next release)
-2. **Make confidentiality levels enforceable, not decorative** — Currently, setting a case to "geheim" does not restrict access. Link confidentiality levels to RBAC so cases at "zaakvertrouwelijk" or higher are invisible without explicit role assignment.
+4. **Add bulk operations** to case list (checkbox select → reassign / status-change / delete / export). Single most impactful efficiency improvement for handlers with 20+ cases. *(Affects: Mark, Priya, Annemarie, Jan-Willem, Sem)*
-3. **Implement audit log export with source identification** — Add IP address, session ID, and user agent to every audit entry. Provide export (CSV/PDF) for ENSIA self-evaluation. Without this, municipalities cannot produce BIO2 compliance evidence.
+5. **Publish OpenAPI 3.0 specification** at a well-known URL. Internal schema definitions already exist — export and publish them. Without this, all third-party integrations (Pipelinq, ZGW consumers, citizen portals) are blocked. *(Affects: Priya, Annemarie, Mark)*
-4. **Implement soft-delete with retention-aware destruction** — Hard deletion of cases with Archiefwet retention obligations is a compliance violation. All deletion must be soft-delete with configurable destruction after retention period.
+6. **Add publiccode.yml** and create GEMMA_ALIGNMENT.md. Prerequisite for GEMMA Softwarecatalogus listing. Without this, VNG cannot recommend the product to 342 municipalities. *(Affects: Annemarie)*
-5. **Add bulk operations on all list views** — Bulk reassign, bulk status change, and bulk delete are essential for real-world case volumes. 3 personas flagged this independently.
+7. **Enforce concrete WCAG AA requirements** throughout specs: min 18px body text, 48×48px touch targets, 4.5:1 contrast, keyboard MUST alternatives for drag-and-drop, visible focus rings, icon+text pairs (no color-only). *(Affects: Henk, Fatima, Sem, Annemarie)*
-6. **Implement webhook/event notification system** — Real-time integration capability. The audit trail already captures events; expose them as configurable outbound webhooks with documented payloads.
+8. **Add Help/Contact component to every page** — Phone number and email visible within 2 clicks from any page. Currently absent from all specs. *(Affects: Henk, Fatima, Jan-Willem)*
-7. **Add email/SMS notifications for case status changes** — Only Nextcloud internal notifications are specified. Citizens and external users (who do not have Nextcloud accounts) need email/SMS.
+9. **Create and ship GEMMA case type template pack** — Omgevingsvergunning, Subsidieaanvraag, Klacht, Bezwaarschrift — with correct statuses, deadlines, roles, docs, and retention rules. One-click admin import. Cuts deployment time from days to hours. *(Affects: Annemarie, Mark, Jan-Willem)*
-8. **Provide pre-built case type templates** — Ship with Omgevingsvergunning, Subsidieaanvraag, and Klacht templates. Most small municipalities lack the expertise to configure CMMN-based case types from scratch.
+10. **Add email/SMS notifications** for case status changes — automatic, in B1 Dutch, explaining what happened and what action is needed. *(Affects: Fatima, Jan-Willem, Mark, Henk, Noor)*
-9. **Specify minimum font sizes (16px+) and touch targets (44x44px)** — No concrete sizing specs exist despite WCAG AA claims. Essential for elderly users and mobile users.
+### MEDIUM (improve when possible)
-10. **Add icons alongside all text labels** — Navigation items, status indicators, buttons, and filter options all need paired icons for low-literate users.
+11. **Implement URL state persistence** — All filters, sort order, and pagination in URL query parameters. Required for sharing views and browser navigation. *(Affects: Sem, Mark, Priya)*
-### COULD (nice-to-have, improves specific persona experience)
+12. **Add RFC 7807 error format** for all API responses. Define error codes (CASE_TYPE_NOT_PUBLISHED, MISSING_REQUIRED_PROPERTIES, etc.). *(Affects: Priya, Annemarie)*
-1. **Add dark mode / prefers-color-scheme support** — Important for young digital natives and evening use. All color references need dark mode variants with verified contrast.
+13. **Add dark mode** using CSS variables + `prefers-color-scheme`. Government handlers work long shifts; this is table-stakes in 2026. *(Affects: Sem)*
-2. **Add global search / command palette (Cmd+K)** — Power user productivity; unified search across cases, tasks, and decisions.
+14. **Add skeleton loading states** with defined component anatomy per card type. No blank screens during data fetch. *(Affects: Sem)*
-3. **Add keyboard shortcuts for common actions** — N=new case, T=new task, /=focus search, ?=show shortcut map.
+15. **Add session management controls** to admin settings: configurable idle timeout, active sessions page, forced logout capability. *(Affects: Noor)*
-4. **Multi-language support (Arabic, Turkish, Frisian)** — Expands accessibility to 600K+ additional residents. Requires RTL CSS support.
+16. **Enforce plain Dutch (B1 level) mandate** — All user-facing text must pass readability standards. Remove ISO 8601 duration strings from UI ("P56D" → "56 dagen / 8 weken"). *(Affects: Henk, Fatima, Jan-Willem, Mark)*
-5. **DigiD / eHerkenning integration path** — Required if citizen portal is built; standard Dutch government authentication.
+17. **Add team workload view** (all handlers' queues, not just "My Work") for coordinators and team leads. *(Affects: Mark, Annemarie)*
-6. **SIEM integration for audit events** — Syslog forwarding or webhook triggers for Security Operations Center monitoring.
+18. **Add webhook/event system** spec (event types, HMAC-SHA256 signature, retry, dead-letter queue). Design now even if implementation is V2. *(Affects: Priya, Noor, Mark)*
-7. **Print-friendly case overview** — For physical records, meetings, and citizens who prefer paper.
+19. **Spec data retention enforcement** — When and how are cases automatically archived/destroyed? Grace period? TMLO export? *(Affects: Annemarie, Noor, Mark)*
-8. **Undo-via-toast pattern instead of confirmation dialogs** — Modern UX pattern reducing flow interruption.
+20. **Add status transition flowchart** visible in admin (case type setup) and handler UI. *(Affects: Mark, Henk)*
---
@@ -233,25 +255,21 @@ Procest has a strong foundation as an internal case management tool for municipa
These features could be turned into OpenSpec changes using `/opsx:new`:
-| Change Name | Description | Related Personas | Estimated Complexity |
-|-------------|-------------|-----------------|---------------------|
-| `citizen-portal` | Citizen-facing "Mijn Zaken" case status tracker with visual progress, plain language, and contact info | Henk, Fatima, Mark, Jan-Willem | XL |
-| `openapi-spec` | Published OpenAPI 3.0 specification at well-known endpoint with Swagger UI | Priya, Annemarie | M |
-| `zgw-api-layer` | Read-only ZGW Zaken, Catalogi, and Besluiten API compatibility endpoints | Priya, Annemarie | XL |
-| `nlgov-api-compliance` | RFC 7807 errors, standard pagination, API versioning, HAL links | Priya, Annemarie | L |
-| `publiccode-yml` | Add publiccode.yml to repository root | Annemarie | S |
-| `url-routing` | Replace hash-based routing with vue-router history mode, URL state for filters | Sem, Mark, Annemarie | L |
-| `data-export` | CSV/Excel export on case list, task list, decision list, and audit log | Mark, Noor | M |
-| `bulk-operations` | Bulk reassign, bulk status change, bulk delete on all list views | Mark, Sem, Annemarie | L |
-| `audit-compliance` | Audit log export, IP/session logging, admin action auditing, SIEM integration | Noor | L |
-| `confidentiality-enforcement` | Link confidentiality levels to access control; restrict visibility based on classification | Noor | L |
-| `soft-delete-retention` | Soft-delete for all entities with retention-aware physical destruction workflow | Noor, Priya | M |
-| `plain-language-b1` | B1 language level requirement, status notification text guidelines, jargon glossary | Henk, Fatima, Jan-Willem | M |
-| `accessibility-sizing` | Minimum font sizes, touch targets, zoom support, focus management specifications | Henk, Fatima | M |
-| `dark-mode` | Dark mode token variants, prefers-color-scheme detection, dark contrast verification | Sem | M |
-| `webhook-events` | Outbound webhook system with configurable event subscriptions and documented payloads | Priya | L |
-| `email-sms-notifications` | Email and SMS notification channels for case status changes | Mark, Henk, Jan-Willem | L |
-| `keyboard-shortcuts` | Global command palette, keyboard shortcut map, power user navigation | Sem | M |
-| `case-type-templates` | Pre-built Omgevingsvergunning, Subsidieaanvraag, Klacht templates with first-run wizard | Annemarie, Jan-Willem | M |
-| `multi-language` | Arabic, Turkish, Frisian language support with RTL CSS | Fatima, Annemarie | XL |
-| `icons-navigation` | Icon-paired labels throughout UI, bottom navigation on mobile | Henk, Fatima | M |
+| Change Name | Description | Related Personas | Complexity |
+|-------------|-------------|-----------------|------------|
+| `mijn-zaken-citizen-portal` | Citizen-facing case status portal: visual timeline, `publicLabel`, document download, B1 Dutch, mobile-first, email/SMS, authentication path | ALL 8 | XL |
+| `bulk-operations` | Bulk select in case/task lists → reassign, status-change, delete, export (CSV/Excel) | Mark, Priya, Annemarie, Sem, Jan-Willem | M |
+| `audit-log-export` | Admin audit log page with date/user/action filter and CSV/PDF export; ENSIA summary export | Noor, Annemarie, Priya | M |
+| `openapi-publication` | Export internal schema to OpenAPI 3.0, publish at well-known URL, commit to versioning and NLGov compliance | Priya, Annemarie, Mark | M |
+| `gemma-alignment-docs` | GEMMA_ALIGNMENT.md, ARCHITECTURE_5LAYER.md, publiccode.yml, EUPL license, deployment guide | Annemarie, Mark | S |
+| `gemma-case-templates` | Import pack: Omgevingsvergunning, Subsidieaanvraag, Klacht, Bezwaarschrift — with statuses, deadlines, roles, docs, retention | Annemarie, Mark, Jan-Willem, Henk | L |
+| `accessibility-enforcement` | Measurable WCAG AA requirements in all specs; Help/Contact component; B1 Dutch mandate; `publicLabel` on StatusType | Henk, Fatima, Sem, Annemarie | M |
+| `multi-tenancy-isolation` | Cross-org isolation test scenarios; RBAC matrix per role; org-scoped API queries | Noor, Mark, Annemarie | L |
+| `notification-system` | Email/SMS for status changes, deadline alerts, task assignments; B1 Dutch default templates; notification preferences | Fatima, Jan-Willem, Mark, Henk, Noor | M |
+| `url-state-persistence` | All list view filters/sort/pagination persisted in URL query parameters | Sem, Mark, Priya | S |
+| `dark-mode-ui` | CSS custom properties for all colors; `prefers-color-scheme` dark mode; `prefers-reduced-motion` | Sem | S |
+| `session-management` | Configurable idle timeout, active sessions page, re-auth for high-risk operations, forced logout | Noor | S |
+| `webhook-events` | Webhook spec (event types, HMAC-SHA256 signature, retry, dead-letter queue) | Priya, Noor, Mark | L |
+| `plain-dutch-standards` | B1 readability mandate, Dutch date formats (DD-MM-YYYY), `publicLabel` field, legal term glossary | Henk, Fatima, Jan-Willem | S |
+| `team-workload-view` | Team-level case/task queue for coordinators; workload balancing suggestion on task assignment | Mark, Annemarie | M |
+| `retention-workflow` | Retention expiry detection, destruction confirmation, TMLO-format e-depot export | Annemarie, Noor | L |
diff --git a/openspec/setup-schemas.ps1 b/openspec/setup-schemas.ps1
new file mode 100644
index 00000000..bd55c793
--- /dev/null
+++ b/openspec/setup-schemas.ps1
@@ -0,0 +1,22 @@
+# Create junction so procest/openspec/schemas points to shared apps-extra/openspec/schemas
+# Run from procest root: .\openspec\setup-schemas.ps1
+
+$schemasDir = Join-Path $PSScriptRoot "schemas"
+$targetDir = (Resolve-Path (Join-Path $PSScriptRoot "..\..\openspec\schemas") -ErrorAction SilentlyContinue)
+
+if (-not $targetDir) {
+ Write-Error "Shared schemas not found at apps-extra/openspec/schemas. Ensure nextcloud-docker-dev workspace is set up."
+ exit 1
+}
+
+if (Test-Path $schemasDir) {
+ $item = Get-Item $schemasDir
+ if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
+ Write-Host "Junction already exists: $schemasDir"
+ exit 0
+ }
+ Remove-Item -Recurse -Force $schemasDir
+}
+
+cmd /c mklink /J $schemasDir $targetDir.Path
+Write-Host "Created junction: $schemasDir -> $($targetDir.Path)"
diff --git a/openspec/specs/admin-settings/spec.md b/openspec/specs/admin-settings/spec.md
index 5dfd48c3..33f12d61 100644
--- a/openspec/specs/admin-settings/spec.md
+++ b/openspec/specs/admin-settings/spec.md
@@ -1,636 +1,636 @@
-# Admin Settings Specification
-
-## Purpose
-
-The admin settings page provides a Nextcloud admin panel for configuring Procest. Administrators manage case types and all their related type definitions: statuses, results, roles, properties, documents, and decisions. The case type system is the behavioral engine of Procest -- every aspect of how a case behaves (allowed statuses, deadlines, required fields, archival rules) is defined here. The admin settings UI follows a list-detail pattern: a case type list on the main page, and a tabbed detail/edit view per case type.
-
-**Feature tiers**: MVP (admin page registration, access control, case type list, case type CRUD, status type CRUD with reorder, default case type, publish action, general tab); V1 (results tab, roles tab, properties tab, documents tab)
-
-## Data Sources
-
-All admin settings data is stored as OpenRegister objects in the `procest` register:
-- **Case types**: schema `caseType`
-- **Status types**: schema `statusType` (linked to caseType via `caseType` reference)
-- **Result types**: schema `resultType` (linked to caseType via `caseType` reference)
-- **Role types**: schema `roleType` (linked to caseType via `caseType` reference)
-- **Property definitions**: schema `propertyDefinition` (linked to caseType via `caseType` reference)
-- **Document types**: schema `documentType` (linked to caseType via `caseType` reference)
-
-## Requirements
-
-### REQ-ADMIN-001: Nextcloud Admin Panel Registration [MVP]
-
-The system MUST register a settings page in the Nextcloud admin panel under the standard administration section.
-
-#### Scenario: Admin settings page is accessible
-- GIVEN a Nextcloud admin user
-- WHEN they navigate to Administration settings
-- THEN a "Procest" entry MUST appear in the admin settings navigation
-- AND clicking "Procest" MUST display the Procest admin settings page
-
-#### Scenario: Regular users cannot access admin settings
-- GIVEN a regular (non-admin) Nextcloud user
-- WHEN they attempt to navigate to Administration > Procest
-- THEN the system MUST deny access
-- AND the "Procest" entry MUST NOT appear in the regular user's settings navigation
-- AND direct URL access to the admin settings endpoint MUST return HTTP 403
-
-#### Scenario: Group admin access
-- GIVEN a Nextcloud group admin (not full admin)
-- WHEN they attempt to access Procest admin settings
-- THEN the system MUST deny access (only full Nextcloud admins may configure case types)
-
-### REQ-ADMIN-002: Case Type List View [MVP]
-
-The admin settings MUST display a list of all case types with key metadata.
-
-#### Scenario: List all case types
-- GIVEN the following case types exist:
- | title | isDraft | processingDeadline | statusCount | resultTypeCount | validFrom | validUntil | isDefault |
- |----------------------|---------|-------------------|-------------|-----------------|------------|------------|-----------|
- | Omgevingsvergunning | false | P56D | 4 | 3 | 2026-01-01 | 2027-12-31 | true |
- | Subsidieaanvraag | false | P42D | 3 | 2 | 2026-01-01 | (none) | false |
- | Klacht behandeling | false | P28D | 3 | 2 | 2026-01-01 | (none) | false |
- | Bezwaarschrift | true | P84D | 2 | 0 | (not set) | (none) | false |
-- WHEN the admin views the case type list
-- THEN all 4 case types MUST be displayed
-- AND each case type entry MUST show:
- - Title
- - Processing deadline in human-readable form (e.g., "56 days")
- - Count of linked status types (e.g., "4 statuses")
- - Count of linked result types (e.g., "3 result types")
- - Published/Draft badge
- - Validity period (e.g., "Jan 2026 -- Dec 2027" or "Jan 2026 -- (no end)")
-- AND the default case type MUST be marked with a star icon or "(default)" label
-
-#### Scenario: Draft types visually distinguished
-- GIVEN case type "Bezwaarschrift" has `isDraft = true`
-- WHEN the admin views the case type list
-- THEN the draft type MUST display a warning badge (e.g., "DRAFT" in amber/yellow)
-- AND the draft type SHOULD have a visually different background or border to distinguish it from published types
-- AND the validity period MUST show "(not set)" when `validFrom` is not configured
-
-#### Scenario: Click to edit case type
-- GIVEN the case type list is displayed
-- WHEN the admin clicks on "Omgevingsvergunning" or its "Edit" button
-- THEN the system MUST navigate to the case type detail/edit view for "Omgevingsvergunning"
-
-### REQ-ADMIN-003: Create Case Type [MVP]
-
-The admin MUST be able to create new case types that start in draft status.
-
-#### Scenario: Add a new case type
-- GIVEN the admin is on the case type list
-- WHEN they click "+ Add Case Type"
-- THEN the system MUST present a case type creation form or navigate to a new case type detail view
-- AND the new case type MUST have `isDraft = true` by default
-- AND the admin MUST be able to fill in at minimum: title, purpose, trigger, subject, processingDeadline, origin, confidentiality, and responsibleUnit (all required fields per ARCHITECTURE.md)
-
-#### Scenario: Created case type appears in list
-- GIVEN the admin fills in the required fields and saves a new case type "Bezwaarschrift"
-- WHEN the save completes successfully
-- THEN the new case type MUST appear in the case type list with a "DRAFT" badge
-- AND the admin MUST be redirected to (or remain on) the detail view to add statuses and other type definitions
-
-#### Scenario: Validation on required fields
-- GIVEN the admin tries to save a case type without filling in the title
-- WHEN they click Save
-- THEN the system MUST display a validation error indicating "Title is required"
-- AND the case type MUST NOT be created
-- AND all other required fields (purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit) MUST also show validation errors if empty
-
-### REQ-ADMIN-004: Case Type Detail/Edit View -- Tabbed Interface [MVP]
-
-The case type detail view MUST use a tabbed interface for organizing the various type definitions.
-
-#### Scenario: Tab layout
-- GIVEN the admin opens the detail view for case type "Omgevingsvergunning"
-- THEN the view MUST display the following tabs:
- - **General** (MVP) -- case type core fields
- - **Statuses** (MVP) -- status type management
- - **Results** (V1) -- result type management
- - **Roles** (V1) -- role type management
- - **Properties** (V1) -- property definition management
- - **Documents** (V1) -- document type management
-- AND the "General" tab MUST be selected by default
-- AND V1 tabs (Results, Roles, Properties, Documents) MAY be hidden or disabled until V1 is implemented
-
-#### Scenario: Save button placement
-- GIVEN the admin is editing a case type
-- THEN a "Save" button MUST be visible at the top of the page (in the header area)
-- AND the Save button MUST persist across tab switches (it is page-level, not tab-level)
-
-### REQ-ADMIN-005: General Tab [MVP]
-
-The General tab MUST allow editing all core case type fields.
-
-#### Scenario: Display and edit general fields
-- GIVEN the admin is on the General tab for "Omgevingsvergunning"
-- THEN the following fields MUST be editable:
- | Field | Value | Type |
- |---------------------|------------------------------------|---------------|
- | Title | Omgevingsvergunning | text input |
- | Description | Vergunning voor bouwactiviteiten | textarea |
- | Purpose | Beoordelen bouwplannen | text input |
- | Trigger | Aanvraag van burger/bedrijf | text input |
- | Subject | Bouw- en verbouwactiviteiten | text input |
- | Processing deadline | 56 (displayed as "P56D") | number + unit |
- | Service target | 42 (displayed as "P42D") | number + unit |
- | Extension allowed | checked | checkbox |
- | Extension period | 28 (displayed as "P28D") | number + unit |
- | Suspension allowed | checked | checkbox |
- | Origin | External | radio buttons |
- | Confidentiality | Internal | select |
- | Publication req. | checked | checkbox |
- | Publication text | Bouwvergunning verleend... | text input |
- | Valid from | 2026-01-01 | date picker |
- | Valid until | 2027-12-31 | date picker |
- | Status | Published / Draft | radio buttons |
-
-#### Scenario: Processing deadline format validation
-- GIVEN the admin enters "abc" in the processing deadline field
-- WHEN they try to save
-- THEN the system MUST display a validation error indicating the deadline must be a valid duration
-- AND the system MUST accept ISO 8601 duration format (e.g., "P56D" for 56 days, "P8W" for 8 weeks)
-- OR the system MUST provide a simplified input (number + unit selector: days/weeks/months) that converts to ISO 8601
-
-#### Scenario: Extension period required when extension allowed
-- GIVEN the admin checks "Extension allowed"
-- WHEN they leave the "Extension period" field empty and try to save
-- THEN the system MUST display a validation error: "Extension period is required when extension is allowed"
-
-#### Scenario: Extension period hidden when extension not allowed
-- GIVEN the admin unchecks "Extension allowed"
-- THEN the "Extension period" field MUST be hidden or disabled
-- AND any previously set extension period value SHOULD be cleared
-
-### REQ-ADMIN-006: Status Type Management [MVP]
-
-The Statuses tab MUST allow managing the ordered list of status types for a case type.
-
-#### Scenario: List status types
-- GIVEN case type "Omgevingsvergunning" has the following status types:
- | order | name | isFinal | notifyInitiator | notificationText |
- |-------|------------------|---------|------------------|-----------------------------------------|
- | 1 | Ontvangen | false | false | |
- | 2 | In behandeling | false | true | Uw zaak is in behandeling genomen |
- | 3 | Besluitvorming | false | false | |
- | 4 | Afgehandeld | true | true | Uw zaak is afgehandeld |
-- WHEN the admin views the Statuses tab
-- THEN all 4 status types MUST be displayed in order
-- AND each status type MUST show: order number, name, isFinal checkbox, notifyInitiator toggle
-- AND status types with `notifyInitiator = true` MUST show the notification text field below them
-
-#### Scenario: Add a new status type
-- GIVEN the admin is on the Statuses tab
-- WHEN they click "+ Add" and enter name "Bezwaar"
-- THEN a new status type MUST be created with the next sequential order number (5)
-- AND the new status type MUST have `isFinal = false` by default
-- AND the status type MUST be linked to the current case type
-
-#### Scenario: Edit a status type
-- GIVEN status type "Ontvangen" exists with order 1
-- WHEN the admin changes the name to "Aanvraag ontvangen"
-- AND clicks Save
-- THEN the status type name MUST be updated to "Aanvraag ontvangen"
-- AND existing cases with this status MUST reflect the updated name
-
-#### Scenario: Reorder status types via drag-and-drop
-- GIVEN 4 status types ordered: Ontvangen (1), In behandeling (2), Besluitvorming (3), Afgehandeld (4)
-- WHEN the admin drags "Besluitvorming" above "In behandeling"
-- THEN the order MUST be updated to: Ontvangen (1), Besluitvorming (2), In behandeling (3), Afgehandeld (4)
-- AND all order fields MUST be recalculated as sequential integers starting from 1
-- AND each status type row MUST display a drag handle icon (e.g., six dots / hamburger icon)
-
-#### Scenario: Mark status as final
-- GIVEN status type "Afgehandeld" with isFinal = false
-- WHEN the admin checks the "Final" checkbox
-- THEN `isFinal` MUST be set to true
-- AND cases reaching this status will be treated as closed by the system
-
-#### Scenario: Delete a status type
-- GIVEN status type "Bezwaar" exists with no cases currently in that status
-- WHEN the admin clicks delete on "Bezwaar"
-- THEN the system MUST prompt for confirmation
-- AND upon confirmation, the status type MUST be deleted
-- AND the remaining status types MUST have their order numbers recalculated sequentially
-
-#### Scenario: Delete status type with active cases
-- GIVEN status type "In behandeling" has 5 cases currently in that status
-- WHEN the admin tries to delete it
-- THEN the system MUST display a warning: "This status is in use by 5 cases. Reassign them before deleting."
-- AND the deletion MUST be blocked until no cases reference this status
-
-#### Scenario: Status type notification configuration
-- GIVEN status type "In behandeling" on the Statuses tab
-- WHEN the admin toggles "Notify initiator" to ON
-- THEN a text field for "Notification text" MUST appear below the toggle
-- AND the admin MUST be able to enter text such as "Uw zaak is in behandeling genomen"
-- AND when the toggle is OFF, the notification text field MUST be hidden
-
-### REQ-ADMIN-007: Default Case Type Selection [MVP]
-
-The admin MUST be able to designate one case type as the default.
-
-#### Scenario: Set default case type
-- GIVEN case types "Omgevingsvergunning" (default), "Subsidieaanvraag", "Klacht behandeling" exist
-- WHEN the admin clicks the default indicator (star/checkbox) on "Subsidieaanvraag"
-- THEN "Subsidieaanvraag" MUST become the default case type
-- AND "Omgevingsvergunning" MUST lose its default status (only one default at a time)
-- AND the star/indicator MUST move to "Subsidieaanvraag"
-
-#### Scenario: Default case type must be published
-- GIVEN a draft case type "Bezwaarschrift"
-- WHEN the admin tries to set it as default
-- THEN the system MUST display an error: "Only published case types can be set as default"
-- AND the default MUST NOT change
-
-#### Scenario: No default set
-- GIVEN no case type is marked as default
-- WHEN a user creates a new case
-- THEN the case creation form MUST require explicit case type selection (no pre-selection)
-
-### REQ-ADMIN-008: Case Type Publish Action [MVP]
-
-The admin MUST be able to publish a draft case type after validating its completeness.
-
-#### Scenario: Publish a complete case type
-- GIVEN draft case type "Bezwaarschrift" with:
- - All required general fields filled in
- - At least 1 status type defined
- - `validFrom` date set
-- WHEN the admin changes the status from "Draft" to "Published" and saves
-- THEN the case type `isDraft` MUST be set to false
-- AND the case type MUST now be available for creating new cases
-- AND the case type list MUST show "Published" instead of "DRAFT"
-
-#### Scenario: Publish incomplete case type -- no statuses
-- GIVEN draft case type "Bezwaarschrift" with no status types defined
-- WHEN the admin tries to publish it
-- THEN the system MUST display a validation error: "At least one status type is required before publishing"
-- AND the case type MUST remain as draft
-
-#### Scenario: Publish incomplete case type -- no validFrom
-- GIVEN draft case type "Bezwaarschrift" with status types but no `validFrom` date
-- WHEN the admin tries to publish it
-- THEN the system MUST display a validation error: "Valid from date is required before publishing"
-- AND the case type MUST remain as draft
-
-#### Scenario: Publish incomplete case type -- missing required general fields
-- GIVEN draft case type "Bezwaarschrift" with `purpose` field empty
-- WHEN the admin tries to publish it
-- THEN the system MUST display validation errors for all missing required fields
-- AND the case type MUST remain as draft
-
-### REQ-ADMIN-009: Result Type Management [V1]
-
-The Results tab SHOULD allow managing result types with archival rules per case type.
-
-#### Scenario: List result types
-- GIVEN case type "Omgevingsvergunning" has the following result types:
- | name | archiveAction | retentionPeriod | retentionDateSource |
- |------------------------|---------------|-----------------|---------------------|
- | Vergunning verleend | retain | P20Y | case_completed |
- | Vergunning geweigerd | destroy | P10Y | case_completed |
- | Ingetrokken | destroy | P5Y | case_completed |
-- WHEN the admin views the Results tab
-- THEN all 3 result types MUST be displayed
-- AND each result type MUST show: name, archive action (retain/destroy), retention period in human-readable form (e.g., "20 years"), and retention date source
-
-#### Scenario: Add a result type
-- GIVEN the admin is on the Results tab
-- WHEN they click "+ Add" and fill in:
- - Name: "Vergunning verleend"
- - Archive action: "retain"
- - Retention period: "P20Y" (20 years)
- - Retention date source: "case_completed"
-- AND click Save
-- THEN the result type MUST be created and linked to the current case type
-- AND it MUST appear in the result types list
-
-#### Scenario: Edit a result type
-- GIVEN result type "Vergunning geweigerd" with retention period P10Y
-- WHEN the admin changes the retention period to P15Y
-- AND clicks Save
-- THEN the retention period MUST be updated to P15Y
-
-#### Scenario: Delete a result type
-- GIVEN result type "Ingetrokken" with no cases referencing it
-- WHEN the admin clicks delete
-- THEN the system MUST prompt for confirmation
-- AND upon confirmation, the result type MUST be deleted
-
-#### Scenario: Delete result type in use
-- GIVEN result type "Vergunning verleend" is referenced by 3 completed cases
-- WHEN the admin tries to delete it
-- THEN the system MUST display a warning: "This result type is in use by 3 cases and cannot be deleted"
-- AND the deletion MUST be blocked
-
-### REQ-ADMIN-010: Role Type Management [V1]
-
-The Roles tab SHOULD allow managing role types with generic role mapping per case type.
-
-#### Scenario: List role types
-- GIVEN case type "Omgevingsvergunning" has the following role types:
- | name | genericRole |
- |--------------------|-----------------|
- | Aanvrager | initiator |
- | Behandelaar | handler |
- | Technisch adviseur | advisor |
- | Beslisser | decision_maker |
-- WHEN the admin views the Roles tab
-- THEN all 4 role types MUST be displayed
-- AND each role type MUST show the name and the generic role mapping
-
-#### Scenario: Add a role type
-- GIVEN the admin is on the Roles tab
-- WHEN they click "+ Add" and enter:
- - Name: "Technisch adviseur"
- - Generic role: "advisor" (selected from dropdown)
-- AND click Save
-- THEN the role type MUST be created and linked to the current case type
-
-#### Scenario: Generic role dropdown options
-- GIVEN the admin is adding or editing a role type
-- THEN the "Generic role" field MUST be a dropdown with the following options (from ARCHITECTURE.md):
- - initiator, handler, advisor, decision_maker, stakeholder, coordinator, contact, co_initiator
-- AND the admin MUST select exactly one generic role per role type
-
-#### Scenario: Edit a role type
-- GIVEN role type "Behandelaar" with genericRole "handler"
-- WHEN the admin changes the name to "Dossierbehandelaar"
-- AND clicks Save
-- THEN the role type name MUST be updated
-
-#### Scenario: Delete a role type
-- GIVEN role type "Technisch adviseur" with no active role assignments referencing it
-- WHEN the admin clicks delete and confirms
-- THEN the role type MUST be deleted
-
-### REQ-ADMIN-011: Property Definition Management [V1]
-
-The Properties tab SHOULD allow managing custom field definitions per case type.
-
-#### Scenario: List property definitions
-- GIVEN case type "Omgevingsvergunning" has the following property definitions:
- | name | format | maxLength | requiredAtStatus |
- |-------------------|--------|-----------|-------------------|
- | Kadastraal nummer | text | 20 | In behandeling |
- | Bouwkosten | number | (none) | Besluitvorming |
- | Oppervlakte | number | (none) | (optional) |
- | Bouwlagen | number | (none) | (optional) |
-- WHEN the admin views the Properties tab
-- THEN all 4 property definitions MUST be displayed
-- AND each MUST show: name, format, max length (if set), and the status at which it is required (or "optional")
-
-#### Scenario: Add a property definition
-- GIVEN the admin is on the Properties tab
-- WHEN they click "+ Add" and fill in:
- - Name: "Kadastraal nummer"
- - Definition: "Het kadastrale perceelnummer"
- - Format: "text" (selected from dropdown: text, number, date, datetime)
- - Max length: 20
- - Required at status: "In behandeling" (selected from the case type's status types)
-- AND click Save
-- THEN the property definition MUST be created and linked to the current case type
-
-#### Scenario: Required at status dropdown
-- GIVEN the admin is adding a property definition
-- THEN the "Required at status" field MUST be a dropdown populated with the case type's status types
-- AND the dropdown MUST include an "(optional)" or "(not required)" option for properties that are never required
-
-#### Scenario: Edit a property definition
-- GIVEN property "Bouwkosten" with format "number"
-- WHEN the admin changes the format to "text"
-- AND clicks Save
-- THEN the format MUST be updated to "text"
-
-#### Scenario: Delete a property definition
-- GIVEN property "Oppervlakte" exists
-- WHEN the admin clicks delete and confirms
-- THEN the property definition MUST be deleted
-- AND any existing case property values for "Oppervlakte" SHOULD be retained on existing cases (orphaned but not lost)
-
-### REQ-ADMIN-012: Document Type Management [V1]
-
-The Documents tab SHOULD allow managing document type requirements per case type.
-
-#### Scenario: List document types
-- GIVEN case type "Omgevingsvergunning" has the following document types:
- | name | direction | requiredAtStatus |
- |------------------------|-----------|---------------------|
- | Bouwtekening | incoming | In behandeling |
- | Constructieberekening | incoming | In behandeling |
- | Situatietekening | incoming | In behandeling |
- | Welstandsadvies | internal | Besluitvorming |
- | Vergunningsbesluit | outgoing | Afgehandeld |
-- WHEN the admin views the Documents tab
-- THEN all 5 document types MUST be displayed
-- AND each MUST show: name, direction (incoming/internal/outgoing), and required-at-status
-
-#### Scenario: Add a document type
-- GIVEN the admin is on the Documents tab
-- WHEN they click "+ Add" and fill in:
- - Name: "Bouwtekening"
- - Category: "Tekeningen"
- - Direction: "incoming" (selected from dropdown: incoming, internal, outgoing)
- - Required at status: "In behandeling" (from case type's statuses)
-- AND click Save
-- THEN the document type MUST be created and linked to the current case type
-
-#### Scenario: Direction dropdown options
-- GIVEN the admin is adding or editing a document type
-- THEN the "Direction" field MUST be a dropdown with options: incoming, internal, outgoing
-- AND these MUST map to: documents received from initiator, internal working documents, and documents sent to initiator
-
-#### Scenario: Edit a document type
-- GIVEN document type "Welstandsadvies" with direction "internal"
-- WHEN the admin changes the required-at-status from "Besluitvorming" to "In behandeling"
-- AND clicks Save
-- THEN the required-at-status MUST be updated
-
-#### Scenario: Delete a document type
-- GIVEN document type "Situatietekening" exists
-- WHEN the admin clicks delete and confirms
-- THEN the document type MUST be deleted from the case type
-
-### REQ-ADMIN-013: Error Scenarios [MVP]
-
-The admin settings MUST handle error conditions gracefully.
-
-#### Scenario: Delete published case type with active cases
-- GIVEN published case type "Omgevingsvergunning" has 10 active (non-final) cases
-- WHEN the admin tries to delete the case type
-- THEN the system MUST display a blocking error: "This case type has 10 active cases and cannot be deleted. Close or reassign all cases first."
-- AND the case type MUST NOT be deleted
-
-#### Scenario: Delete published case type with only completed cases
-- GIVEN published case type "Klacht behandeling" has 5 cases, all with final status
-- WHEN the admin tries to delete the case type
-- THEN the system MUST display a warning: "This case type has 5 completed cases. Deleting it will make those cases reference a missing type. Proceed?"
-- AND upon confirmation, the case type MUST be deleted
-- AND the system SHOULD set `isDraft = true` or mark it as archived rather than hard-deleting
-
-#### Scenario: Reorder to duplicate order numbers
-- GIVEN the admin somehow creates two status types with the same order number (e.g., via concurrent editing)
-- WHEN the system detects duplicate order numbers
-- THEN the system MUST automatically renumber status types sequentially based on their current position
-- AND display a notification: "Status order has been recalculated"
-
-#### Scenario: Save fails due to network error
-- GIVEN the admin edits a case type and clicks Save
-- AND the API request fails due to a network error
-- WHEN the error occurs
-- THEN the system MUST display an error message: "Failed to save changes. Please try again."
-- AND the form data MUST be preserved (not lost)
-- AND the admin MUST be able to retry saving without re-entering data
-
-#### Scenario: Concurrent editing conflict
-- GIVEN admin "A" and admin "B" both open case type "Omgevingsvergunning" for editing
-- AND admin "A" saves changes to the processing deadline
-- WHEN admin "B" tries to save their changes
-- THEN the system SHOULD detect the conflict (e.g., via version/timestamp comparison)
-- AND display a warning: "This case type was modified by another user. Reload to see the latest version."
-- OR the system MAY use last-write-wins if conflict detection is not implemented in MVP
-
-### REQ-ADMIN-014: Validation Rules [MVP]
-
-The admin settings MUST enforce validation rules on case type configuration.
-
-#### Scenario: Processing deadline format validation
-- GIVEN the admin enters a processing deadline
-- THEN the system MUST validate it as a valid ISO 8601 duration (e.g., "P56D", "P8W", "P2M")
-- AND if using a simplified input (number + unit), the system MUST convert to ISO 8601 on save
-- AND invalid values (negative numbers, zero, non-numeric input) MUST be rejected with a clear error message
-
-#### Scenario: Extension period required when extension allowed
-- GIVEN the admin checks "Extension allowed" on the General tab
-- WHEN they try to save without setting an extension period
-- THEN the system MUST display: "Extension period is required when extension is allowed"
-- AND the save MUST be blocked
-
-#### Scenario: Valid from must precede valid until
-- GIVEN the admin sets validFrom = 2027-01-01 and validUntil = 2026-12-31
-- WHEN they try to save
-- THEN the system MUST display: "Valid from date must be before valid until date"
-- AND the save MUST be blocked
-
-#### Scenario: At least one non-final status required
-- GIVEN a case type with only one status type marked as `isFinal = true`
-- WHEN the admin tries to save
-- THEN the system MUST display a warning: "At least one non-final status is recommended for proper case lifecycle"
-- AND the save MAY proceed (warning, not blocking)
-
-#### Scenario: Status type name uniqueness within case type
-- GIVEN case type "Omgevingsvergunning" already has a status type "Ontvangen"
-- WHEN the admin tries to add another status type named "Ontvangen"
-- THEN the system MUST display: "A status type with this name already exists for this case type"
-- AND the creation MUST be blocked
-
-### REQ-ADMIN-015: Case Type List Layout [MVP]
-
-The case type list MUST follow the layout structure defined in DESIGN-REFERENCES.md section 3.6.
-
-#### Scenario: List layout structure
-- GIVEN the admin views the case type list
-- THEN the page MUST display:
- - A page title "Administration > Procest"
- - A "CASE TYPES" section header with an "+ Add Case Type" button
- - A list of case type cards, each showing metadata as described in REQ-ADMIN-002
-- AND published types MUST display a "Published" badge in a neutral/positive color
-- AND draft types MUST display a "DRAFT" badge in amber/warning color with a different visual treatment
-- AND the default case type MUST show a star icon or "(default)" label
-
-#### Scenario: Empty case type list
-- GIVEN no case types have been created
-- WHEN the admin views the case type list
-- THEN the system MUST display an empty state message (e.g., "No case types configured yet")
-- AND the "+ Add Case Type" button MUST be prominently displayed
-- AND the system SHOULD provide guidance (e.g., "Create your first case type to start managing cases")
-
-### REQ-ADMIN-016: Case Type Detail Layout [MVP]
-
-The case type detail/edit view MUST follow the layout structure defined in DESIGN-REFERENCES.md section 3.7.
-
-#### Scenario: Detail view header
-- GIVEN the admin opens the detail view for "Omgevingsvergunning"
-- THEN the page MUST display:
- - Breadcrumb: "Administration > Procest > Omgevingsvergunning"
- - A "Save" button in the header area
- - The tabbed interface as defined in REQ-ADMIN-004
-
-#### Scenario: Statuses tab layout
-- GIVEN the admin is on the Statuses tab
-- THEN the layout MUST show:
- - Section header "STATUSES (drag to reorder)" with an "+ Add" button
- - A list of status types with drag handles on the left
- - Each status type row showing: drag handle, order number, name, notification toggle, "Final" checkbox
- - Status types with notification enabled showing the notification text field below the row
-
-#### Scenario: Back navigation
-- GIVEN the admin is on the case type detail view
-- WHEN they click the breadcrumb link "Procest"
-- THEN the system MUST navigate back to the case type list
-- AND if there are unsaved changes, the system SHOULD prompt: "You have unsaved changes. Discard?"
-
-## Non-Functional Requirements
-
-- **Performance**: Case type list MUST load within 1 second for up to 50 case types. Case type detail view (including all linked type definitions) MUST load within 2 seconds.
-- **Accessibility**: All form fields MUST have associated labels. Drag-and-drop reordering MUST have a keyboard alternative (e.g., up/down arrow buttons). Error messages MUST be associated with their fields via `aria-describedby`. All content MUST meet WCAG AA standards.
-- **Localization**: All labels, error messages, validation messages, and placeholder text MUST support English and Dutch localization.
-- **Data integrity**: Deleting a case type or sub-entity MUST use soft-delete or referential integrity checks. The system MUST prevent orphaning active cases.
-- **Responsiveness**: The admin settings page MUST be usable on desktop viewports (minimum 1024px width). Mobile responsiveness is not required for admin settings.
-
-### Current Implementation Status
-
-**Implemented:**
-- Admin panel registration via `OCA\Procest\Settings\AdminSettings` (`lib/Settings/AdminSettings.php`) and `OCA\Procest\Sections\SettingsSection` (`lib/Sections/SettingsSection.php`) -- registers the "Procest" section in Nextcloud admin settings with icon support.
-- Admin settings Vue root component (`src/views/settings/AdminRoot.vue`) renders the full admin page with two sections: Case Type Management and ZGW API Mapping.
-- Case type list view (`src/views/settings/CaseTypeList.vue`) using `CnIndexPage` -- displays title, isDraft badge (Draft/Published), processing deadline, validity period. Supports set-as-default (star icon, published-only) and delete actions.
-- Case type detail/edit view (`src/views/settings/CaseTypeDetail.vue`) with tabbed interface: General and Statuses tabs are implemented. Publish/unpublish buttons with validation errors. Save button in header.
-- General tab (`src/views/settings/tabs/GeneralTab.vue`) with fields: title, description, purpose, trigger, subject, processing deadline, service target, extension allowed/period, suspension allowed, origin, confidentiality, publication required/text, valid from/until, draft/published status.
-- Statuses tab (`src/views/settings/tabs/StatusesTab.vue`) with ordered list, drag-and-drop reorder, inline editing, add/delete, isFinal checkbox, notifyInitiator toggle with notification text field.
-- Case type CRUD via OpenRegister object store (`src/store/modules/object.js` using `createObjectStore` from `@conduction/nextcloud-vue`).
-- Default case type selection persisted via `SettingsService` (`lib/Service/SettingsService.php`, config key `default_case_type`).
-- Settings controller (`lib/Controller/SettingsController.php`) with index/create/load endpoints.
-- Register configuration auto-import from `procest_register.json` (`lib/Service/SettingsService.php::loadConfiguration`).
-- Case type admin orchestrator component (`src/views/settings/CaseTypeAdmin.vue`) managing list/detail view switching.
-- Duration formatting helpers (`src/utils/durationHelpers.js`).
-- Case type validation utilities (`src/utils/caseTypeValidation.js`).
-
-**Not yet implemented:**
-- Results tab (V1) -- result type CRUD with archival rules.
-- Roles tab (V1) -- role type CRUD with generic role mapping.
-- Properties tab (V1) -- property definition CRUD with required-at-status linking.
-- Documents tab (V1) -- document type CRUD with direction and required-at-status.
-- Publish validation: checking for at least one status type and validFrom date before publishing (partial -- UI has publish errors display but completeness checks may not cover all scenarios).
-- Delete case type blocking when active cases exist (no backend enforcement found).
-- Concurrent editing conflict detection.
-- Keyboard alternative for drag-and-drop reorder.
-
-### Standards & References
-
-- **ZGW Catalogi API (VNG)**: The case type data model maps directly to ZaakType, StatusType, ResultaatType, RolType, EigenschapType, InformatieObjectType from the ZGW Catalogi API specification (VNG-Realisatie/catalogi-api).
-- **CMMN 1.1**: Case type modeled after CaseDefinition concept; status types correspond to CMMN Milestone sequences.
-- **Schema.org**: Properties use `schema:name`, `schema:description`, `schema:identifier` mappings.
-- **ISO 8601**: Duration format for processing deadlines, extension periods, retention periods.
-- **WCAG AA**: Spec requires accessible form labels, keyboard alternatives for drag-and-drop, `aria-describedby` for error messages.
-- **GEMMA**: Dutch municipal architecture standards for zaakgericht werken.
-
-### Specificity Assessment
-
-This spec is highly specific and implementation-ready. Requirements are well-structured with concrete scenarios, data tables, and validation rules.
-
-**Strengths:** Detailed Gherkin scenarios covering happy paths and error cases. Clear feature tier separation (MVP vs V1). Explicit field definitions with types.
-
-**Missing/Ambiguous:**
-- No API endpoint definitions (REST paths, request/response schemas) -- relies on OpenRegister generic CRUD.
-- Publish validation logic not fully specified at the backend level (controller vs service layer responsibility).
-- Archival rules for result types reference `retentionDateSource` options but do not define their semantics in detail (e.g., what "custom_property" or "related_case" means concretely).
-- No specification of how V1 tabs become available (feature flag, config, or automatic based on version).
-- Decision types (REQ-CT-11 in case-types spec) are mentioned in the data model but not in the admin-settings spec tabs.
-
-**Open questions:**
-1. Should the admin settings enforce backend validation (server-side) or is frontend validation sufficient for MVP?
-2. How should the system handle case type versioning -- can a published case type be edited, or must it be unpublished first?
-3. Should delete of status types cascade to status records on existing cases?
+# Admin Settings Specification
+
+## Purpose
+
+The admin settings page provides a Nextcloud admin panel for configuring Procest. Administrators manage case types and all their related type definitions: statuses, results, roles, properties, documents, and decisions. The case type system is the behavioral engine of Procest -- every aspect of how a case behaves (allowed statuses, deadlines, required fields, archival rules) is defined here. The admin settings UI follows a list-detail pattern: a case type list on the main page, and a tabbed detail/edit view per case type.
+
+**Feature tiers**: MVP (admin page registration, access control, case type list, case type CRUD, status type CRUD with reorder, default case type, publish action, general tab); V1 (results tab, roles tab, properties tab, documents tab)
+
+## Data Sources
+
+All admin settings data is stored as OpenRegister objects in the `procest` register:
+- **Case types**: schema `caseType`
+- **Status types**: schema `statusType` (linked to caseType via `caseType` reference)
+- **Result types**: schema `resultType` (linked to caseType via `caseType` reference)
+- **Role types**: schema `roleType` (linked to caseType via `caseType` reference)
+- **Property definitions**: schema `propertyDefinition` (linked to caseType via `caseType` reference)
+- **Document types**: schema `documentType` (linked to caseType via `caseType` reference)
+
+## Requirements
+
+### REQ-ADMIN-001: Nextcloud Admin Panel Registration [MVP]
+
+The system MUST register a settings page in the Nextcloud admin panel under the standard administration section.
+
+#### Scenario: Admin settings page is accessible
+- GIVEN a Nextcloud admin user
+- WHEN they navigate to Administration settings
+- THEN a "Procest" entry MUST appear in the admin settings navigation
+- AND clicking "Procest" MUST display the Procest admin settings page
+
+#### Scenario: Regular users cannot access admin settings
+- GIVEN a regular (non-admin) Nextcloud user
+- WHEN they attempt to navigate to Administration > Procest
+- THEN the system MUST deny access
+- AND the "Procest" entry MUST NOT appear in the regular user's settings navigation
+- AND direct URL access to the admin settings endpoint MUST return HTTP 403
+
+#### Scenario: Group admin access
+- GIVEN a Nextcloud group admin (not full admin)
+- WHEN they attempt to access Procest admin settings
+- THEN the system MUST deny access (only full Nextcloud admins may configure case types)
+
+### REQ-ADMIN-002: Case Type List View [MVP]
+
+The admin settings MUST display a list of all case types with key metadata.
+
+#### Scenario: List all case types
+- GIVEN the following case types exist:
+ | title | isDraft | processingDeadline | statusCount | resultTypeCount | validFrom | validUntil | isDefault |
+ |----------------------|---------|-------------------|-------------|-----------------|------------|------------|-----------|
+ | Omgevingsvergunning | false | P56D | 4 | 3 | 2026-01-01 | 2027-12-31 | true |
+ | Subsidieaanvraag | false | P42D | 3 | 2 | 2026-01-01 | (none) | false |
+ | Klacht behandeling | false | P28D | 3 | 2 | 2026-01-01 | (none) | false |
+ | Bezwaarschrift | true | P84D | 2 | 0 | (not set) | (none) | false |
+- WHEN the admin views the case type list
+- THEN all 4 case types MUST be displayed
+- AND each case type entry MUST show:
+ - Title
+ - Processing deadline in human-readable form (e.g., "56 days")
+ - Count of linked status types (e.g., "4 statuses")
+ - Count of linked result types (e.g., "3 result types")
+ - Published/Draft badge
+ - Validity period (e.g., "Jan 2026 -- Dec 2027" or "Jan 2026 -- (no end)")
+- AND the default case type MUST be marked with a star icon or "(default)" label
+
+#### Scenario: Draft types visually distinguished
+- GIVEN case type "Bezwaarschrift" has `isDraft = true`
+- WHEN the admin views the case type list
+- THEN the draft type MUST display a warning badge (e.g., "DRAFT" in amber/yellow)
+- AND the draft type SHOULD have a visually different background or border to distinguish it from published types
+- AND the validity period MUST show "(not set)" when `validFrom` is not configured
+
+#### Scenario: Click to edit case type
+- GIVEN the case type list is displayed
+- WHEN the admin clicks on "Omgevingsvergunning" or its "Edit" button
+- THEN the system MUST navigate to the case type detail/edit view for "Omgevingsvergunning"
+
+### REQ-ADMIN-003: Create Case Type [MVP]
+
+The admin MUST be able to create new case types that start in draft status.
+
+#### Scenario: Add a new case type
+- GIVEN the admin is on the case type list
+- WHEN they click "+ Add Case Type"
+- THEN the system MUST present a case type creation form or navigate to a new case type detail view
+- AND the new case type MUST have `isDraft = true` by default
+- AND the admin MUST be able to fill in at minimum: title, purpose, trigger, subject, processingDeadline, origin, confidentiality, and responsibleUnit (all required fields per ARCHITECTURE.md)
+
+#### Scenario: Created case type appears in list
+- GIVEN the admin fills in the required fields and saves a new case type "Bezwaarschrift"
+- WHEN the save completes successfully
+- THEN the new case type MUST appear in the case type list with a "DRAFT" badge
+- AND the admin MUST be redirected to (or remain on) the detail view to add statuses and other type definitions
+
+#### Scenario: Validation on required fields
+- GIVEN the admin tries to save a case type without filling in the title
+- WHEN they click Save
+- THEN the system MUST display a validation error indicating "Title is required"
+- AND the case type MUST NOT be created
+- AND all other required fields (purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit) MUST also show validation errors if empty
+
+### REQ-ADMIN-004: Case Type Detail/Edit View -- Tabbed Interface [MVP]
+
+The case type detail view MUST use a tabbed interface for organizing the various type definitions.
+
+#### Scenario: Tab layout
+- GIVEN the admin opens the detail view for case type "Omgevingsvergunning"
+- THEN the view MUST display the following tabs:
+ - **General** (MVP) -- case type core fields
+ - **Statuses** (MVP) -- status type management
+ - **Results** (V1) -- result type management
+ - **Roles** (V1) -- role type management
+ - **Properties** (V1) -- property definition management
+ - **Documents** (V1) -- document type management
+- AND the "General" tab MUST be selected by default
+- AND V1 tabs (Results, Roles, Properties, Documents) MAY be hidden or disabled until V1 is implemented
+
+#### Scenario: Save button placement
+- GIVEN the admin is editing a case type
+- THEN a "Save" button MUST be visible at the top of the page (in the header area)
+- AND the Save button MUST persist across tab switches (it is page-level, not tab-level)
+
+### REQ-ADMIN-005: General Tab [MVP]
+
+The General tab MUST allow editing all core case type fields.
+
+#### Scenario: Display and edit general fields
+- GIVEN the admin is on the General tab for "Omgevingsvergunning"
+- THEN the following fields MUST be editable:
+ | Field | Value | Type |
+ |---------------------|------------------------------------|---------------|
+ | Title | Omgevingsvergunning | text input |
+ | Description | Vergunning voor bouwactiviteiten | textarea |
+ | Purpose | Beoordelen bouwplannen | text input |
+ | Trigger | Aanvraag van burger/bedrijf | text input |
+ | Subject | Bouw- en verbouwactiviteiten | text input |
+ | Processing deadline | 56 (displayed as "P56D") | number + unit |
+ | Service target | 42 (displayed as "P42D") | number + unit |
+ | Extension allowed | checked | checkbox |
+ | Extension period | 28 (displayed as "P28D") | number + unit |
+ | Suspension allowed | checked | checkbox |
+ | Origin | External | radio buttons |
+ | Confidentiality | Internal | select |
+ | Publication req. | checked | checkbox |
+ | Publication text | Bouwvergunning verleend... | text input |
+ | Valid from | 2026-01-01 | date picker |
+ | Valid until | 2027-12-31 | date picker |
+ | Status | Published / Draft | radio buttons |
+
+#### Scenario: Processing deadline format validation
+- GIVEN the admin enters "abc" in the processing deadline field
+- WHEN they try to save
+- THEN the system MUST display a validation error indicating the deadline must be a valid duration
+- AND the system MUST accept ISO 8601 duration format (e.g., "P56D" for 56 days, "P8W" for 8 weeks)
+- OR the system MUST provide a simplified input (number + unit selector: days/weeks/months) that converts to ISO 8601
+
+#### Scenario: Extension period required when extension allowed
+- GIVEN the admin checks "Extension allowed"
+- WHEN they leave the "Extension period" field empty and try to save
+- THEN the system MUST display a validation error: "Extension period is required when extension is allowed"
+
+#### Scenario: Extension period hidden when extension not allowed
+- GIVEN the admin unchecks "Extension allowed"
+- THEN the "Extension period" field MUST be hidden or disabled
+- AND any previously set extension period value SHOULD be cleared
+
+### REQ-ADMIN-006: Status Type Management [MVP]
+
+The Statuses tab MUST allow managing the ordered list of status types for a case type.
+
+#### Scenario: List status types
+- GIVEN case type "Omgevingsvergunning" has the following status types:
+ | order | name | isFinal | notifyInitiator | notificationText |
+ |-------|------------------|---------|------------------|-----------------------------------------|
+ | 1 | Ontvangen | false | false | |
+ | 2 | In behandeling | false | true | Uw zaak is in behandeling genomen |
+ | 3 | Besluitvorming | false | false | |
+ | 4 | Afgehandeld | true | true | Uw zaak is afgehandeld |
+- WHEN the admin views the Statuses tab
+- THEN all 4 status types MUST be displayed in order
+- AND each status type MUST show: order number, name, isFinal checkbox, notifyInitiator toggle
+- AND status types with `notifyInitiator = true` MUST show the notification text field below them
+
+#### Scenario: Add a new status type
+- GIVEN the admin is on the Statuses tab
+- WHEN they click "+ Add" and enter name "Bezwaar"
+- THEN a new status type MUST be created with the next sequential order number (5)
+- AND the new status type MUST have `isFinal = false` by default
+- AND the status type MUST be linked to the current case type
+
+#### Scenario: Edit a status type
+- GIVEN status type "Ontvangen" exists with order 1
+- WHEN the admin changes the name to "Aanvraag ontvangen"
+- AND clicks Save
+- THEN the status type name MUST be updated to "Aanvraag ontvangen"
+- AND existing cases with this status MUST reflect the updated name
+
+#### Scenario: Reorder status types via drag-and-drop
+- GIVEN 4 status types ordered: Ontvangen (1), In behandeling (2), Besluitvorming (3), Afgehandeld (4)
+- WHEN the admin drags "Besluitvorming" above "In behandeling"
+- THEN the order MUST be updated to: Ontvangen (1), Besluitvorming (2), In behandeling (3), Afgehandeld (4)
+- AND all order fields MUST be recalculated as sequential integers starting from 1
+- AND each status type row MUST display a drag handle icon (e.g., six dots / hamburger icon)
+
+#### Scenario: Mark status as final
+- GIVEN status type "Afgehandeld" with isFinal = false
+- WHEN the admin checks the "Final" checkbox
+- THEN `isFinal` MUST be set to true
+- AND cases reaching this status will be treated as closed by the system
+
+#### Scenario: Delete a status type
+- GIVEN status type "Bezwaar" exists with no cases currently in that status
+- WHEN the admin clicks delete on "Bezwaar"
+- THEN the system MUST prompt for confirmation
+- AND upon confirmation, the status type MUST be deleted
+- AND the remaining status types MUST have their order numbers recalculated sequentially
+
+#### Scenario: Delete status type with active cases
+- GIVEN status type "In behandeling" has 5 cases currently in that status
+- WHEN the admin tries to delete it
+- THEN the system MUST display a warning: "This status is in use by 5 cases. Reassign them before deleting."
+- AND the deletion MUST be blocked until no cases reference this status
+
+#### Scenario: Status type notification configuration
+- GIVEN status type "In behandeling" on the Statuses tab
+- WHEN the admin toggles "Notify initiator" to ON
+- THEN a text field for "Notification text" MUST appear below the toggle
+- AND the admin MUST be able to enter text such as "Uw zaak is in behandeling genomen"
+- AND when the toggle is OFF, the notification text field MUST be hidden
+
+### REQ-ADMIN-007: Default Case Type Selection [MVP]
+
+The admin MUST be able to designate one case type as the default.
+
+#### Scenario: Set default case type
+- GIVEN case types "Omgevingsvergunning" (default), "Subsidieaanvraag", "Klacht behandeling" exist
+- WHEN the admin clicks the default indicator (star/checkbox) on "Subsidieaanvraag"
+- THEN "Subsidieaanvraag" MUST become the default case type
+- AND "Omgevingsvergunning" MUST lose its default status (only one default at a time)
+- AND the star/indicator MUST move to "Subsidieaanvraag"
+
+#### Scenario: Default case type must be published
+- GIVEN a draft case type "Bezwaarschrift"
+- WHEN the admin tries to set it as default
+- THEN the system MUST display an error: "Only published case types can be set as default"
+- AND the default MUST NOT change
+
+#### Scenario: No default set
+- GIVEN no case type is marked as default
+- WHEN a user creates a new case
+- THEN the case creation form MUST require explicit case type selection (no pre-selection)
+
+### REQ-ADMIN-008: Case Type Publish Action [MVP]
+
+The admin MUST be able to publish a draft case type after validating its completeness.
+
+#### Scenario: Publish a complete case type
+- GIVEN draft case type "Bezwaarschrift" with:
+ - All required general fields filled in
+ - At least 1 status type defined
+ - `validFrom` date set
+- WHEN the admin changes the status from "Draft" to "Published" and saves
+- THEN the case type `isDraft` MUST be set to false
+- AND the case type MUST now be available for creating new cases
+- AND the case type list MUST show "Published" instead of "DRAFT"
+
+#### Scenario: Publish incomplete case type -- no statuses
+- GIVEN draft case type "Bezwaarschrift" with no status types defined
+- WHEN the admin tries to publish it
+- THEN the system MUST display a validation error: "At least one status type is required before publishing"
+- AND the case type MUST remain as draft
+
+#### Scenario: Publish incomplete case type -- no validFrom
+- GIVEN draft case type "Bezwaarschrift" with status types but no `validFrom` date
+- WHEN the admin tries to publish it
+- THEN the system MUST display a validation error: "Valid from date is required before publishing"
+- AND the case type MUST remain as draft
+
+#### Scenario: Publish incomplete case type -- missing required general fields
+- GIVEN draft case type "Bezwaarschrift" with `purpose` field empty
+- WHEN the admin tries to publish it
+- THEN the system MUST display validation errors for all missing required fields
+- AND the case type MUST remain as draft
+
+### REQ-ADMIN-009: Result Type Management [V1]
+
+The Results tab SHOULD allow managing result types with archival rules per case type.
+
+#### Scenario: List result types
+- GIVEN case type "Omgevingsvergunning" has the following result types:
+ | name | archiveAction | retentionPeriod | retentionDateSource |
+ |------------------------|---------------|-----------------|---------------------|
+ | Vergunning verleend | retain | P20Y | case_completed |
+ | Vergunning geweigerd | destroy | P10Y | case_completed |
+ | Ingetrokken | destroy | P5Y | case_completed |
+- WHEN the admin views the Results tab
+- THEN all 3 result types MUST be displayed
+- AND each result type MUST show: name, archive action (retain/destroy), retention period in human-readable form (e.g., "20 years"), and retention date source
+
+#### Scenario: Add a result type
+- GIVEN the admin is on the Results tab
+- WHEN they click "+ Add" and fill in:
+ - Name: "Vergunning verleend"
+ - Archive action: "retain"
+ - Retention period: "P20Y" (20 years)
+ - Retention date source: "case_completed"
+- AND click Save
+- THEN the result type MUST be created and linked to the current case type
+- AND it MUST appear in the result types list
+
+#### Scenario: Edit a result type
+- GIVEN result type "Vergunning geweigerd" with retention period P10Y
+- WHEN the admin changes the retention period to P15Y
+- AND clicks Save
+- THEN the retention period MUST be updated to P15Y
+
+#### Scenario: Delete a result type
+- GIVEN result type "Ingetrokken" with no cases referencing it
+- WHEN the admin clicks delete
+- THEN the system MUST prompt for confirmation
+- AND upon confirmation, the result type MUST be deleted
+
+#### Scenario: Delete result type in use
+- GIVEN result type "Vergunning verleend" is referenced by 3 completed cases
+- WHEN the admin tries to delete it
+- THEN the system MUST display a warning: "This result type is in use by 3 cases and cannot be deleted"
+- AND the deletion MUST be blocked
+
+### REQ-ADMIN-010: Role Type Management [V1]
+
+The Roles tab SHOULD allow managing role types with generic role mapping per case type.
+
+#### Scenario: List role types
+- GIVEN case type "Omgevingsvergunning" has the following role types:
+ | name | genericRole |
+ |--------------------|-----------------|
+ | Aanvrager | initiator |
+ | Behandelaar | handler |
+ | Technisch adviseur | advisor |
+ | Beslisser | decision_maker |
+- WHEN the admin views the Roles tab
+- THEN all 4 role types MUST be displayed
+- AND each role type MUST show the name and the generic role mapping
+
+#### Scenario: Add a role type
+- GIVEN the admin is on the Roles tab
+- WHEN they click "+ Add" and enter:
+ - Name: "Technisch adviseur"
+ - Generic role: "advisor" (selected from dropdown)
+- AND click Save
+- THEN the role type MUST be created and linked to the current case type
+
+#### Scenario: Generic role dropdown options
+- GIVEN the admin is adding or editing a role type
+- THEN the "Generic role" field MUST be a dropdown with the following options (from ARCHITECTURE.md):
+ - initiator, handler, advisor, decision_maker, stakeholder, coordinator, contact, co_initiator
+- AND the admin MUST select exactly one generic role per role type
+
+#### Scenario: Edit a role type
+- GIVEN role type "Behandelaar" with genericRole "handler"
+- WHEN the admin changes the name to "Dossierbehandelaar"
+- AND clicks Save
+- THEN the role type name MUST be updated
+
+#### Scenario: Delete a role type
+- GIVEN role type "Technisch adviseur" with no active role assignments referencing it
+- WHEN the admin clicks delete and confirms
+- THEN the role type MUST be deleted
+
+### REQ-ADMIN-011: Property Definition Management [V1]
+
+The Properties tab SHOULD allow managing custom field definitions per case type.
+
+#### Scenario: List property definitions
+- GIVEN case type "Omgevingsvergunning" has the following property definitions:
+ | name | format | maxLength | requiredAtStatus |
+ |-------------------|--------|-----------|-------------------|
+ | Kadastraal nummer | text | 20 | In behandeling |
+ | Bouwkosten | number | (none) | Besluitvorming |
+ | Oppervlakte | number | (none) | (optional) |
+ | Bouwlagen | number | (none) | (optional) |
+- WHEN the admin views the Properties tab
+- THEN all 4 property definitions MUST be displayed
+- AND each MUST show: name, format, max length (if set), and the status at which it is required (or "optional")
+
+#### Scenario: Add a property definition
+- GIVEN the admin is on the Properties tab
+- WHEN they click "+ Add" and fill in:
+ - Name: "Kadastraal nummer"
+ - Definition: "Het kadastrale perceelnummer"
+ - Format: "text" (selected from dropdown: text, number, date, datetime)
+ - Max length: 20
+ - Required at status: "In behandeling" (selected from the case type's status types)
+- AND click Save
+- THEN the property definition MUST be created and linked to the current case type
+
+#### Scenario: Required at status dropdown
+- GIVEN the admin is adding a property definition
+- THEN the "Required at status" field MUST be a dropdown populated with the case type's status types
+- AND the dropdown MUST include an "(optional)" or "(not required)" option for properties that are never required
+
+#### Scenario: Edit a property definition
+- GIVEN property "Bouwkosten" with format "number"
+- WHEN the admin changes the format to "text"
+- AND clicks Save
+- THEN the format MUST be updated to "text"
+
+#### Scenario: Delete a property definition
+- GIVEN property "Oppervlakte" exists
+- WHEN the admin clicks delete and confirms
+- THEN the property definition MUST be deleted
+- AND any existing case property values for "Oppervlakte" SHOULD be retained on existing cases (orphaned but not lost)
+
+### REQ-ADMIN-012: Document Type Management [V1]
+
+The Documents tab SHOULD allow managing document type requirements per case type.
+
+#### Scenario: List document types
+- GIVEN case type "Omgevingsvergunning" has the following document types:
+ | name | direction | requiredAtStatus |
+ |------------------------|-----------|---------------------|
+ | Bouwtekening | incoming | In behandeling |
+ | Constructieberekening | incoming | In behandeling |
+ | Situatietekening | incoming | In behandeling |
+ | Welstandsadvies | internal | Besluitvorming |
+ | Vergunningsbesluit | outgoing | Afgehandeld |
+- WHEN the admin views the Documents tab
+- THEN all 5 document types MUST be displayed
+- AND each MUST show: name, direction (incoming/internal/outgoing), and required-at-status
+
+#### Scenario: Add a document type
+- GIVEN the admin is on the Documents tab
+- WHEN they click "+ Add" and fill in:
+ - Name: "Bouwtekening"
+ - Category: "Tekeningen"
+ - Direction: "incoming" (selected from dropdown: incoming, internal, outgoing)
+ - Required at status: "In behandeling" (from case type's statuses)
+- AND click Save
+- THEN the document type MUST be created and linked to the current case type
+
+#### Scenario: Direction dropdown options
+- GIVEN the admin is adding or editing a document type
+- THEN the "Direction" field MUST be a dropdown with options: incoming, internal, outgoing
+- AND these MUST map to: documents received from initiator, internal working documents, and documents sent to initiator
+
+#### Scenario: Edit a document type
+- GIVEN document type "Welstandsadvies" with direction "internal"
+- WHEN the admin changes the required-at-status from "Besluitvorming" to "In behandeling"
+- AND clicks Save
+- THEN the required-at-status MUST be updated
+
+#### Scenario: Delete a document type
+- GIVEN document type "Situatietekening" exists
+- WHEN the admin clicks delete and confirms
+- THEN the document type MUST be deleted from the case type
+
+### REQ-ADMIN-013: Error Scenarios [MVP]
+
+The admin settings MUST handle error conditions gracefully.
+
+#### Scenario: Delete published case type with active cases
+- GIVEN published case type "Omgevingsvergunning" has 10 active (non-final) cases
+- WHEN the admin tries to delete the case type
+- THEN the system MUST display a blocking error: "This case type has 10 active cases and cannot be deleted. Close or reassign all cases first."
+- AND the case type MUST NOT be deleted
+
+#### Scenario: Delete published case type with only completed cases
+- GIVEN published case type "Klacht behandeling" has 5 cases, all with final status
+- WHEN the admin tries to delete the case type
+- THEN the system MUST display a warning: "This case type has 5 completed cases. Deleting it will make those cases reference a missing type. Proceed?"
+- AND upon confirmation, the case type MUST be deleted
+- AND the system SHOULD set `isDraft = true` or mark it as archived rather than hard-deleting
+
+#### Scenario: Reorder to duplicate order numbers
+- GIVEN the admin somehow creates two status types with the same order number (e.g., via concurrent editing)
+- WHEN the system detects duplicate order numbers
+- THEN the system MUST automatically renumber status types sequentially based on their current position
+- AND display a notification: "Status order has been recalculated"
+
+#### Scenario: Save fails due to network error
+- GIVEN the admin edits a case type and clicks Save
+- AND the API request fails due to a network error
+- WHEN the error occurs
+- THEN the system MUST display an error message: "Failed to save changes. Please try again."
+- AND the form data MUST be preserved (not lost)
+- AND the admin MUST be able to retry saving without re-entering data
+
+#### Scenario: Concurrent editing conflict
+- GIVEN admin "A" and admin "B" both open case type "Omgevingsvergunning" for editing
+- AND admin "A" saves changes to the processing deadline
+- WHEN admin "B" tries to save their changes
+- THEN the system SHOULD detect the conflict (e.g., via version/timestamp comparison)
+- AND display a warning: "This case type was modified by another user. Reload to see the latest version."
+- OR the system MAY use last-write-wins if conflict detection is not implemented in MVP
+
+### REQ-ADMIN-014: Validation Rules [MVP]
+
+The admin settings MUST enforce validation rules on case type configuration.
+
+#### Scenario: Processing deadline format validation
+- GIVEN the admin enters a processing deadline
+- THEN the system MUST validate it as a valid ISO 8601 duration (e.g., "P56D", "P8W", "P2M")
+- AND if using a simplified input (number + unit), the system MUST convert to ISO 8601 on save
+- AND invalid values (negative numbers, zero, non-numeric input) MUST be rejected with a clear error message
+
+#### Scenario: Extension period required when extension allowed
+- GIVEN the admin checks "Extension allowed" on the General tab
+- WHEN they try to save without setting an extension period
+- THEN the system MUST display: "Extension period is required when extension is allowed"
+- AND the save MUST be blocked
+
+#### Scenario: Valid from must precede valid until
+- GIVEN the admin sets validFrom = 2027-01-01 and validUntil = 2026-12-31
+- WHEN they try to save
+- THEN the system MUST display: "Valid from date must be before valid until date"
+- AND the save MUST be blocked
+
+#### Scenario: At least one non-final status required
+- GIVEN a case type with only one status type marked as `isFinal = true`
+- WHEN the admin tries to save
+- THEN the system MUST display a warning: "At least one non-final status is recommended for proper case lifecycle"
+- AND the save MAY proceed (warning, not blocking)
+
+#### Scenario: Status type name uniqueness within case type
+- GIVEN case type "Omgevingsvergunning" already has a status type "Ontvangen"
+- WHEN the admin tries to add another status type named "Ontvangen"
+- THEN the system MUST display: "A status type with this name already exists for this case type"
+- AND the creation MUST be blocked
+
+### REQ-ADMIN-015: Case Type List Layout [MVP]
+
+The case type list MUST follow the layout structure defined in DESIGN-REFERENCES.md section 3.6.
+
+#### Scenario: List layout structure
+- GIVEN the admin views the case type list
+- THEN the page MUST display:
+ - A page title "Administration > Procest"
+ - A "CASE TYPES" section header with an "+ Add Case Type" button
+ - A list of case type cards, each showing metadata as described in REQ-ADMIN-002
+- AND published types MUST display a "Published" badge in a neutral/positive color
+- AND draft types MUST display a "DRAFT" badge in amber/warning color with a different visual treatment
+- AND the default case type MUST show a star icon or "(default)" label
+
+#### Scenario: Empty case type list
+- GIVEN no case types have been created
+- WHEN the admin views the case type list
+- THEN the system MUST display an empty state message (e.g., "No case types configured yet")
+- AND the "+ Add Case Type" button MUST be prominently displayed
+- AND the system SHOULD provide guidance (e.g., "Create your first case type to start managing cases")
+
+### REQ-ADMIN-016: Case Type Detail Layout [MVP]
+
+The case type detail/edit view MUST follow the layout structure defined in DESIGN-REFERENCES.md section 3.7.
+
+#### Scenario: Detail view header
+- GIVEN the admin opens the detail view for "Omgevingsvergunning"
+- THEN the page MUST display:
+ - Breadcrumb: "Administration > Procest > Omgevingsvergunning"
+ - A "Save" button in the header area
+ - The tabbed interface as defined in REQ-ADMIN-004
+
+#### Scenario: Statuses tab layout
+- GIVEN the admin is on the Statuses tab
+- THEN the layout MUST show:
+ - Section header "STATUSES (drag to reorder)" with an "+ Add" button
+ - A list of status types with drag handles on the left
+ - Each status type row showing: drag handle, order number, name, notification toggle, "Final" checkbox
+ - Status types with notification enabled showing the notification text field below the row
+
+#### Scenario: Back navigation
+- GIVEN the admin is on the case type detail view
+- WHEN they click the breadcrumb link "Procest"
+- THEN the system MUST navigate back to the case type list
+- AND if there are unsaved changes, the system SHOULD prompt: "You have unsaved changes. Discard?"
+
+## Non-Functional Requirements
+
+- **Performance**: Case type list MUST load within 1 second for up to 50 case types. Case type detail view (including all linked type definitions) MUST load within 2 seconds.
+- **Accessibility**: All form fields MUST have associated labels. Drag-and-drop reordering MUST have a keyboard alternative (e.g., up/down arrow buttons). Error messages MUST be associated with their fields via `aria-describedby`. All content MUST meet WCAG AA standards.
+- **Localization**: All labels, error messages, validation messages, and placeholder text MUST support English and Dutch localization.
+- **Data integrity**: Deleting a case type or sub-entity MUST use soft-delete or referential integrity checks. The system MUST prevent orphaning active cases.
+- **Responsiveness**: The admin settings page MUST be usable on desktop viewports (minimum 1024px width). Mobile responsiveness is not required for admin settings.
+
+### Current Implementation Status
+
+**Implemented:**
+- Admin panel registration via `OCA\Procest\Settings\AdminSettings` (`lib/Settings/AdminSettings.php`) and `OCA\Procest\Sections\SettingsSection` (`lib/Sections/SettingsSection.php`) -- registers the "Procest" section in Nextcloud admin settings with icon support.
+- Admin settings Vue root component (`src/views/settings/AdminRoot.vue`) renders the full admin page with two sections: Case Type Management and ZGW API Mapping.
+- Case type list view (`src/views/settings/CaseTypeList.vue`) using `CnIndexPage` -- displays title, isDraft badge (Draft/Published), processing deadline, validity period. Supports set-as-default (star icon, published-only) and delete actions.
+- Case type detail/edit view (`src/views/settings/CaseTypeDetail.vue`) with tabbed interface: General and Statuses tabs are implemented. Publish/unpublish buttons with validation errors. Save button in header.
+- General tab (`src/views/settings/tabs/GeneralTab.vue`) with fields: title, description, purpose, trigger, subject, processing deadline, service target, extension allowed/period, suspension allowed, origin, confidentiality, publication required/text, valid from/until, draft/published status.
+- Statuses tab (`src/views/settings/tabs/StatusesTab.vue`) with ordered list, drag-and-drop reorder, inline editing, add/delete, isFinal checkbox, notifyInitiator toggle with notification text field.
+- Case type CRUD via OpenRegister object store (`src/store/modules/object.js` using `createObjectStore` from `@conduction/nextcloud-vue`).
+- Default case type selection persisted via `SettingsService` (`lib/Service/SettingsService.php`, config key `default_case_type`).
+- Settings controller (`lib/Controller/SettingsController.php`) with index/create/load endpoints.
+- Register configuration auto-import from `procest_register.json` (`lib/Service/SettingsService.php::loadConfiguration`).
+- Case type admin orchestrator component (`src/views/settings/CaseTypeAdmin.vue`) managing list/detail view switching.
+- Duration formatting helpers (`src/utils/durationHelpers.js`).
+- Case type validation utilities (`src/utils/caseTypeValidation.js`).
+
+**Not yet implemented:**
+- Results tab (V1) -- result type CRUD with archival rules.
+- Roles tab (V1) -- role type CRUD with generic role mapping.
+- Properties tab (V1) -- property definition CRUD with required-at-status linking.
+- Documents tab (V1) -- document type CRUD with direction and required-at-status.
+- Publish validation: checking for at least one status type and validFrom date before publishing (partial -- UI has publish errors display but completeness checks may not cover all scenarios).
+- Delete case type blocking when active cases exist (no backend enforcement found).
+- Concurrent editing conflict detection.
+- Keyboard alternative for drag-and-drop reorder.
+
+### Standards & References
+
+- **ZGW Catalogi API (VNG)**: The case type data model maps directly to ZaakType, StatusType, ResultaatType, RolType, EigenschapType, InformatieObjectType from the ZGW Catalogi API specification (VNG-Realisatie/catalogi-api).
+- **CMMN 1.1**: Case type modeled after CaseDefinition concept; status types correspond to CMMN Milestone sequences.
+- **Schema.org**: Properties use `schema:name`, `schema:description`, `schema:identifier` mappings.
+- **ISO 8601**: Duration format for processing deadlines, extension periods, retention periods.
+- **WCAG AA**: Spec requires accessible form labels, keyboard alternatives for drag-and-drop, `aria-describedby` for error messages.
+- **GEMMA**: Dutch municipal architecture standards for zaakgericht werken.
+
+### Specificity Assessment
+
+This spec is highly specific and implementation-ready. Requirements are well-structured with concrete scenarios, data tables, and validation rules.
+
+**Strengths:** Detailed Gherkin scenarios covering happy paths and error cases. Clear feature tier separation (MVP vs V1). Explicit field definitions with types.
+
+**Missing/Ambiguous:**
+- No API endpoint definitions (REST paths, request/response schemas) -- relies on OpenRegister generic CRUD.
+- Publish validation logic not fully specified at the backend level (controller vs service layer responsibility).
+- Archival rules for result types reference `retentionDateSource` options but do not define their semantics in detail (e.g., what "custom_property" or "related_case" means concretely).
+- No specification of how V1 tabs become available (feature flag, config, or automatic based on version).
+- Decision types (REQ-CT-11 in case-types spec) are mentioned in the data model but not in the admin-settings spec tabs.
+
+**Open questions:**
+1. Should the admin settings enforce backend validation (server-side) or is frontend validation sufficient for MVP?
+2. How should the system handle case type versioning -- can a published case type be edited, or must it be unpublished first?
+3. Should delete of status types cascade to status records on existing cases?
diff --git a/openspec/specs/base-register-seed-data/spec.md b/openspec/specs/base-register-seed-data/spec.md
index 62bda820..229ebd31 100644
--- a/openspec/specs/base-register-seed-data/spec.md
+++ b/openspec/specs/base-register-seed-data/spec.md
@@ -1,835 +1,835 @@
-# Base Register Seed Data Specification
-
-## Purpose
-
-Define mock/test register JSON files for five Dutch base registrations (BRP, KVK, BAG, DSO, ORI) with realistic seed data that enables full-cycle testing and demos of Procest (case management) and Pipelinq (CRM) features without external API access. These registers supplement the existing `procest_register.json` and `pipelinq_register.json` by providing the government data layer that these apps query during citizen/business identification, case enrichment, address resolution, permit intake, and council information display.
-
-**Relationship to existing specs**: This spec extends `openregister/openspec/specs/mock-registers/spec.md` (which defines BRP and KVK requirements) by adding BAG, DSO, and ORI registers, specifying cross-register relationships, and defining concrete seed data scenarios tied to Procest and Pipelinq test cases.
-
-**Consuming specs**:
-- Procest `case-dashboard-view` (REQ-CDV-05b): BRP-persoon and BAG-object as linked objects
-- Procest `vth-module` (REQ-VTH-01): DSO vergunningaanvraag intake with BAG locatie
-- Procest `zaak-intake-flow`: Betrokkene identification via BRP/KVK
-- Procest `legesberekening`: BAG oppervlakte for fee calculation
-- Pipelinq `klantbeeld-360`: BRP/KVK enrichment for 360-degree customer view
-- Pipelinq `kcc-werkplek`: BSN/KVK citizen/business identification
-- Pipelinq `prospect-discovery`: KVK data for prospect search and scoring
-
-**Feature tier**: MVP (BRP + KVK + BAG), V1 (DSO + ORI)
-
----
-
-## File Structure
-
-```
-procest/lib/Settings/
- procest_register.json -- existing app register (unchanged)
- brp_register.json -- BRP (persons)
- kvk_register.json -- KVK (businesses)
- bag_register.json -- BAG (addresses/buildings)
- dso_register.json -- DSO (permits/environment)
- ori_register.json -- ORI (council information)
-```
-
-Each file follows the OpenRegister JSON format: OpenAPI 3.0 envelope with `x-openregister` metadata, `components.registers` (register definition), `components.schemas` (entity schemas), and `components.objects` (seed data). The repair step (`InitializeSettings`) loads each file via `SettingsService::loadConfiguration()`.
-
----
-
-## REQ-SEED-001: BRP Register (Basisregistratie Personen)
-
-**Feature tier**: MVP
-
-The system MUST provide a `brp_register.json` file containing a BRP register with an `ingeschrevenPersoon` schema and at least 25 fictional person records.
-
-### Register Definition
-
-| Field | Value |
-|-------|-------|
-| slug | `brp` |
-| title | `BRP (Basisregistratie Personen)` |
-| version | `1.0.0` |
-| description | `Mock BRP register for development and testing. Contains fictional persons aligned with the Haal Centraal BRP Personen Bevragen API v2 response structure. Authority: RVIG (Rijksdienst voor Identiteitsgegevens).` |
-| tablePrefix | (empty) |
-| folder | `Open Registers/BRP` |
-| schemas | `["ingeschrevenPersoon"]` |
-
-### Schema: `ingeschrevenPersoon`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `burgerservicenummer` | string (9 digits) | yes | no | BSN, MUST pass 11-proef validation | `"999993653"` |
-| `voornamen` | string | yes | no | First names | `"Jan Albert"` |
-| `voorletters` | string | no | no | Initials | `"J.A."` |
-| `voorvoegsel` | string | no | no | Name prefix (tussenvoegsel) | `"de"` |
-| `geslachtsnaam` | string | yes | yes | Family name | `"Vries"` |
-| `aanhef` | string | no | no | Form of address | `"De heer"` |
-| `geslachtsaanduiding` | string (enum) | yes | yes | Gender: `man`, `vrouw`, `onbekend` | `"man"` |
-| `geboortedatum` | string (date) | yes | no | Date of birth (YYYY-MM-DD) | `"1985-03-15"` |
-| `geboorteplaats` | string | no | no | Place of birth | `"Amsterdam"` |
-| `geboorteland` | string | no | no | Country of birth (code table) | `"Nederland"` |
-| `overlijdensdatum` | string (date) | no | no | Date of death (null if alive) | `null` |
-| `verblijfplaatsStraat` | string | no | no | Street name | `"Keizersgracht"` |
-| `verblijfplaatsHuisnummer` | integer | no | no | House number | `100` |
-| `verblijfplaatsHuisletter` | string | no | no | House letter | `"A"` |
-| `verblijfplaatsHuisnummertoevoeging` | string | no | no | House number suffix | `"bis"` |
-| `verblijfplaatsPostcode` | string | no | no | Postal code (####XX) | `"1015AA"` |
-| `verblijfplaatsWoonplaats` | string | no | yes | City | `"Amsterdam"` |
-| `verblijfplaatsGemeente` | string | no | yes | Municipality of registration | `"Amsterdam"` |
-| `nationaliteit` | string | no | yes | Nationality | `"Nederlandse"` |
-| `burgerlijkeStaat` | string (enum) | no | yes | Marital status: `ongehuwd`, `gehuwd`, `gescheiden`, `weduwe/weduwnaar`, `partnerschap` | `"gehuwd"` |
-| `partnerBsn` | string | no | no | BSN of partner (cross-ref within register) | `"999990019"` |
-| `partnerNaam` | string | no | no | Full name of partner | `"Maria Bakker"` |
-| `kinderen` | array of objects | no | no | Children `[{bsn, naam}]` | `[{"bsn":"999990020","naam":"Sophie de Vries"}]` |
-| `ouders` | array of objects | no | no | Parents `[{bsn, naam}]` | `[{"bsn":"999990001","naam":"Pieter de Vries"}]` |
-| `datumInschrijving` | string (date) | no | no | Registration date in municipality | `"2010-06-01"` |
-
-**Design notes**:
-- The flat property structure (e.g., `verblijfplaatsStraat` instead of nested `verblijfplaats.straat`) matches how OpenRegister stores object properties in the JSON column. Nested objects can be used but flat is simpler for faceting and search.
-- The `partner`, `kinderen`, and `ouders` references use BSN strings that can be resolved within the same register, enabling cross-referencing without requiring UUID joins.
-
-#### Scenario SEED-001a: BSN 11-proef validation
-
-- GIVEN a seed person with `burgerservicenummer` value `"999993653"`
-- WHEN the weighted checksum is calculated: `(9*9 + 9*8 + 9*7 + 9*6 + 9*5 + 3*4 + 6*3 + 5*2 - 3*1)`
-- THEN the result MUST be divisible by 11
-- AND all 25+ seed BSNs MUST pass the 11-proef
-- AND all BSNs MUST start with `9999` (the known-fictional BSN range used by RVIG for testing)
-
-#### Scenario SEED-001b: Family unit consistency
-
-- GIVEN the seed data contains the De Vries family:
- - Jan Albert de Vries (BSN 999993653, born 1985-03-15, man, gehuwd)
- - Maria Bakker-de Vries (BSN 999990019, born 1987-11-22, vrouw, gehuwd)
- - Sophie de Vries (BSN 999990020, born 2015-06-10, vrouw, ongehuwd)
- - Thomas de Vries (BSN 999990021, born 2018-09-03, man, ongehuwd)
-- THEN Jan's `partnerBsn` MUST equal Maria's BSN and vice versa
-- AND Jan's `kinderen` MUST list Sophie and Thomas
-- AND Sophie's `ouders` MUST list Jan and Maria
-- AND all four MUST share the same `verblijfplaatsStraat`, `verblijfplaatsHuisnummer`, `verblijfplaatsPostcode`
-
-#### Scenario SEED-001c: Geographic distribution
-
-- GIVEN the 25+ seed persons
-- THEN persons MUST be distributed across at least 5 municipalities: Amsterdam, Utrecht, Rotterdam, Den Haag, Tilburg
-- AND postcodes MUST be realistic for the specified city (e.g., Amsterdam: 10xx, Utrecht: 35xx, Rotterdam: 30xx)
-
-#### Scenario SEED-001d: Demographic diversity
-
-- GIVEN the seed data
-- THEN the following scenarios MUST be covered:
- - At least 3 married couples with children (family units)
- - At least 2 single persons (ongehuwd, no partner)
- - At least 1 divorced person (gescheiden)
- - At least 1 deceased person (overlijdensdatum set)
- - At least 1 person with non-Dutch nationality
- - At least 1 person with registered partnership (partnerschap)
- - Ages ranging from minors (under 18) to elderly (over 75)
-
-### Seed Data Requirements Summary
-
-| Scenario | Min Records | Purpose |
-|----------|-------------|---------|
-| Family with 2 children (De Vries) | 4 | Procest zaak-betrokkene linking, Pipelinq klantbeeld family view |
-| Family with 1 child (Bakker) | 3 | Second family for cross-case testing |
-| Family with 3 children (Jansen) | 5 | Large family, multi-child scenarios |
-| Single persons | 3 | Pipelinq client creation from BRP |
-| Divorced person + ex-partner | 2 | Burgerlijke staat edge case |
-| Elderly couple | 2 | Age range coverage |
-| Deceased person | 1 | Overlijden edge case |
-| Non-Dutch nationals | 2 | Nationality filter testing |
-| Registered partnership | 2 | Partnerschap scenario |
-| Business owner (also in KVK) | 1 | Cross-register: BRP person = KVK eigenaar |
-| **Total minimum** | **25** | |
-
----
-
-## REQ-SEED-002: KVK Register (Kamer van Koophandel)
-
-**Feature tier**: MVP
-
-The system MUST provide a `kvk_register.json` file containing a KVK register with a `maatschappelijkeActiviteit` schema and at least 15 fictional business records.
-
-### Register Definition
-
-| Field | Value |
-|-------|-------|
-| slug | `kvk` |
-| title | `KVK (Handelsregister)` |
-| version | `1.0.0` |
-| description | `Mock KVK register for development and testing. Contains fictional businesses aligned with the KVK Handelsregister API (Basisprofiel/Vestigingsprofiel) response structure. Authority: Kamer van Koophandel.` |
-| tablePrefix | (empty) |
-| folder | `Open Registers/KVK` |
-| schemas | `["maatschappelijkeActiviteit", "vestiging"]` |
-
-### Schema: `maatschappelijkeActiviteit`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `kvkNummer` | string (8 digits) | yes | no | KVK registration number | `"90001234"` |
-| `handelsnaam` | string | yes | yes | Primary trade name | `"Bakkerij De Vries B.V."` |
-| `handelsnamen` | array of strings | no | no | All trade names | `["Bakkerij De Vries","De Vries Patisserie"]` |
-| `rechtsvorm` | string | yes | yes | Legal form display name | `"Besloten Vennootschap"` |
-| `rechtsvormCode` | string | yes | yes | Legal form code: `BV`, `NV`, `Eenmanszaak`, `Stichting`, `VOF`, `CV`, `Cooperatie`, `Vereniging`, `Maatschap` | `"BV"` |
-| `rsin` | string (9 digits) | no | no | RSIN (Rechtspersonen en Samenwerkingsverbanden Identificatienummer) | `"123456789"` |
-| `vestigingsadresStraat` | string | no | no | Street name of main establishment | `"Prinsengracht"` |
-| `vestigingsadresHuisnummer` | integer | no | no | House number | `200` |
-| `vestigingsadresPostcode` | string | no | no | Postal code (####XX) | `"1016GS"` |
-| `vestigingsadresPlaats` | string | no | yes | City | `"Amsterdam"` |
-| `vestigingsadresProvincie` | string | no | yes | Province | `"Noord-Holland"` |
-| `sbiHoofdactiviteit` | string | yes | yes | Primary SBI code | `"1071"` |
-| `sbiHoofdactiviteitOmschrijving` | string | no | yes | Primary SBI description | `"Vervaardiging van brood en banket"` |
-| `sbiActiviteiten` | array of objects | no | no | All SBI activities `[{sbiCode, omschrijving, isHoofdactiviteit}]` | see below |
-| `aantalWerkzamePersonen` | integer | no | no | Number of employees | `25` |
-| `datumOprichting` | string (date) | no | no | Date of establishment | `"2005-09-12"` |
-| `datumUitschrijving` | string (date) | no | no | Date of deregistration (null if active) | `null` |
-| `actief` | boolean | yes | yes | Whether the business is active | `true` |
-| `eigenaarNaam` | string | no | no | Owner name (links to BRP for eenmanszaak) | `"J.A. de Vries"` |
-| `eigenaarBsn` | string | no | no | Owner BSN (cross-ref to BRP, for eenmanszaak/VOF) | `"999993653"` |
-| `website` | string (uri) | no | no | Company website | `"https://www.devries-bakkerij.nl"` |
-| `emailadres` | string (email) | no | no | Contact email | `"info@devries-bakkerij.nl"` |
-| `telefoonnummer` | string | no | no | Contact phone | `"+31 20 1234567"` |
-
-### Schema: `vestiging`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `vestigingsnummer` | string (12 digits) | yes | no | Vestiging registration number | `"000012345678"` |
-| `kvkNummer` | string (8 digits) | yes | no | Parent KVK number (cross-ref) | `"90001234"` |
-| `handelsnaam` | string | yes | yes | Trade name of this vestiging | `"Bakkerij De Vries - Filiaal Zuid"` |
-| `type` | string (enum) | yes | yes | `hoofdvestiging` or `nevenvestiging` | `"nevenvestiging"` |
-| `adresStraat` | string | no | no | Street name | `"Beethovenstraat"` |
-| `adresHuisnummer` | integer | no | no | House number | `42` |
-| `adresPostcode` | string | no | no | Postal code | `"1077JJ"` |
-| `adresPlaats` | string | no | yes | City | `"Amsterdam"` |
-| `sbiActiviteiten` | array of objects | no | no | SBI activities at this location | see parent schema |
-| `aantalWerkzamePersonen` | integer | no | no | Employees at this location | `8` |
-| `actief` | boolean | yes | yes | Whether the vestiging is active | `true` |
-
-#### Scenario SEED-002a: Legal form diversity
-
-- GIVEN the 15+ seed businesses
-- THEN the following legal forms MUST be represented:
- - BV (Besloten Vennootschap): at least 4 records
- - Eenmanszaak: at least 3 records (with `eigenaarBsn` linking to BRP persons)
- - Stichting: at least 2 records
- - VOF (Vennootschap onder Firma): at least 1 record
- - NV (Naamloze Vennootschap): at least 1 record
- - Vereniging: at least 1 record
-- AND at least 1 business MUST have `actief: false` with `datumUitschrijving` set
-
-#### Scenario SEED-002b: SBI code diversity
-
-- GIVEN the seed businesses
-- THEN businesses MUST cover at least 8 different SBI top-level sections:
- - A (Landbouw): e.g., `"0111"` Akkerbouw
- - C (Industrie): e.g., `"1071"` Brood en banket
- - F (Bouw): e.g., `"4120"` Algemene burgerlijke en utiliteitsbouw
- - G (Handel): e.g., `"4711"` Supermarkten
- - I (Horeca): e.g., `"5610"` Restaurants
- - J (Informatie/communicatie): e.g., `"6201"` Ontwikkelen en produceren van software
- - M (Advisering): e.g., `"6920"` Accountancy en belastingadvies
- - Q (Zorg): e.g., `"8610"` Ziekenhuizen
-
-#### Scenario SEED-002c: Cross-register BRP linkage
-
-- GIVEN BRP person "Jan Albert de Vries" (BSN 999993653) is a business owner
-- WHEN the KVK seed data includes an eenmanszaak "De Vries Consultancy"
-- THEN `eigenaarBsn` MUST equal `"999993653"`
-- AND `eigenaarNaam` MUST equal `"J.A. de Vries"`
-- AND `vestigingsadresStraat` + `vestigingsadresPostcode` SHOULD match Jan's BRP `verblijfplaatsStraat` + `verblijfplaatsPostcode` (common for eenmanszaak)
-
-#### Scenario SEED-002d: Business with multiple vestigingen
-
-- GIVEN seed business "Bakkerij De Vries B.V." (KVK 90001234)
-- THEN at least 2 vestiging records MUST exist:
- - Hoofdvestiging: Prinsengracht 200, Amsterdam
- - Nevenvestiging: Beethovenstraat 42, Amsterdam
-- AND both vestigingen MUST reference the same `kvkNummer`
-
-### Seed Data Requirements Summary
-
-| Scenario | Min Records | Purpose |
-|----------|-------------|---------|
-| BV businesses (various sectors) | 4 | Pipelinq client management, prospect discovery |
-| Eenmanszaak (with BRP link) | 3 | Cross-register testing, KCC identification |
-| Stichtingen | 2 | Non-profit sector testing |
-| VOF | 1 | Multi-owner business |
-| NV | 1 | Large corporation scenario |
-| Vereniging | 1 | Community organization |
-| Inactive business | 1 | Deregistered edge case |
-| Multi-vestiging business | 1 (+2 vestigingen) | Vestiging search in Pipelinq |
-| IT/software company | 1 | Pipelinq SBI filter testing |
-| **Total minimum maatschappelijkeActiviteit** | **15** | |
-| **Total minimum vestiging** | **18** | (15 hoofd + 3 neven) |
-
----
-
-## REQ-SEED-003: BAG Register (Basisregistratie Adressen en Gebouwen)
-
-**Feature tier**: MVP
-
-The system MUST provide a `bag_register.json` file containing a BAG register with schemas for `nummeraanduiding`, `openbareRuimte`, `woonplaats`, `verblijfsobject`, and `pand`, with seed data that matches the addresses used in BRP and KVK seed data.
-
-### Register Definition
-
-| Field | Value |
-|-------|-------|
-| slug | `bag` |
-| title | `BAG (Basisregistratie Adressen en Gebouwen)` |
-| version | `1.0.0` |
-| description | `Mock BAG register for development and testing. Contains fictional addresses and buildings aligned with the BAG API Individuele Bevragingen v2 response structure. Authority: Kadaster.` |
-| tablePrefix | (empty) |
-| folder | `Open Registers/BAG` |
-| schemas | `["nummeraanduiding", "openbareRuimte", "woonplaats", "verblijfsobject", "pand"]` |
-
-### Schema: `nummeraanduiding`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363200000000001"` |
-| `huisnummer` | integer | yes | no | House number | `100` |
-| `huisletter` | string | no | no | House letter | `"A"` |
-| `huisnummertoevoeging` | string | no | no | House number suffix | `"bis"` |
-| `postcode` | string | yes | yes | Postal code (####XX) | `"1015AA"` |
-| `status` | string (enum) | yes | yes | `naamgeving uitgegeven`, `naamgeving ingetrokken` | `"naamgeving uitgegeven"` |
-| `typeAdresseerbaarObject` | string (enum) | no | yes | `Verblijfsobject`, `Standplaats`, `Ligplaats` | `"Verblijfsobject"` |
-| `openbareRuimteNaam` | string | yes | no | Street name (denormalized for search) | `"Keizersgracht"` |
-| `woonplaatsNaam` | string | yes | yes | City name (denormalized for search) | `"Amsterdam"` |
-| `openbareRuimteId` | string | no | no | Reference to openbareRuimte | `"0363300000000001"` |
-| `verblijfsobjectId` | string | no | no | Reference to verblijfsobject | `"0363010000000001"` |
-
-### Schema: `openbareRuimte`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363300000000001"` |
-| `naam` | string | yes | yes | Street/public space name | `"Keizersgracht"` |
-| `type` | string (enum) | yes | yes | `Weg`, `Water`, `Spoorbaan`, `Terrein`, `Kunstwerk`, `Landschappelijk gebied`, `Administratief gebied` | `"Weg"` |
-| `status` | string (enum) | yes | yes | `naamgeving uitgegeven`, `naamgeving ingetrokken` | `"naamgeving uitgegeven"` |
-| `woonplaatsNaam` | string | yes | yes | City name | `"Amsterdam"` |
-| `woonplaatsId` | string | no | no | Reference to woonplaats | `"3594"` |
-
-### Schema: `woonplaats`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `identificatie` | string (4 digits) | yes | no | Woonplaats code | `"3594"` |
-| `naam` | string | yes | yes | City/town name | `"Amsterdam"` |
-| `status` | string (enum) | yes | yes | `woonplaats aangewezen`, `woonplaats ingetrokken` | `"woonplaats aangewezen"` |
-| `gemeente` | string | no | yes | Municipality name | `"Amsterdam"` |
-| `provincie` | string | no | yes | Province name | `"Noord-Holland"` |
-
-### Schema: `verblijfsobject`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363010000000001"` |
-| `status` | string (enum) | yes | yes | `verblijfsobject gevormd`, `verblijfsobject in gebruik (niet ingemeten)`, `verblijfsobject in gebruik`, `verblijfsobject ingetrokken`, `verblijfsobject buiten gebruik` | `"verblijfsobject in gebruik"` |
-| `gebruiksdoel` | string (enum) | yes | yes | `woonfunctie`, `bijeenkomstfunctie`, `celfunctie`, `gezondheidszorgfunctie`, `industriefunctie`, `kantoorfunctie`, `logiesfunctie`, `onderwijsfunctie`, `sportfunctie`, `winkelfunctie`, `overige gebruiksfunctie` | `"woonfunctie"` |
-| `gebruiksdoelen` | array of strings | no | no | Multiple use purposes | `["woonfunctie"]` |
-| `oppervlakte` | integer | yes | no | Usable surface area in m2 | `120` |
-| `pandId` | string | no | no | Reference to pand | `"0363100000000001"` |
-| `nummeraanduidingId` | string | no | no | Reference to main nummeraanduiding | `"0363200000000001"` |
-| `bouwjaar` | integer | no | no | Construction year (from pand, denormalized) | `1895` |
-
-### Schema: `pand`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363100000000001"` |
-| `status` | string (enum) | yes | yes | `bouwvergunning verleend`, `bouw gestart`, `pand in gebruik (niet ingemeten)`, `pand in gebruik`, `sloopvergunning verleend`, `pand gesloopt`, `pand buiten gebruik`, `niet gerealiseerd pand`, `verbouwing pand` | `"pand in gebruik"` |
-| `oorspronkelijkBouwjaar` | integer | yes | no | Original construction year | `1895` |
-| `oppervlakte` | integer | no | no | Gross surface area in m2 | `450` |
-
-#### Scenario SEED-003a: BAG addresses match BRP persons
-
-- GIVEN BRP person Jan de Vries lives at Keizersgracht 100A, 1015AA Amsterdam
-- THEN the BAG MUST contain:
- - A `woonplaats` record for Amsterdam (identificatie `"3594"`)
- - An `openbareRuimte` record for Keizersgracht in Amsterdam
- - A `nummeraanduiding` with huisnummer 100, huisletter A, postcode 1015AA
- - A `verblijfsobject` with `gebruiksdoel` = `"woonfunctie"`, linked to a `pand`
- - A `pand` with `oorspronkelijkBouwjaar` and `status` = `"pand in gebruik"`
-
-#### Scenario SEED-003b: BAG addresses match KVK businesses
-
-- GIVEN KVK business "Bakkerij De Vries B.V." at Prinsengracht 200, 1016GS Amsterdam
-- THEN the BAG MUST contain corresponding `nummeraanduiding`, `openbareRuimte`, `verblijfsobject` (gebruiksdoel `"winkelfunctie"`), and `pand` records
-- AND the BAG address components MUST be consistent: `nummeraanduiding.openbareRuimteNaam` = the openbareRuimte name, `nummeraanduiding.woonplaatsNaam` = the woonplaats name
-
-#### Scenario SEED-003c: Address for DSO vergunningaanvraag
-
-- GIVEN DSO vergunningaanvraag for a building project at Herengracht 300, 1016CE Amsterdam
-- THEN the BAG MUST contain the corresponding address records
-- AND the `pand` SHOULD have `status` = `"verbouwing pand"` to represent an ongoing building project
-- AND the `verblijfsobject` MUST have `oppervlakte` set (used in legesberekening)
-
-#### Scenario SEED-003d: Multiple residents at one address
-
-- GIVEN the Jansen family (5 persons) lives at Maliebaan 50, 3581CS Utrecht
-- THEN ONE `nummeraanduiding` record MUST exist for that address
-- AND the `verblijfsobject` `gebruiksdoel` MUST be `"woonfunctie"`
-- AND all 5 BRP persons MUST reference the same address (postcode + huisnummer + straat + woonplaats)
-
-### Seed Data Requirements Summary
-
-| Entity | Min Records | Notes |
-|--------|-------------|-------|
-| woonplaats | 5 | Amsterdam, Utrecht, Rotterdam, Den Haag, Tilburg |
-| openbareRuimte | 20 | Streets matching BRP/KVK addresses |
-| nummeraanduiding | 35 | All BRP + KVK addresses (deduplicated) |
-| verblijfsobject | 35 | One per nummeraanduiding |
-| pand | 30 | Some shared (apartment buildings) |
-
----
-
-## REQ-SEED-004: DSO Register (Digitaal Stelsel Omgevingswet)
-
-**Feature tier**: V1
-
-The system MUST provide a `dso_register.json` file containing a DSO register with schemas for `vergunningaanvraag` and `activiteit`, with seed data representing permit applications in the Omgevingswet domain.
-
-### Register Definition
-
-| Field | Value |
-|-------|-------|
-| slug | `dso` |
-| title | `DSO (Digitaal Stelsel Omgevingswet)` |
-| version | `1.0.0` |
-| description | `Mock DSO register for development and testing. Contains fictional permit applications aligned with the STAM/IMAM (Standaard Aanvragen en Meldingen / Informatiemodel Aanvragen en Meldingen) standard. Authority: Ministerie van BZK via IPLO.` |
-| tablePrefix | (empty) |
-| folder | `Open Registers/DSO` |
-| schemas | `["vergunningaanvraag", "activiteit"]` |
-
-### Schema: `vergunningaanvraag`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `zaaknummer` | string | yes | no | DSO case reference number | `"OLO-2026-00001"` |
-| `aanvraagdatum` | string (date) | yes | no | Date of application | `"2026-01-15"` |
-| `procedureType` | string (enum) | yes | yes | `regulier` (8 wk), `uitgebreid` (26 wk) | `"regulier"` |
-| `omschrijving` | string | yes | no | Description of the project | `"Verbouwing woonhuis tot kantoor"` |
-| `locatieAdres` | string | no | no | Address of the project (display) | `"Herengracht 300, 1016CE Amsterdam"` |
-| `locatiePostcode` | string | no | yes | Postcode of the project location | `"1016CE"` |
-| `locatiePlaats` | string | no | yes | City of the project location | `"Amsterdam"` |
-| `locatieBagId` | string | no | no | BAG nummeraanduiding identificatie (cross-ref) | `"0363200000000010"` |
-| `locatieKadastraalPerceel` | string | no | no | Cadastral parcel identifier | `"ASD04-F-1234"` |
-| `initiatiefnemerNaam` | string | yes | no | Applicant name | `"Petra Jansen"` |
-| `initiatiefnemerBsn` | string | no | no | Applicant BSN (cross-ref to BRP) | `"999990027"` |
-| `initiatiefnemerKvk` | string | no | no | Applicant KVK number (cross-ref, if business) | `"90001234"` |
-| `gemachtigdeNaam` | string | no | no | Authorized representative name | `"Architectenbureau Van Dam B.V."` |
-| `bouwkosten` | number | no | no | Estimated construction costs in EUR | `180000` |
-| `oppervlakte` | integer | no | no | Area in m2 | `250` |
-| `activiteiten` | array of strings | no | no | List of activities from the application | `["Bouwen","Kappen","Uitrit aanleggen"]` |
-| `status` | string (enum) | yes | yes | `ingediend`, `ontvankelijk`, `in_behandeling`, `besluit_genomen`, `verleend`, `geweigerd`, `ingetrokken`, `buiten_behandeling` | `"ingediend"` |
-| `besluitdatum` | string (date) | no | no | Date of decision | `null` |
-| `resultaat` | string (enum) | no | yes | `verleend`, `geweigerd`, `deels_verleend` | `null` |
-
-### Schema: `activiteit`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `naam` | string | yes | yes | Activity name from Omgevingswet | `"Bouwen van een bouwwerk"` |
-| `code` | string | yes | no | DSO activity code | `"BOUWEN-001"` |
-| `categorie` | string | yes | yes | `bouwactiviteit`, `milieubelastende activiteit`, `omgevingsplanactiviteit`, `Natura 2000-activiteit`, `ontgrondingsactiviteit` | `"bouwactiviteit"` |
-| `regelgevingType` | string | no | yes | `vergunningplicht`, `meldingsplicht`, `informatieplicht` | `"vergunningplicht"` |
-| `bevoegdGezag` | string | no | yes | Competent authority type | `"gemeente"` |
-| `omschrijving` | string | no | no | Detailed description of the activity | `"Het bouwen van een bouwwerk waarvoor een omgevingsvergunning vereist is"` |
-
-#### Scenario SEED-004a: Bouwvergunning linked to BAG
-
-- GIVEN a vergunningaanvraag for "Verbouwing woonhuis" at Herengracht 300
-- THEN `locatieBagId` MUST reference a valid BAG `nummeraanduiding` in the BAG seed data
-- AND the `locatieAdres` MUST match the BAG address components
-- AND `initiatiefnemerBsn` MUST reference a valid BRP person
-
-#### Scenario SEED-004b: Multiple activities in one application
-
-- GIVEN a vergunningaanvraag with `activiteiten: ["Bouwen","Kappen","Uitrit aanleggen"]`
-- THEN 3 corresponding `activiteit` records MUST exist in the DSO register
-- AND the `vergunningaanvraag` links to these activities by name
-
-#### Scenario SEED-004c: Various permit types
-
-- GIVEN the seed data
-- THEN the following application types MUST be represented:
- - Bouwvergunning (bouwen van een bouwwerk): reguliere procedure
- - Milieuvergunning (milieubelastende activiteit): uitgebreide procedure
- - Kapvergunning (vellen van houtopstand): reguliere procedure
- - Omgevingsplanactiviteit (afwijken van omgevingsplan): reguliere procedure
- - Combined application (samenloop): multiple activities in one aanvraag
-- AND at least 1 application MUST have `status` = `"verleend"` with `besluitdatum` set
-- AND at least 1 application MUST have `status` = `"geweigerd"`
-
-### Seed Data Requirements Summary
-
-| Entity | Min Records | Notes |
-|--------|-------------|-------|
-| vergunningaanvraag | 8 | Various types, statuses, and locations |
-| activiteit | 12 | Standard Omgevingswet activities |
-
----
-
-## REQ-SEED-005: ORI Register (Open Raadsinformatie)
-
-**Feature tier**: V1
-
-The system MUST provide an `ori_register.json` file containing an ORI register with schemas for council meetings, agenda items, motions, votes, council members, and factions, with seed data representing a fictional municipal council.
-
-### Register Definition
-
-| Field | Value |
-|-------|-------|
-| slug | `ori` |
-| title | `ORI (Open Raadsinformatie)` |
-| version | `1.0.0` |
-| description | `Mock ORI register for development and testing. Contains fictional council proceedings aligned with the Popolo data standard and Open State Foundation ORI API conventions. Authority: gemeenteraad (municipal council).` |
-| tablePrefix | (empty) |
-| folder | `Open Registers/ORI` |
-| schemas | `["vergadering", "agendapunt", "document", "motie", "amendement", "stemming", "raadslid", "fractie"]` |
-
-### Schema: `vergadering`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `naam` | string | yes | no | Meeting name | `"Raadsvergadering 15 januari 2026"` |
-| `type` | string (enum) | yes | yes | `raadsvergadering`, `commissievergadering`, `informatieavond`, `presidium` | `"raadsvergadering"` |
-| `commissie` | string | no | yes | Committee name (if commissievergadering) | `"Commissie Ruimte en Wonen"` |
-| `startDatum` | string (date-time) | yes | no | Start date/time | `"2026-01-15T19:30:00+01:00"` |
-| `eindDatum` | string (date-time) | no | no | End date/time | `"2026-01-15T23:15:00+01:00"` |
-| `locatie` | string | no | no | Physical location | `"Raadzaal, Stadhuis"` |
-| `status` | string (enum) | yes | yes | `gepland`, `bevestigd`, `afgelopen`, `geannuleerd` | `"afgelopen"` |
-| `voorzitter` | string | no | no | Chair name | `"Burgemeester Van den Berg"` |
-
-### Schema: `agendapunt`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `titel` | string | yes | no | Agenda item title | `"Vaststelling bestemmingsplan Centrum-Oost"` |
-| `vergaderingId` | string (uuid) | yes | no | Reference to vergadering | (uuid) |
-| `volgorde` | integer | yes | no | Order on agenda | `3` |
-| `type` | string (enum) | yes | yes | `bespreekstuk`, `hamerstuk`, `informerend`, `procedureel` | `"bespreekstuk"` |
-| `portefeuille` | string | no | yes | Portfolio/department | `"Ruimtelijke Ordening"` |
-| `resultaat` | string | no | yes | Outcome | `"Aangenomen"` |
-
-### Schema: `document`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `titel` | string | yes | no | Document title | `"Raadsvoorstel vaststelling bestemmingsplan"` |
-| `type` | string (enum) | yes | yes | `raadsvoorstel`, `raadsbesluit`, `amendement`, `motie`, `brief`, `nota`, `verslag`, `bijlage` | `"raadsvoorstel"` |
-| `agendapuntId` | string (uuid) | no | no | Reference to agendapunt | (uuid) |
-| `datum` | string (date) | yes | no | Document date | `"2026-01-08"` |
-| `bestandsnaam` | string | no | no | File name | `"RV-2026-001-bestemmingsplan.pdf"` |
-| `samenvatting` | string | no | no | Summary | `"Voorstel tot vaststelling van het bestemmingsplan Centrum-Oost"` |
-
-### Schema: `motie`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `titel` | string | yes | no | Motion title | `"Motie vreemd: Meer groen in de binnenstad"` |
-| `agendapuntId` | string (uuid) | no | no | Reference to agenda item (null for motie vreemd) | (uuid or null) |
-| `indieners` | array of strings | yes | no | Submitting faction names | `["GroenLinks","D66"]` |
-| `dictum` | string | yes | no | The actual request/instruction | `"Verzoekt het college om binnen 6 maanden een groenplan op te stellen voor de binnenstad"` |
-| `datumIndiening` | string (date) | yes | no | Date of submission | `"2026-01-15"` |
-| `status` | string (enum) | yes | yes | `ingediend`, `aangenomen`, `verworpen`, `ingetrokken`, `aangehouden` | `"aangenomen"` |
-| `voorStemmen` | integer | no | no | Votes in favor | `22` |
-| `tegenStemmen` | integer | no | no | Votes against | `15` |
-
-### Schema: `amendement`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `titel` | string | yes | no | Amendment title | `"Amendement: Maximale bouwhoogte 25 meter"` |
-| `agendapuntId` | string (uuid) | yes | no | Reference to agenda item | (uuid) |
-| `indieners` | array of strings | yes | no | Submitting faction names | `["SP","PvdA"]` |
-| `wijziging` | string | yes | no | Proposed change text | `"Wijzigt artikel 3.2: maximale bouwhoogte van 30 naar 25 meter"` |
-| `toelichting` | string | no | no | Explanation | `"Om het historische straatbeeld te beschermen"` |
-| `datumIndiening` | string (date) | yes | no | Date of submission | `"2026-01-15"` |
-| `status` | string (enum) | yes | yes | `ingediend`, `aangenomen`, `verworpen`, `ingetrokken` | `"verworpen"` |
-| `voorStemmen` | integer | no | no | Votes in favor | `14` |
-| `tegenStemmen` | integer | no | no | Votes against | `23` |
-
-### Schema: `stemming`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `onderwerp` | string | yes | no | What is being voted on | `"Motie: Meer groen in de binnenstad"` |
-| `type` | string (enum) | yes | yes | `motie`, `amendement`, `raadsvoorstel`, `benoeming` | `"motie"` |
-| `vergaderingId` | string (uuid) | yes | no | Reference to vergadering | (uuid) |
-| `datum` | string (date) | yes | no | Vote date | `"2026-01-15"` |
-| `resultaat` | string (enum) | yes | yes | `aangenomen`, `verworpen` | `"aangenomen"` |
-| `voorStemmen` | integer | yes | no | Votes in favor | `22` |
-| `tegenStemmen` | integer | yes | no | Votes against | `15` |
-| `onthouding` | integer | no | no | Abstentions | `0` |
-| `stemmenPerFractie` | array of objects | no | no | `[{fractie, stem, aantalLeden}]` | see below |
-
-### Schema: `raadslid`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `naam` | string | yes | yes | Full name | `"Ahmed El Amrani"` |
-| `fractie` | string | yes | yes | Faction name | `"GroenLinks"` |
-| `functie` | string | no | yes | Role: `raadslid`, `fractievoorzitter`, `wethouder`, `burgemeester` | `"raadslid"` |
-| `email` | string (email) | no | no | Council email | `"a.elamrani@gemeenteraad.nl"` |
-| `actief` | boolean | yes | yes | Currently serving | `true` |
-| `startdatum` | string (date) | no | no | Start of term | `"2022-03-30"` |
-| `einddatum` | string (date) | no | no | End of term (null if current) | `null` |
-| `portefeuilles` | array of strings | no | no | Portfolio areas | `["Duurzaamheid","Groen"]` |
-
-### Schema: `fractie`
-
-| Property | Type | Required | Facetable | Description | Example |
-|----------|------|----------|-----------|-------------|---------|
-| `naam` | string | yes | yes | Faction/party name | `"GroenLinks"` |
-| `afkorting` | string | no | yes | Abbreviation | `"GL"` |
-| `aantalZetels` | integer | yes | no | Number of seats | `7` |
-| `coalitie` | boolean | yes | yes | Part of the coalition | `true` |
-| `fractievoorzitter` | string | no | no | Chair name | `"Ahmed El Amrani"` |
-
-#### Scenario SEED-005a: Complete council composition
-
-- GIVEN the seed data
-- THEN at least 7 fracties MUST exist representing a realistic Dutch council composition:
- - VVD (6 zetels, coalitie)
- - GroenLinks (7 zetels, coalitie)
- - D66 (5 zetels, coalitie)
- - PvdA (4 zetels, oppositie)
- - CDA (3 zetels, oppositie)
- - SP (3 zetels, oppositie)
- - Lokaal Belang (2 zetels, oppositie)
-- AND at least 30 raadslid records MUST exist (sum of all zetels)
-- AND each raadslid MUST reference a valid fractie name
-
-#### Scenario SEED-005b: Council meeting with full proceedings
-
-- GIVEN a raadsvergadering "Raadsvergadering 15 januari 2026"
-- THEN the meeting MUST have at least 8 agendapunten
-- AND at least 2 moties MUST be linked (1 aangenomen, 1 verworpen)
-- AND at least 1 amendement MUST be linked
-- AND at least 3 stemmingen MUST be recorded with `stemmenPerFractie` data
-- AND at least 5 documenten MUST be linked to various agendapunten
-
-#### Scenario SEED-005c: Committee meeting
-
-- GIVEN the seed data
-- THEN at least 1 commissievergadering MUST exist (e.g., "Commissie Ruimte en Wonen")
-- AND the committee meeting MUST have at least 3 agendapunten of type `bespreekstuk` or `informerend`
-
-### Seed Data Requirements Summary
-
-| Entity | Min Records | Notes |
-|--------|-------------|-------|
-| fractie | 7 | Realistic Dutch council composition |
-| raadslid | 30 | All council members across factions |
-| vergadering | 3 | 2 raadsvergaderingen + 1 commissie |
-| agendapunt | 15 | Across all meetings |
-| document | 20 | Raadsvoorstellen, besluiten, bijlagen |
-| motie | 4 | Various statuses |
-| amendement | 2 | Aangenomen + verworpen |
-| stemming | 6 | With fractie-level detail |
-
----
-
-## REQ-SEED-006: Cross-Register Relationship Integrity
-
-**Feature tier**: MVP
-
-All cross-register references between seed data MUST be consistent and resolvable.
-
-#### Scenario SEED-006a: BRP persons live at BAG addresses
-
-- GIVEN BRP person "Jan de Vries" with `verblijfplaatsStraat` = `"Keizersgracht"`, `verblijfplaatsHuisnummer` = `100`, `verblijfplaatsPostcode` = `"1015AA"`, `verblijfplaatsWoonplaats` = `"Amsterdam"`
-- THEN the BAG register MUST contain:
- - A `nummeraanduiding` with matching `openbareRuimteNaam`, `huisnummer`, `postcode`, `woonplaatsNaam`
- - A `verblijfsobject` linked to that nummeraanduiding with `gebruiksdoel` = `"woonfunctie"`
-- AND this mapping MUST hold for ALL BRP person addresses
-
-#### Scenario SEED-006b: KVK businesses have BAG vestigingsadressen
-
-- GIVEN KVK business "Bakkerij De Vries B.V." at Prinsengracht 200, 1016GS Amsterdam
-- THEN the BAG register MUST contain a `nummeraanduiding` + `verblijfsobject` at that address
-- AND the `verblijfsobject.gebruiksdoel` MUST be appropriate for the business type (e.g., `"winkelfunctie"` for a bakery, `"kantoorfunctie"` for a consultancy)
-
-#### Scenario SEED-006c: DSO applications reference BAG and BRP
-
-- GIVEN DSO vergunningaanvraag at Herengracht 300
-- THEN `locatieBagId` MUST reference an existing BAG `nummeraanduiding.identificatie`
-- AND `initiatiefnemerBsn` MUST reference an existing BRP `ingeschrevenPersoon.burgerservicenummer`
-
-#### Scenario SEED-006d: Eenmanszaak owners link BRP to KVK
-
-- GIVEN KVK eenmanszaak "De Vries Consultancy" with `eigenaarBsn` = `"999993653"`
-- THEN BRP person with BSN `"999993653"` MUST exist
-- AND the business `vestigingsadresStraat`/`vestigingsadresPostcode` SHOULD match the BRP person's `verblijfplaatsStraat`/`verblijfplaatsPostcode` (typical for eenmanszaak)
-
-#### Scenario SEED-006e: Procest cases can reference all registers
-
-- GIVEN a Procest case of type "Omgevingsvergunning" created from seed data
-- THEN the case SHOULD be linkable to:
- - A BRP person as `betrokkene` (aanvrager) via BSN
- - A BAG address as `zaakobject` via nummeraanduiding ID
- - A DSO vergunningaanvraag as source via zaaknummer
- - An ORI agendapunt (optional, for politically sensitive cases)
-
-#### Scenario SEED-006f: Pipelinq clients map to KVK
-
-- GIVEN a Pipelinq client of type `"organization"` with a KVK number
-- THEN the KVK number MUST match a `maatschappelijkeActiviteit.kvkNummer` in the KVK seed data
-- AND the client `address` SHOULD match the KVK `vestigingsadresStraat` + `vestigingsadresPlaats`
-
----
-
-## REQ-SEED-007: Seed Data Loading
-
-**Feature tier**: MVP
-
-The register JSON files MUST be loadable by the existing OpenRegister configuration mechanism.
-
-#### Scenario SEED-007a: Auto-load on app install
-
-- GIVEN the `brp_register.json`, `kvk_register.json`, `bag_register.json` files exist in `procest/lib/Settings/`
-- WHEN the Procest repair step runs (app install or update)
-- THEN the `SettingsService::loadConfiguration()` method MUST load each register file
-- AND registers, schemas, and seed objects MUST be created in OpenRegister
-- AND seed objects MUST be created from the `components.objects` array in each file
-
-#### Scenario SEED-007b: Skip if already populated
-
-- GIVEN the BRP register already contains person objects
-- WHEN the repair step runs again
-- THEN existing data MUST NOT be duplicated
-- AND the repair step MUST log that seeding was skipped
-
-#### Scenario SEED-007c: Seed data uses @self references
-
-- GIVEN seed objects in the JSON file use the `@self` pattern from opencatalogi
-- THEN each seed object MUST include:
- ```json
- {
- "@self": {
- "register": "brp",
- "schema": "ingeschrevenPersoon",
- "slug": "jan-de-vries"
- },
- "burgerservicenummer": "999993653",
- "voornamen": "Jan Albert",
- ...
- }
- ```
-- AND the `slug` MUST be unique within the schema
-- AND the `register` and `schema` values MUST reference definitions in the same file
-
-#### Scenario SEED-007d: Configuration toggle
-
-- GIVEN an admin sets app config `base_registers_seeding` to `false` (via Procest admin settings or `occ config:app:set`)
-- THEN the repair step MUST skip loading base register seed data
-- AND existing base registers MUST NOT be affected (not deleted)
-
----
-
-## Dependencies
-
-- **OpenRegister core**: Register, schema, and object management; JSON configuration loading via `ConfigurationService`
-- **Procest repair step**: `InitializeSettings` + `SettingsService::loadConfiguration()` pattern for auto-loading on install
-- **Pipelinq register**: `pipelinq_register.json` client schema -- Pipelinq clients reference KVK/BRP identifiers
-- **GGM (ggm-openregister)**: The GGM schemas in `99-kern.openregister.json` provide an alternative, more detailed data model. The schemas defined in this spec are simplified versions optimized for seed data and app testing, not full GGM compliance.
-
----
-
-## Standards & References
-
-- **Haal Centraal BRP Personen Bevragen API v2** -- BRP person schema structure. Source: RVIG (Rijksdienst voor Identiteitsgegevens). URL: https://developer.rvig.nl/brp-api/overview/
-- **KVK Handelsregister API** -- Basisprofiel and Vestigingsprofiel endpoints. Source: Kamer van Koophandel. URL: https://developers.kvk.nl/
-- **BAG API Individuele Bevragingen v2** -- Nummeraanduiding, OpenbareRuimte, Woonplaats, Verblijfsobject, Pand. Source: Kadaster. URL: https://lvbag.github.io/BAG-API/
-- **STAM v6 / IMAM** -- Standaard Aanvragen en Meldingen / Informatiemodel Aanvragen en Meldingen for DSO vergunningaanvragen. Source: IPLO / Ministerie van BZK. URL: https://iplo.nl/digitaal-stelsel/aansluiten/standaarden/stam-imam/
-- **Popolo Data Standard** -- International standard for political entities (Person, Organization, Event, Motion, VoteEvent). Source: Popolo Project. URL: https://www.popoloproject.com/specs/
-- **Open Raadsinformatie (ORI)** -- Open State Foundation project for standardizing Dutch council information. URL: https://openraadsinformatie.nl/
-- **SBI (Standaard Bedrijfsindeling)** -- Official Dutch Standard Industrial Classification for business activity codes. Source: KVK/CBS.
-- **BSN 11-proef** -- Checksum algorithm for Dutch citizen service numbers. The weighted sum `(d1*9 + d2*8 + d3*7 + d4*6 + d5*5 + d6*4 + d7*3 + d8*2 - d9*1)` must be divisible by 11 and not equal to 0.
-- **GGM (Gemeentelijk Gegevensmodel) v2.5.0** -- Municipal data model. Used for entity naming alignment. Source: VNG. Available at `ggm-openregister/` in this workspace.
-- **ZGW APIs (VNG)** -- Zaakgericht Werken APIs for case management alignment. Procest case-betrokkene linking uses ZGW conventions.
-- **RVIG test BSN range** -- BSNs starting with `9999` are reserved for testing purposes by RVIG.
-
----
-
-## Current Implementation Status
-
-**Implemented in OpenRegister (not Procest).** All five base register JSON files are available as JSON files that can be loaded on demand from `openregister/lib/Settings/`. The files are NOT in the Procest codebase -- they live in the OpenRegister app which is the canonical home for base registry data. Procest and Pipelinq consume these registers after loading.
-
-### Using Mock Register Data
-
-All five base registers are available in `openregister/lib/Settings/`:
-
-| Register | File | Records | Slug | Schemas |
-|----------|------|---------|------|---------|
-| BRP | `brp_register.json` | 35 persons | `brp` | `ingeschreven-persoon` |
-| KVK | `kvk_register.json` | 16 businesses + 14 branches | `kvk` | `maatschappelijke-activiteit`, `vestiging` |
-| BAG | `bag_register.json` | 32 addresses + 21 objects + 21 buildings | `bag` | `nummeraanduiding`, `verblijfsobject`, `pand` |
-| DSO | `dso_register.json` | 53 records | `dso` | `activiteit`, `locatie`, `omgevingsdocument`, `vergunningaanvraag` |
-| ORI | `ori_register.json` | 115 records | `ori` | `vergadering`, `agendapunt`, `raadsdocument`, `stemming`, `raadslid`, `fractie` |
-
-**Loading all registers:**
-```bash
-docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/brp_register.json
-docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/kvk_register.json
-docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/bag_register.json
-docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/dso_register.json
-docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/ori_register.json
-```
-
-**Or via the API:**
-```bash
-curl -X POST "http://localhost:8080/index.php/apps/openregister/api/registers/import" \
- -u admin:admin -H "Content-Type: application/json" \
- -d @openregister/lib/Settings/brp_register.json
-```
-
-**Test data for Procest use cases:**
-- **Case with initiator (BRP)**: BSN `999993653` (Suzanne Moulin) -- link as case initiator via betrokkene
-- **Case with BAG-object**: Use BAG nummeraanduiding records -- link address to bouwvergunning case (REQ-CDV-05b)
-- **VTH with DSO vergunningaanvraag**: Use DSO `vergunningaanvraag` records for omgevingsvergunning intake testing
-- **Legesberekening**: BAG `verblijfsobject` records include `oppervlakte` field for fee calculation
-- **StUF-BG person lookup**: BSN `999993653` to test `npsLv01` query
-- **ORI council data**: Use ORI records to test B&W besluit workflow with raadsinformatie
-
-**Querying mock data:**
-```bash
-# Find person by BSN
-curl "http://localhost:8080/index.php/apps/openregister/api/objects/{brp_register_id}/{person_schema_id}?_search=999993653" -u admin:admin
-
-# Find BAG address
-curl "http://localhost:8080/index.php/apps/openregister/api/objects/{bag_register_id}/{nummeraanduiding_schema_id}?_search=1015" -u admin:admin
-```
-
-**Foundation available:**
-- `SettingsService::loadConfiguration()` can load register JSON files from `lib/Settings/` (currently loads `procest_register.json`).
-- The `InitializeSettings` repair step runs on app install/upgrade and calls `loadConfiguration()`.
-- The GGM at `ggm-openregister/` provides full GGM schemas that could serve as a reference or alternative (955 schemas across 12 registers), but they contain no seed data.
-- OpenCatalogi's `publication_register.json` demonstrates the `@self` seed object pattern in `components.objects`.
-
----
-
-## Specificity Assessment
-
-**This spec is implementation-ready for the data model.** All schemas are fully defined with property names, types, constraints, and examples. Cross-register relationships are specified with concrete scenarios.
-
-**Strengths:**
-- Complete property tables for all 16 schemas across 5 registers
-- Concrete seed data requirements with minimum record counts
-- Cross-register integrity scenarios with specific field-level mappings
-- BSN 11-proef validation requirement with algorithm specification
-- File structure and loading mechanism aligned with existing Procest patterns
-
-**What needs further research before implementation:**
-1. **BSN generation**: A utility function or lookup table of 25+ valid test BSNs in the `9999xxxx` range that pass the 11-proef is needed. This is straightforward to compute.
-2. **BAG identificatie format**: The 16-digit BAG identification numbers follow a `GGGG-TT-NNNNNNNNNN` pattern where GGGG = gemeentecode, TT = object type. Need to verify correct gemeentecodes for the 5 seed municipalities.
-3. **DSO zaaknummer format**: The actual DSO/Omgevingsloket zaaknummer format may differ from the `OLO-YYYY-NNNNN` pattern used here. Need to verify with IPLO documentation.
-4. **ORI entity alignment**: The ORI project has been evolving; need to verify that the Popolo-based model used here matches the current ORI API output format.
-5. **Multiple register loading**: The current `SettingsService::loadConfiguration()` loads one register file (`procest_register.json`). It may need to be extended to iterate over multiple files, or each base register file could be loaded by a separate repair step.
-
-**Open questions:**
-1. Should base register seed data live in Procest or in OpenRegister? The mock-registers spec in `openregister/openspec/specs/mock-registers/` suggests OpenRegister as the home. However, Procest-specific test scenarios (e.g., families for case testing) argue for Procest ownership. **Recommendation**: Put the files in Procest (it owns the test scenarios), but keep the schema definitions compatible with the OpenRegister mock-registers spec.
-2. Should Pipelinq also load these registers, or should it depend on Procest to seed them? **Recommendation**: Procest seeds them; Pipelinq reads them. This avoids duplicate seeding when both apps are installed.
-3. How large should the dataset be? The spec defines minimums (25 BRP, 15 KVK, etc.) but larger datasets (100+ per register) would better test pagination, faceting, and search performance. Consider a `--extended` flag for the seeder.
-4. Should the DSO and ORI registers be in separate files or combined? **Recommendation**: Separate files (one per register) for maintainability and independent loading.
+# Base Register Seed Data Specification
+
+## Purpose
+
+Define mock/test register JSON files for five Dutch base registrations (BRP, KVK, BAG, DSO, ORI) with realistic seed data that enables full-cycle testing and demos of Procest (case management) and Pipelinq (CRM) features without external API access. These registers supplement the existing `procest_register.json` and `pipelinq_register.json` by providing the government data layer that these apps query during citizen/business identification, case enrichment, address resolution, permit intake, and council information display.
+
+**Relationship to existing specs**: This spec extends `openregister/openspec/specs/mock-registers/spec.md` (which defines BRP and KVK requirements) by adding BAG, DSO, and ORI registers, specifying cross-register relationships, and defining concrete seed data scenarios tied to Procest and Pipelinq test cases.
+
+**Consuming specs**:
+- Procest `case-dashboard-view` (REQ-CDV-05b): BRP-persoon and BAG-object as linked objects
+- Procest `vth-module` (REQ-VTH-01): DSO vergunningaanvraag intake with BAG locatie
+- Procest `zaak-intake-flow`: Betrokkene identification via BRP/KVK
+- Procest `legesberekening`: BAG oppervlakte for fee calculation
+- Pipelinq `klantbeeld-360`: BRP/KVK enrichment for 360-degree customer view
+- Pipelinq `kcc-werkplek`: BSN/KVK citizen/business identification
+- Pipelinq `prospect-discovery`: KVK data for prospect search and scoring
+
+**Feature tier**: MVP (BRP + KVK + BAG), V1 (DSO + ORI)
+
+---
+
+## File Structure
+
+```
+procest/lib/Settings/
+ procest_register.json -- existing app register (unchanged)
+ brp_register.json -- BRP (persons)
+ kvk_register.json -- KVK (businesses)
+ bag_register.json -- BAG (addresses/buildings)
+ dso_register.json -- DSO (permits/environment)
+ ori_register.json -- ORI (council information)
+```
+
+Each file follows the OpenRegister JSON format: OpenAPI 3.0 envelope with `x-openregister` metadata, `components.registers` (register definition), `components.schemas` (entity schemas), and `components.objects` (seed data). The repair step (`InitializeSettings`) loads each file via `SettingsService::loadConfiguration()`.
+
+---
+
+## REQ-SEED-001: BRP Register (Basisregistratie Personen)
+
+**Feature tier**: MVP
+
+The system MUST provide a `brp_register.json` file containing a BRP register with an `ingeschrevenPersoon` schema and at least 25 fictional person records.
+
+### Register Definition
+
+| Field | Value |
+|-------|-------|
+| slug | `brp` |
+| title | `BRP (Basisregistratie Personen)` |
+| version | `1.0.0` |
+| description | `Mock BRP register for development and testing. Contains fictional persons aligned with the Haal Centraal BRP Personen Bevragen API v2 response structure. Authority: RVIG (Rijksdienst voor Identiteitsgegevens).` |
+| tablePrefix | (empty) |
+| folder | `Open Registers/BRP` |
+| schemas | `["ingeschrevenPersoon"]` |
+
+### Schema: `ingeschrevenPersoon`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `burgerservicenummer` | string (9 digits) | yes | no | BSN, MUST pass 11-proef validation | `"999993653"` |
+| `voornamen` | string | yes | no | First names | `"Jan Albert"` |
+| `voorletters` | string | no | no | Initials | `"J.A."` |
+| `voorvoegsel` | string | no | no | Name prefix (tussenvoegsel) | `"de"` |
+| `geslachtsnaam` | string | yes | yes | Family name | `"Vries"` |
+| `aanhef` | string | no | no | Form of address | `"De heer"` |
+| `geslachtsaanduiding` | string (enum) | yes | yes | Gender: `man`, `vrouw`, `onbekend` | `"man"` |
+| `geboortedatum` | string (date) | yes | no | Date of birth (YYYY-MM-DD) | `"1985-03-15"` |
+| `geboorteplaats` | string | no | no | Place of birth | `"Amsterdam"` |
+| `geboorteland` | string | no | no | Country of birth (code table) | `"Nederland"` |
+| `overlijdensdatum` | string (date) | no | no | Date of death (null if alive) | `null` |
+| `verblijfplaatsStraat` | string | no | no | Street name | `"Keizersgracht"` |
+| `verblijfplaatsHuisnummer` | integer | no | no | House number | `100` |
+| `verblijfplaatsHuisletter` | string | no | no | House letter | `"A"` |
+| `verblijfplaatsHuisnummertoevoeging` | string | no | no | House number suffix | `"bis"` |
+| `verblijfplaatsPostcode` | string | no | no | Postal code (####XX) | `"1015AA"` |
+| `verblijfplaatsWoonplaats` | string | no | yes | City | `"Amsterdam"` |
+| `verblijfplaatsGemeente` | string | no | yes | Municipality of registration | `"Amsterdam"` |
+| `nationaliteit` | string | no | yes | Nationality | `"Nederlandse"` |
+| `burgerlijkeStaat` | string (enum) | no | yes | Marital status: `ongehuwd`, `gehuwd`, `gescheiden`, `weduwe/weduwnaar`, `partnerschap` | `"gehuwd"` |
+| `partnerBsn` | string | no | no | BSN of partner (cross-ref within register) | `"999990019"` |
+| `partnerNaam` | string | no | no | Full name of partner | `"Maria Bakker"` |
+| `kinderen` | array of objects | no | no | Children `[{bsn, naam}]` | `[{"bsn":"999990020","naam":"Sophie de Vries"}]` |
+| `ouders` | array of objects | no | no | Parents `[{bsn, naam}]` | `[{"bsn":"999990001","naam":"Pieter de Vries"}]` |
+| `datumInschrijving` | string (date) | no | no | Registration date in municipality | `"2010-06-01"` |
+
+**Design notes**:
+- The flat property structure (e.g., `verblijfplaatsStraat` instead of nested `verblijfplaats.straat`) matches how OpenRegister stores object properties in the JSON column. Nested objects can be used but flat is simpler for faceting and search.
+- The `partner`, `kinderen`, and `ouders` references use BSN strings that can be resolved within the same register, enabling cross-referencing without requiring UUID joins.
+
+#### Scenario SEED-001a: BSN 11-proef validation
+
+- GIVEN a seed person with `burgerservicenummer` value `"999993653"`
+- WHEN the weighted checksum is calculated: `(9*9 + 9*8 + 9*7 + 9*6 + 9*5 + 3*4 + 6*3 + 5*2 - 3*1)`
+- THEN the result MUST be divisible by 11
+- AND all 25+ seed BSNs MUST pass the 11-proef
+- AND all BSNs MUST start with `9999` (the known-fictional BSN range used by RVIG for testing)
+
+#### Scenario SEED-001b: Family unit consistency
+
+- GIVEN the seed data contains the De Vries family:
+ - Jan Albert de Vries (BSN 999993653, born 1985-03-15, man, gehuwd)
+ - Maria Bakker-de Vries (BSN 999990019, born 1987-11-22, vrouw, gehuwd)
+ - Sophie de Vries (BSN 999990020, born 2015-06-10, vrouw, ongehuwd)
+ - Thomas de Vries (BSN 999990021, born 2018-09-03, man, ongehuwd)
+- THEN Jan's `partnerBsn` MUST equal Maria's BSN and vice versa
+- AND Jan's `kinderen` MUST list Sophie and Thomas
+- AND Sophie's `ouders` MUST list Jan and Maria
+- AND all four MUST share the same `verblijfplaatsStraat`, `verblijfplaatsHuisnummer`, `verblijfplaatsPostcode`
+
+#### Scenario SEED-001c: Geographic distribution
+
+- GIVEN the 25+ seed persons
+- THEN persons MUST be distributed across at least 5 municipalities: Amsterdam, Utrecht, Rotterdam, Den Haag, Tilburg
+- AND postcodes MUST be realistic for the specified city (e.g., Amsterdam: 10xx, Utrecht: 35xx, Rotterdam: 30xx)
+
+#### Scenario SEED-001d: Demographic diversity
+
+- GIVEN the seed data
+- THEN the following scenarios MUST be covered:
+ - At least 3 married couples with children (family units)
+ - At least 2 single persons (ongehuwd, no partner)
+ - At least 1 divorced person (gescheiden)
+ - At least 1 deceased person (overlijdensdatum set)
+ - At least 1 person with non-Dutch nationality
+ - At least 1 person with registered partnership (partnerschap)
+ - Ages ranging from minors (under 18) to elderly (over 75)
+
+### Seed Data Requirements Summary
+
+| Scenario | Min Records | Purpose |
+|----------|-------------|---------|
+| Family with 2 children (De Vries) | 4 | Procest zaak-betrokkene linking, Pipelinq klantbeeld family view |
+| Family with 1 child (Bakker) | 3 | Second family for cross-case testing |
+| Family with 3 children (Jansen) | 5 | Large family, multi-child scenarios |
+| Single persons | 3 | Pipelinq client creation from BRP |
+| Divorced person + ex-partner | 2 | Burgerlijke staat edge case |
+| Elderly couple | 2 | Age range coverage |
+| Deceased person | 1 | Overlijden edge case |
+| Non-Dutch nationals | 2 | Nationality filter testing |
+| Registered partnership | 2 | Partnerschap scenario |
+| Business owner (also in KVK) | 1 | Cross-register: BRP person = KVK eigenaar |
+| **Total minimum** | **25** | |
+
+---
+
+## REQ-SEED-002: KVK Register (Kamer van Koophandel)
+
+**Feature tier**: MVP
+
+The system MUST provide a `kvk_register.json` file containing a KVK register with a `maatschappelijkeActiviteit` schema and at least 15 fictional business records.
+
+### Register Definition
+
+| Field | Value |
+|-------|-------|
+| slug | `kvk` |
+| title | `KVK (Handelsregister)` |
+| version | `1.0.0` |
+| description | `Mock KVK register for development and testing. Contains fictional businesses aligned with the KVK Handelsregister API (Basisprofiel/Vestigingsprofiel) response structure. Authority: Kamer van Koophandel.` |
+| tablePrefix | (empty) |
+| folder | `Open Registers/KVK` |
+| schemas | `["maatschappelijkeActiviteit", "vestiging"]` |
+
+### Schema: `maatschappelijkeActiviteit`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `kvkNummer` | string (8 digits) | yes | no | KVK registration number | `"90001234"` |
+| `handelsnaam` | string | yes | yes | Primary trade name | `"Bakkerij De Vries B.V."` |
+| `handelsnamen` | array of strings | no | no | All trade names | `["Bakkerij De Vries","De Vries Patisserie"]` |
+| `rechtsvorm` | string | yes | yes | Legal form display name | `"Besloten Vennootschap"` |
+| `rechtsvormCode` | string | yes | yes | Legal form code: `BV`, `NV`, `Eenmanszaak`, `Stichting`, `VOF`, `CV`, `Cooperatie`, `Vereniging`, `Maatschap` | `"BV"` |
+| `rsin` | string (9 digits) | no | no | RSIN (Rechtspersonen en Samenwerkingsverbanden Identificatienummer) | `"123456789"` |
+| `vestigingsadresStraat` | string | no | no | Street name of main establishment | `"Prinsengracht"` |
+| `vestigingsadresHuisnummer` | integer | no | no | House number | `200` |
+| `vestigingsadresPostcode` | string | no | no | Postal code (####XX) | `"1016GS"` |
+| `vestigingsadresPlaats` | string | no | yes | City | `"Amsterdam"` |
+| `vestigingsadresProvincie` | string | no | yes | Province | `"Noord-Holland"` |
+| `sbiHoofdactiviteit` | string | yes | yes | Primary SBI code | `"1071"` |
+| `sbiHoofdactiviteitOmschrijving` | string | no | yes | Primary SBI description | `"Vervaardiging van brood en banket"` |
+| `sbiActiviteiten` | array of objects | no | no | All SBI activities `[{sbiCode, omschrijving, isHoofdactiviteit}]` | see below |
+| `aantalWerkzamePersonen` | integer | no | no | Number of employees | `25` |
+| `datumOprichting` | string (date) | no | no | Date of establishment | `"2005-09-12"` |
+| `datumUitschrijving` | string (date) | no | no | Date of deregistration (null if active) | `null` |
+| `actief` | boolean | yes | yes | Whether the business is active | `true` |
+| `eigenaarNaam` | string | no | no | Owner name (links to BRP for eenmanszaak) | `"J.A. de Vries"` |
+| `eigenaarBsn` | string | no | no | Owner BSN (cross-ref to BRP, for eenmanszaak/VOF) | `"999993653"` |
+| `website` | string (uri) | no | no | Company website | `"https://www.devries-bakkerij.nl"` |
+| `emailadres` | string (email) | no | no | Contact email | `"info@devries-bakkerij.nl"` |
+| `telefoonnummer` | string | no | no | Contact phone | `"+31 20 1234567"` |
+
+### Schema: `vestiging`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `vestigingsnummer` | string (12 digits) | yes | no | Vestiging registration number | `"000012345678"` |
+| `kvkNummer` | string (8 digits) | yes | no | Parent KVK number (cross-ref) | `"90001234"` |
+| `handelsnaam` | string | yes | yes | Trade name of this vestiging | `"Bakkerij De Vries - Filiaal Zuid"` |
+| `type` | string (enum) | yes | yes | `hoofdvestiging` or `nevenvestiging` | `"nevenvestiging"` |
+| `adresStraat` | string | no | no | Street name | `"Beethovenstraat"` |
+| `adresHuisnummer` | integer | no | no | House number | `42` |
+| `adresPostcode` | string | no | no | Postal code | `"1077JJ"` |
+| `adresPlaats` | string | no | yes | City | `"Amsterdam"` |
+| `sbiActiviteiten` | array of objects | no | no | SBI activities at this location | see parent schema |
+| `aantalWerkzamePersonen` | integer | no | no | Employees at this location | `8` |
+| `actief` | boolean | yes | yes | Whether the vestiging is active | `true` |
+
+#### Scenario SEED-002a: Legal form diversity
+
+- GIVEN the 15+ seed businesses
+- THEN the following legal forms MUST be represented:
+ - BV (Besloten Vennootschap): at least 4 records
+ - Eenmanszaak: at least 3 records (with `eigenaarBsn` linking to BRP persons)
+ - Stichting: at least 2 records
+ - VOF (Vennootschap onder Firma): at least 1 record
+ - NV (Naamloze Vennootschap): at least 1 record
+ - Vereniging: at least 1 record
+- AND at least 1 business MUST have `actief: false` with `datumUitschrijving` set
+
+#### Scenario SEED-002b: SBI code diversity
+
+- GIVEN the seed businesses
+- THEN businesses MUST cover at least 8 different SBI top-level sections:
+ - A (Landbouw): e.g., `"0111"` Akkerbouw
+ - C (Industrie): e.g., `"1071"` Brood en banket
+ - F (Bouw): e.g., `"4120"` Algemene burgerlijke en utiliteitsbouw
+ - G (Handel): e.g., `"4711"` Supermarkten
+ - I (Horeca): e.g., `"5610"` Restaurants
+ - J (Informatie/communicatie): e.g., `"6201"` Ontwikkelen en produceren van software
+ - M (Advisering): e.g., `"6920"` Accountancy en belastingadvies
+ - Q (Zorg): e.g., `"8610"` Ziekenhuizen
+
+#### Scenario SEED-002c: Cross-register BRP linkage
+
+- GIVEN BRP person "Jan Albert de Vries" (BSN 999993653) is a business owner
+- WHEN the KVK seed data includes an eenmanszaak "De Vries Consultancy"
+- THEN `eigenaarBsn` MUST equal `"999993653"`
+- AND `eigenaarNaam` MUST equal `"J.A. de Vries"`
+- AND `vestigingsadresStraat` + `vestigingsadresPostcode` SHOULD match Jan's BRP `verblijfplaatsStraat` + `verblijfplaatsPostcode` (common for eenmanszaak)
+
+#### Scenario SEED-002d: Business with multiple vestigingen
+
+- GIVEN seed business "Bakkerij De Vries B.V." (KVK 90001234)
+- THEN at least 2 vestiging records MUST exist:
+ - Hoofdvestiging: Prinsengracht 200, Amsterdam
+ - Nevenvestiging: Beethovenstraat 42, Amsterdam
+- AND both vestigingen MUST reference the same `kvkNummer`
+
+### Seed Data Requirements Summary
+
+| Scenario | Min Records | Purpose |
+|----------|-------------|---------|
+| BV businesses (various sectors) | 4 | Pipelinq client management, prospect discovery |
+| Eenmanszaak (with BRP link) | 3 | Cross-register testing, KCC identification |
+| Stichtingen | 2 | Non-profit sector testing |
+| VOF | 1 | Multi-owner business |
+| NV | 1 | Large corporation scenario |
+| Vereniging | 1 | Community organization |
+| Inactive business | 1 | Deregistered edge case |
+| Multi-vestiging business | 1 (+2 vestigingen) | Vestiging search in Pipelinq |
+| IT/software company | 1 | Pipelinq SBI filter testing |
+| **Total minimum maatschappelijkeActiviteit** | **15** | |
+| **Total minimum vestiging** | **18** | (15 hoofd + 3 neven) |
+
+---
+
+## REQ-SEED-003: BAG Register (Basisregistratie Adressen en Gebouwen)
+
+**Feature tier**: MVP
+
+The system MUST provide a `bag_register.json` file containing a BAG register with schemas for `nummeraanduiding`, `openbareRuimte`, `woonplaats`, `verblijfsobject`, and `pand`, with seed data that matches the addresses used in BRP and KVK seed data.
+
+### Register Definition
+
+| Field | Value |
+|-------|-------|
+| slug | `bag` |
+| title | `BAG (Basisregistratie Adressen en Gebouwen)` |
+| version | `1.0.0` |
+| description | `Mock BAG register for development and testing. Contains fictional addresses and buildings aligned with the BAG API Individuele Bevragingen v2 response structure. Authority: Kadaster.` |
+| tablePrefix | (empty) |
+| folder | `Open Registers/BAG` |
+| schemas | `["nummeraanduiding", "openbareRuimte", "woonplaats", "verblijfsobject", "pand"]` |
+
+### Schema: `nummeraanduiding`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363200000000001"` |
+| `huisnummer` | integer | yes | no | House number | `100` |
+| `huisletter` | string | no | no | House letter | `"A"` |
+| `huisnummertoevoeging` | string | no | no | House number suffix | `"bis"` |
+| `postcode` | string | yes | yes | Postal code (####XX) | `"1015AA"` |
+| `status` | string (enum) | yes | yes | `naamgeving uitgegeven`, `naamgeving ingetrokken` | `"naamgeving uitgegeven"` |
+| `typeAdresseerbaarObject` | string (enum) | no | yes | `Verblijfsobject`, `Standplaats`, `Ligplaats` | `"Verblijfsobject"` |
+| `openbareRuimteNaam` | string | yes | no | Street name (denormalized for search) | `"Keizersgracht"` |
+| `woonplaatsNaam` | string | yes | yes | City name (denormalized for search) | `"Amsterdam"` |
+| `openbareRuimteId` | string | no | no | Reference to openbareRuimte | `"0363300000000001"` |
+| `verblijfsobjectId` | string | no | no | Reference to verblijfsobject | `"0363010000000001"` |
+
+### Schema: `openbareRuimte`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363300000000001"` |
+| `naam` | string | yes | yes | Street/public space name | `"Keizersgracht"` |
+| `type` | string (enum) | yes | yes | `Weg`, `Water`, `Spoorbaan`, `Terrein`, `Kunstwerk`, `Landschappelijk gebied`, `Administratief gebied` | `"Weg"` |
+| `status` | string (enum) | yes | yes | `naamgeving uitgegeven`, `naamgeving ingetrokken` | `"naamgeving uitgegeven"` |
+| `woonplaatsNaam` | string | yes | yes | City name | `"Amsterdam"` |
+| `woonplaatsId` | string | no | no | Reference to woonplaats | `"3594"` |
+
+### Schema: `woonplaats`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `identificatie` | string (4 digits) | yes | no | Woonplaats code | `"3594"` |
+| `naam` | string | yes | yes | City/town name | `"Amsterdam"` |
+| `status` | string (enum) | yes | yes | `woonplaats aangewezen`, `woonplaats ingetrokken` | `"woonplaats aangewezen"` |
+| `gemeente` | string | no | yes | Municipality name | `"Amsterdam"` |
+| `provincie` | string | no | yes | Province name | `"Noord-Holland"` |
+
+### Schema: `verblijfsobject`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363010000000001"` |
+| `status` | string (enum) | yes | yes | `verblijfsobject gevormd`, `verblijfsobject in gebruik (niet ingemeten)`, `verblijfsobject in gebruik`, `verblijfsobject ingetrokken`, `verblijfsobject buiten gebruik` | `"verblijfsobject in gebruik"` |
+| `gebruiksdoel` | string (enum) | yes | yes | `woonfunctie`, `bijeenkomstfunctie`, `celfunctie`, `gezondheidszorgfunctie`, `industriefunctie`, `kantoorfunctie`, `logiesfunctie`, `onderwijsfunctie`, `sportfunctie`, `winkelfunctie`, `overige gebruiksfunctie` | `"woonfunctie"` |
+| `gebruiksdoelen` | array of strings | no | no | Multiple use purposes | `["woonfunctie"]` |
+| `oppervlakte` | integer | yes | no | Usable surface area in m2 | `120` |
+| `pandId` | string | no | no | Reference to pand | `"0363100000000001"` |
+| `nummeraanduidingId` | string | no | no | Reference to main nummeraanduiding | `"0363200000000001"` |
+| `bouwjaar` | integer | no | no | Construction year (from pand, denormalized) | `1895` |
+
+### Schema: `pand`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `identificatie` | string (16 digits) | yes | no | BAG object ID | `"0363100000000001"` |
+| `status` | string (enum) | yes | yes | `bouwvergunning verleend`, `bouw gestart`, `pand in gebruik (niet ingemeten)`, `pand in gebruik`, `sloopvergunning verleend`, `pand gesloopt`, `pand buiten gebruik`, `niet gerealiseerd pand`, `verbouwing pand` | `"pand in gebruik"` |
+| `oorspronkelijkBouwjaar` | integer | yes | no | Original construction year | `1895` |
+| `oppervlakte` | integer | no | no | Gross surface area in m2 | `450` |
+
+#### Scenario SEED-003a: BAG addresses match BRP persons
+
+- GIVEN BRP person Jan de Vries lives at Keizersgracht 100A, 1015AA Amsterdam
+- THEN the BAG MUST contain:
+ - A `woonplaats` record for Amsterdam (identificatie `"3594"`)
+ - An `openbareRuimte` record for Keizersgracht in Amsterdam
+ - A `nummeraanduiding` with huisnummer 100, huisletter A, postcode 1015AA
+ - A `verblijfsobject` with `gebruiksdoel` = `"woonfunctie"`, linked to a `pand`
+ - A `pand` with `oorspronkelijkBouwjaar` and `status` = `"pand in gebruik"`
+
+#### Scenario SEED-003b: BAG addresses match KVK businesses
+
+- GIVEN KVK business "Bakkerij De Vries B.V." at Prinsengracht 200, 1016GS Amsterdam
+- THEN the BAG MUST contain corresponding `nummeraanduiding`, `openbareRuimte`, `verblijfsobject` (gebruiksdoel `"winkelfunctie"`), and `pand` records
+- AND the BAG address components MUST be consistent: `nummeraanduiding.openbareRuimteNaam` = the openbareRuimte name, `nummeraanduiding.woonplaatsNaam` = the woonplaats name
+
+#### Scenario SEED-003c: Address for DSO vergunningaanvraag
+
+- GIVEN DSO vergunningaanvraag for a building project at Herengracht 300, 1016CE Amsterdam
+- THEN the BAG MUST contain the corresponding address records
+- AND the `pand` SHOULD have `status` = `"verbouwing pand"` to represent an ongoing building project
+- AND the `verblijfsobject` MUST have `oppervlakte` set (used in legesberekening)
+
+#### Scenario SEED-003d: Multiple residents at one address
+
+- GIVEN the Jansen family (5 persons) lives at Maliebaan 50, 3581CS Utrecht
+- THEN ONE `nummeraanduiding` record MUST exist for that address
+- AND the `verblijfsobject` `gebruiksdoel` MUST be `"woonfunctie"`
+- AND all 5 BRP persons MUST reference the same address (postcode + huisnummer + straat + woonplaats)
+
+### Seed Data Requirements Summary
+
+| Entity | Min Records | Notes |
+|--------|-------------|-------|
+| woonplaats | 5 | Amsterdam, Utrecht, Rotterdam, Den Haag, Tilburg |
+| openbareRuimte | 20 | Streets matching BRP/KVK addresses |
+| nummeraanduiding | 35 | All BRP + KVK addresses (deduplicated) |
+| verblijfsobject | 35 | One per nummeraanduiding |
+| pand | 30 | Some shared (apartment buildings) |
+
+---
+
+## REQ-SEED-004: DSO Register (Digitaal Stelsel Omgevingswet)
+
+**Feature tier**: V1
+
+The system MUST provide a `dso_register.json` file containing a DSO register with schemas for `vergunningaanvraag` and `activiteit`, with seed data representing permit applications in the Omgevingswet domain.
+
+### Register Definition
+
+| Field | Value |
+|-------|-------|
+| slug | `dso` |
+| title | `DSO (Digitaal Stelsel Omgevingswet)` |
+| version | `1.0.0` |
+| description | `Mock DSO register for development and testing. Contains fictional permit applications aligned with the STAM/IMAM (Standaard Aanvragen en Meldingen / Informatiemodel Aanvragen en Meldingen) standard. Authority: Ministerie van BZK via IPLO.` |
+| tablePrefix | (empty) |
+| folder | `Open Registers/DSO` |
+| schemas | `["vergunningaanvraag", "activiteit"]` |
+
+### Schema: `vergunningaanvraag`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `zaaknummer` | string | yes | no | DSO case reference number | `"OLO-2026-00001"` |
+| `aanvraagdatum` | string (date) | yes | no | Date of application | `"2026-01-15"` |
+| `procedureType` | string (enum) | yes | yes | `regulier` (8 wk), `uitgebreid` (26 wk) | `"regulier"` |
+| `omschrijving` | string | yes | no | Description of the project | `"Verbouwing woonhuis tot kantoor"` |
+| `locatieAdres` | string | no | no | Address of the project (display) | `"Herengracht 300, 1016CE Amsterdam"` |
+| `locatiePostcode` | string | no | yes | Postcode of the project location | `"1016CE"` |
+| `locatiePlaats` | string | no | yes | City of the project location | `"Amsterdam"` |
+| `locatieBagId` | string | no | no | BAG nummeraanduiding identificatie (cross-ref) | `"0363200000000010"` |
+| `locatieKadastraalPerceel` | string | no | no | Cadastral parcel identifier | `"ASD04-F-1234"` |
+| `initiatiefnemerNaam` | string | yes | no | Applicant name | `"Petra Jansen"` |
+| `initiatiefnemerBsn` | string | no | no | Applicant BSN (cross-ref to BRP) | `"999990027"` |
+| `initiatiefnemerKvk` | string | no | no | Applicant KVK number (cross-ref, if business) | `"90001234"` |
+| `gemachtigdeNaam` | string | no | no | Authorized representative name | `"Architectenbureau Van Dam B.V."` |
+| `bouwkosten` | number | no | no | Estimated construction costs in EUR | `180000` |
+| `oppervlakte` | integer | no | no | Area in m2 | `250` |
+| `activiteiten` | array of strings | no | no | List of activities from the application | `["Bouwen","Kappen","Uitrit aanleggen"]` |
+| `status` | string (enum) | yes | yes | `ingediend`, `ontvankelijk`, `in_behandeling`, `besluit_genomen`, `verleend`, `geweigerd`, `ingetrokken`, `buiten_behandeling` | `"ingediend"` |
+| `besluitdatum` | string (date) | no | no | Date of decision | `null` |
+| `resultaat` | string (enum) | no | yes | `verleend`, `geweigerd`, `deels_verleend` | `null` |
+
+### Schema: `activiteit`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `naam` | string | yes | yes | Activity name from Omgevingswet | `"Bouwen van een bouwwerk"` |
+| `code` | string | yes | no | DSO activity code | `"BOUWEN-001"` |
+| `categorie` | string | yes | yes | `bouwactiviteit`, `milieubelastende activiteit`, `omgevingsplanactiviteit`, `Natura 2000-activiteit`, `ontgrondingsactiviteit` | `"bouwactiviteit"` |
+| `regelgevingType` | string | no | yes | `vergunningplicht`, `meldingsplicht`, `informatieplicht` | `"vergunningplicht"` |
+| `bevoegdGezag` | string | no | yes | Competent authority type | `"gemeente"` |
+| `omschrijving` | string | no | no | Detailed description of the activity | `"Het bouwen van een bouwwerk waarvoor een omgevingsvergunning vereist is"` |
+
+#### Scenario SEED-004a: Bouwvergunning linked to BAG
+
+- GIVEN a vergunningaanvraag for "Verbouwing woonhuis" at Herengracht 300
+- THEN `locatieBagId` MUST reference a valid BAG `nummeraanduiding` in the BAG seed data
+- AND the `locatieAdres` MUST match the BAG address components
+- AND `initiatiefnemerBsn` MUST reference a valid BRP person
+
+#### Scenario SEED-004b: Multiple activities in one application
+
+- GIVEN a vergunningaanvraag with `activiteiten: ["Bouwen","Kappen","Uitrit aanleggen"]`
+- THEN 3 corresponding `activiteit` records MUST exist in the DSO register
+- AND the `vergunningaanvraag` links to these activities by name
+
+#### Scenario SEED-004c: Various permit types
+
+- GIVEN the seed data
+- THEN the following application types MUST be represented:
+ - Bouwvergunning (bouwen van een bouwwerk): reguliere procedure
+ - Milieuvergunning (milieubelastende activiteit): uitgebreide procedure
+ - Kapvergunning (vellen van houtopstand): reguliere procedure
+ - Omgevingsplanactiviteit (afwijken van omgevingsplan): reguliere procedure
+ - Combined application (samenloop): multiple activities in one aanvraag
+- AND at least 1 application MUST have `status` = `"verleend"` with `besluitdatum` set
+- AND at least 1 application MUST have `status` = `"geweigerd"`
+
+### Seed Data Requirements Summary
+
+| Entity | Min Records | Notes |
+|--------|-------------|-------|
+| vergunningaanvraag | 8 | Various types, statuses, and locations |
+| activiteit | 12 | Standard Omgevingswet activities |
+
+---
+
+## REQ-SEED-005: ORI Register (Open Raadsinformatie)
+
+**Feature tier**: V1
+
+The system MUST provide an `ori_register.json` file containing an ORI register with schemas for council meetings, agenda items, motions, votes, council members, and factions, with seed data representing a fictional municipal council.
+
+### Register Definition
+
+| Field | Value |
+|-------|-------|
+| slug | `ori` |
+| title | `ORI (Open Raadsinformatie)` |
+| version | `1.0.0` |
+| description | `Mock ORI register for development and testing. Contains fictional council proceedings aligned with the Popolo data standard and Open State Foundation ORI API conventions. Authority: gemeenteraad (municipal council).` |
+| tablePrefix | (empty) |
+| folder | `Open Registers/ORI` |
+| schemas | `["vergadering", "agendapunt", "document", "motie", "amendement", "stemming", "raadslid", "fractie"]` |
+
+### Schema: `vergadering`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `naam` | string | yes | no | Meeting name | `"Raadsvergadering 15 januari 2026"` |
+| `type` | string (enum) | yes | yes | `raadsvergadering`, `commissievergadering`, `informatieavond`, `presidium` | `"raadsvergadering"` |
+| `commissie` | string | no | yes | Committee name (if commissievergadering) | `"Commissie Ruimte en Wonen"` |
+| `startDatum` | string (date-time) | yes | no | Start date/time | `"2026-01-15T19:30:00+01:00"` |
+| `eindDatum` | string (date-time) | no | no | End date/time | `"2026-01-15T23:15:00+01:00"` |
+| `locatie` | string | no | no | Physical location | `"Raadzaal, Stadhuis"` |
+| `status` | string (enum) | yes | yes | `gepland`, `bevestigd`, `afgelopen`, `geannuleerd` | `"afgelopen"` |
+| `voorzitter` | string | no | no | Chair name | `"Burgemeester Van den Berg"` |
+
+### Schema: `agendapunt`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `titel` | string | yes | no | Agenda item title | `"Vaststelling bestemmingsplan Centrum-Oost"` |
+| `vergaderingId` | string (uuid) | yes | no | Reference to vergadering | (uuid) |
+| `volgorde` | integer | yes | no | Order on agenda | `3` |
+| `type` | string (enum) | yes | yes | `bespreekstuk`, `hamerstuk`, `informerend`, `procedureel` | `"bespreekstuk"` |
+| `portefeuille` | string | no | yes | Portfolio/department | `"Ruimtelijke Ordening"` |
+| `resultaat` | string | no | yes | Outcome | `"Aangenomen"` |
+
+### Schema: `document`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `titel` | string | yes | no | Document title | `"Raadsvoorstel vaststelling bestemmingsplan"` |
+| `type` | string (enum) | yes | yes | `raadsvoorstel`, `raadsbesluit`, `amendement`, `motie`, `brief`, `nota`, `verslag`, `bijlage` | `"raadsvoorstel"` |
+| `agendapuntId` | string (uuid) | no | no | Reference to agendapunt | (uuid) |
+| `datum` | string (date) | yes | no | Document date | `"2026-01-08"` |
+| `bestandsnaam` | string | no | no | File name | `"RV-2026-001-bestemmingsplan.pdf"` |
+| `samenvatting` | string | no | no | Summary | `"Voorstel tot vaststelling van het bestemmingsplan Centrum-Oost"` |
+
+### Schema: `motie`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `titel` | string | yes | no | Motion title | `"Motie vreemd: Meer groen in de binnenstad"` |
+| `agendapuntId` | string (uuid) | no | no | Reference to agenda item (null for motie vreemd) | (uuid or null) |
+| `indieners` | array of strings | yes | no | Submitting faction names | `["GroenLinks","D66"]` |
+| `dictum` | string | yes | no | The actual request/instruction | `"Verzoekt het college om binnen 6 maanden een groenplan op te stellen voor de binnenstad"` |
+| `datumIndiening` | string (date) | yes | no | Date of submission | `"2026-01-15"` |
+| `status` | string (enum) | yes | yes | `ingediend`, `aangenomen`, `verworpen`, `ingetrokken`, `aangehouden` | `"aangenomen"` |
+| `voorStemmen` | integer | no | no | Votes in favor | `22` |
+| `tegenStemmen` | integer | no | no | Votes against | `15` |
+
+### Schema: `amendement`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `titel` | string | yes | no | Amendment title | `"Amendement: Maximale bouwhoogte 25 meter"` |
+| `agendapuntId` | string (uuid) | yes | no | Reference to agenda item | (uuid) |
+| `indieners` | array of strings | yes | no | Submitting faction names | `["SP","PvdA"]` |
+| `wijziging` | string | yes | no | Proposed change text | `"Wijzigt artikel 3.2: maximale bouwhoogte van 30 naar 25 meter"` |
+| `toelichting` | string | no | no | Explanation | `"Om het historische straatbeeld te beschermen"` |
+| `datumIndiening` | string (date) | yes | no | Date of submission | `"2026-01-15"` |
+| `status` | string (enum) | yes | yes | `ingediend`, `aangenomen`, `verworpen`, `ingetrokken` | `"verworpen"` |
+| `voorStemmen` | integer | no | no | Votes in favor | `14` |
+| `tegenStemmen` | integer | no | no | Votes against | `23` |
+
+### Schema: `stemming`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `onderwerp` | string | yes | no | What is being voted on | `"Motie: Meer groen in de binnenstad"` |
+| `type` | string (enum) | yes | yes | `motie`, `amendement`, `raadsvoorstel`, `benoeming` | `"motie"` |
+| `vergaderingId` | string (uuid) | yes | no | Reference to vergadering | (uuid) |
+| `datum` | string (date) | yes | no | Vote date | `"2026-01-15"` |
+| `resultaat` | string (enum) | yes | yes | `aangenomen`, `verworpen` | `"aangenomen"` |
+| `voorStemmen` | integer | yes | no | Votes in favor | `22` |
+| `tegenStemmen` | integer | yes | no | Votes against | `15` |
+| `onthouding` | integer | no | no | Abstentions | `0` |
+| `stemmenPerFractie` | array of objects | no | no | `[{fractie, stem, aantalLeden}]` | see below |
+
+### Schema: `raadslid`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `naam` | string | yes | yes | Full name | `"Ahmed El Amrani"` |
+| `fractie` | string | yes | yes | Faction name | `"GroenLinks"` |
+| `functie` | string | no | yes | Role: `raadslid`, `fractievoorzitter`, `wethouder`, `burgemeester` | `"raadslid"` |
+| `email` | string (email) | no | no | Council email | `"a.elamrani@gemeenteraad.nl"` |
+| `actief` | boolean | yes | yes | Currently serving | `true` |
+| `startdatum` | string (date) | no | no | Start of term | `"2022-03-30"` |
+| `einddatum` | string (date) | no | no | End of term (null if current) | `null` |
+| `portefeuilles` | array of strings | no | no | Portfolio areas | `["Duurzaamheid","Groen"]` |
+
+### Schema: `fractie`
+
+| Property | Type | Required | Facetable | Description | Example |
+|----------|------|----------|-----------|-------------|---------|
+| `naam` | string | yes | yes | Faction/party name | `"GroenLinks"` |
+| `afkorting` | string | no | yes | Abbreviation | `"GL"` |
+| `aantalZetels` | integer | yes | no | Number of seats | `7` |
+| `coalitie` | boolean | yes | yes | Part of the coalition | `true` |
+| `fractievoorzitter` | string | no | no | Chair name | `"Ahmed El Amrani"` |
+
+#### Scenario SEED-005a: Complete council composition
+
+- GIVEN the seed data
+- THEN at least 7 fracties MUST exist representing a realistic Dutch council composition:
+ - VVD (6 zetels, coalitie)
+ - GroenLinks (7 zetels, coalitie)
+ - D66 (5 zetels, coalitie)
+ - PvdA (4 zetels, oppositie)
+ - CDA (3 zetels, oppositie)
+ - SP (3 zetels, oppositie)
+ - Lokaal Belang (2 zetels, oppositie)
+- AND at least 30 raadslid records MUST exist (sum of all zetels)
+- AND each raadslid MUST reference a valid fractie name
+
+#### Scenario SEED-005b: Council meeting with full proceedings
+
+- GIVEN a raadsvergadering "Raadsvergadering 15 januari 2026"
+- THEN the meeting MUST have at least 8 agendapunten
+- AND at least 2 moties MUST be linked (1 aangenomen, 1 verworpen)
+- AND at least 1 amendement MUST be linked
+- AND at least 3 stemmingen MUST be recorded with `stemmenPerFractie` data
+- AND at least 5 documenten MUST be linked to various agendapunten
+
+#### Scenario SEED-005c: Committee meeting
+
+- GIVEN the seed data
+- THEN at least 1 commissievergadering MUST exist (e.g., "Commissie Ruimte en Wonen")
+- AND the committee meeting MUST have at least 3 agendapunten of type `bespreekstuk` or `informerend`
+
+### Seed Data Requirements Summary
+
+| Entity | Min Records | Notes |
+|--------|-------------|-------|
+| fractie | 7 | Realistic Dutch council composition |
+| raadslid | 30 | All council members across factions |
+| vergadering | 3 | 2 raadsvergaderingen + 1 commissie |
+| agendapunt | 15 | Across all meetings |
+| document | 20 | Raadsvoorstellen, besluiten, bijlagen |
+| motie | 4 | Various statuses |
+| amendement | 2 | Aangenomen + verworpen |
+| stemming | 6 | With fractie-level detail |
+
+---
+
+## REQ-SEED-006: Cross-Register Relationship Integrity
+
+**Feature tier**: MVP
+
+All cross-register references between seed data MUST be consistent and resolvable.
+
+#### Scenario SEED-006a: BRP persons live at BAG addresses
+
+- GIVEN BRP person "Jan de Vries" with `verblijfplaatsStraat` = `"Keizersgracht"`, `verblijfplaatsHuisnummer` = `100`, `verblijfplaatsPostcode` = `"1015AA"`, `verblijfplaatsWoonplaats` = `"Amsterdam"`
+- THEN the BAG register MUST contain:
+ - A `nummeraanduiding` with matching `openbareRuimteNaam`, `huisnummer`, `postcode`, `woonplaatsNaam`
+ - A `verblijfsobject` linked to that nummeraanduiding with `gebruiksdoel` = `"woonfunctie"`
+- AND this mapping MUST hold for ALL BRP person addresses
+
+#### Scenario SEED-006b: KVK businesses have BAG vestigingsadressen
+
+- GIVEN KVK business "Bakkerij De Vries B.V." at Prinsengracht 200, 1016GS Amsterdam
+- THEN the BAG register MUST contain a `nummeraanduiding` + `verblijfsobject` at that address
+- AND the `verblijfsobject.gebruiksdoel` MUST be appropriate for the business type (e.g., `"winkelfunctie"` for a bakery, `"kantoorfunctie"` for a consultancy)
+
+#### Scenario SEED-006c: DSO applications reference BAG and BRP
+
+- GIVEN DSO vergunningaanvraag at Herengracht 300
+- THEN `locatieBagId` MUST reference an existing BAG `nummeraanduiding.identificatie`
+- AND `initiatiefnemerBsn` MUST reference an existing BRP `ingeschrevenPersoon.burgerservicenummer`
+
+#### Scenario SEED-006d: Eenmanszaak owners link BRP to KVK
+
+- GIVEN KVK eenmanszaak "De Vries Consultancy" with `eigenaarBsn` = `"999993653"`
+- THEN BRP person with BSN `"999993653"` MUST exist
+- AND the business `vestigingsadresStraat`/`vestigingsadresPostcode` SHOULD match the BRP person's `verblijfplaatsStraat`/`verblijfplaatsPostcode` (typical for eenmanszaak)
+
+#### Scenario SEED-006e: Procest cases can reference all registers
+
+- GIVEN a Procest case of type "Omgevingsvergunning" created from seed data
+- THEN the case SHOULD be linkable to:
+ - A BRP person as `betrokkene` (aanvrager) via BSN
+ - A BAG address as `zaakobject` via nummeraanduiding ID
+ - A DSO vergunningaanvraag as source via zaaknummer
+ - An ORI agendapunt (optional, for politically sensitive cases)
+
+#### Scenario SEED-006f: Pipelinq clients map to KVK
+
+- GIVEN a Pipelinq client of type `"organization"` with a KVK number
+- THEN the KVK number MUST match a `maatschappelijkeActiviteit.kvkNummer` in the KVK seed data
+- AND the client `address` SHOULD match the KVK `vestigingsadresStraat` + `vestigingsadresPlaats`
+
+---
+
+## REQ-SEED-007: Seed Data Loading
+
+**Feature tier**: MVP
+
+The register JSON files MUST be loadable by the existing OpenRegister configuration mechanism.
+
+#### Scenario SEED-007a: Auto-load on app install
+
+- GIVEN the `brp_register.json`, `kvk_register.json`, `bag_register.json` files exist in `procest/lib/Settings/`
+- WHEN the Procest repair step runs (app install or update)
+- THEN the `SettingsService::loadConfiguration()` method MUST load each register file
+- AND registers, schemas, and seed objects MUST be created in OpenRegister
+- AND seed objects MUST be created from the `components.objects` array in each file
+
+#### Scenario SEED-007b: Skip if already populated
+
+- GIVEN the BRP register already contains person objects
+- WHEN the repair step runs again
+- THEN existing data MUST NOT be duplicated
+- AND the repair step MUST log that seeding was skipped
+
+#### Scenario SEED-007c: Seed data uses @self references
+
+- GIVEN seed objects in the JSON file use the `@self` pattern from opencatalogi
+- THEN each seed object MUST include:
+ ```json
+ {
+ "@self": {
+ "register": "brp",
+ "schema": "ingeschrevenPersoon",
+ "slug": "jan-de-vries"
+ },
+ "burgerservicenummer": "999993653",
+ "voornamen": "Jan Albert",
+ ...
+ }
+ ```
+- AND the `slug` MUST be unique within the schema
+- AND the `register` and `schema` values MUST reference definitions in the same file
+
+#### Scenario SEED-007d: Configuration toggle
+
+- GIVEN an admin sets app config `base_registers_seeding` to `false` (via Procest admin settings or `occ config:app:set`)
+- THEN the repair step MUST skip loading base register seed data
+- AND existing base registers MUST NOT be affected (not deleted)
+
+---
+
+## Dependencies
+
+- **OpenRegister core**: Register, schema, and object management; JSON configuration loading via `ConfigurationService`
+- **Procest repair step**: `InitializeSettings` + `SettingsService::loadConfiguration()` pattern for auto-loading on install
+- **Pipelinq register**: `pipelinq_register.json` client schema -- Pipelinq clients reference KVK/BRP identifiers
+- **GGM (ggm-openregister)**: The GGM schemas in `99-kern.openregister.json` provide an alternative, more detailed data model. The schemas defined in this spec are simplified versions optimized for seed data and app testing, not full GGM compliance.
+
+---
+
+## Standards & References
+
+- **Haal Centraal BRP Personen Bevragen API v2** -- BRP person schema structure. Source: RVIG (Rijksdienst voor Identiteitsgegevens). URL: https://developer.rvig.nl/brp-api/overview/
+- **KVK Handelsregister API** -- Basisprofiel and Vestigingsprofiel endpoints. Source: Kamer van Koophandel. URL: https://developers.kvk.nl/
+- **BAG API Individuele Bevragingen v2** -- Nummeraanduiding, OpenbareRuimte, Woonplaats, Verblijfsobject, Pand. Source: Kadaster. URL: https://lvbag.github.io/BAG-API/
+- **STAM v6 / IMAM** -- Standaard Aanvragen en Meldingen / Informatiemodel Aanvragen en Meldingen for DSO vergunningaanvragen. Source: IPLO / Ministerie van BZK. URL: https://iplo.nl/digitaal-stelsel/aansluiten/standaarden/stam-imam/
+- **Popolo Data Standard** -- International standard for political entities (Person, Organization, Event, Motion, VoteEvent). Source: Popolo Project. URL: https://www.popoloproject.com/specs/
+- **Open Raadsinformatie (ORI)** -- Open State Foundation project for standardizing Dutch council information. URL: https://openraadsinformatie.nl/
+- **SBI (Standaard Bedrijfsindeling)** -- Official Dutch Standard Industrial Classification for business activity codes. Source: KVK/CBS.
+- **BSN 11-proef** -- Checksum algorithm for Dutch citizen service numbers. The weighted sum `(d1*9 + d2*8 + d3*7 + d4*6 + d5*5 + d6*4 + d7*3 + d8*2 - d9*1)` must be divisible by 11 and not equal to 0.
+- **GGM (Gemeentelijk Gegevensmodel) v2.5.0** -- Municipal data model. Used for entity naming alignment. Source: VNG. Available at `ggm-openregister/` in this workspace.
+- **ZGW APIs (VNG)** -- Zaakgericht Werken APIs for case management alignment. Procest case-betrokkene linking uses ZGW conventions.
+- **RVIG test BSN range** -- BSNs starting with `9999` are reserved for testing purposes by RVIG.
+
+---
+
+## Current Implementation Status
+
+**Implemented in OpenRegister (not Procest).** All five base register JSON files are available as JSON files that can be loaded on demand from `openregister/lib/Settings/`. The files are NOT in the Procest codebase -- they live in the OpenRegister app which is the canonical home for base registry data. Procest and Pipelinq consume these registers after loading.
+
+### Using Mock Register Data
+
+All five base registers are available in `openregister/lib/Settings/`:
+
+| Register | File | Records | Slug | Schemas |
+|----------|------|---------|------|---------|
+| BRP | `brp_register.json` | 35 persons | `brp` | `ingeschreven-persoon` |
+| KVK | `kvk_register.json` | 16 businesses + 14 branches | `kvk` | `maatschappelijke-activiteit`, `vestiging` |
+| BAG | `bag_register.json` | 32 addresses + 21 objects + 21 buildings | `bag` | `nummeraanduiding`, `verblijfsobject`, `pand` |
+| DSO | `dso_register.json` | 53 records | `dso` | `activiteit`, `locatie`, `omgevingsdocument`, `vergunningaanvraag` |
+| ORI | `ori_register.json` | 115 records | `ori` | `vergadering`, `agendapunt`, `raadsdocument`, `stemming`, `raadslid`, `fractie` |
+
+**Loading all registers:**
+```bash
+docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/brp_register.json
+docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/kvk_register.json
+docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/bag_register.json
+docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/dso_register.json
+docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/ori_register.json
+```
+
+**Or via the API:**
+```bash
+curl -X POST "http://localhost:8080/index.php/apps/openregister/api/registers/import" \
+ -u admin:admin -H "Content-Type: application/json" \
+ -d @openregister/lib/Settings/brp_register.json
+```
+
+**Test data for Procest use cases:**
+- **Case with initiator (BRP)**: BSN `999993653` (Suzanne Moulin) -- link as case initiator via betrokkene
+- **Case with BAG-object**: Use BAG nummeraanduiding records -- link address to bouwvergunning case (REQ-CDV-05b)
+- **VTH with DSO vergunningaanvraag**: Use DSO `vergunningaanvraag` records for omgevingsvergunning intake testing
+- **Legesberekening**: BAG `verblijfsobject` records include `oppervlakte` field for fee calculation
+- **StUF-BG person lookup**: BSN `999993653` to test `npsLv01` query
+- **ORI council data**: Use ORI records to test B&W besluit workflow with raadsinformatie
+
+**Querying mock data:**
+```bash
+# Find person by BSN
+curl "http://localhost:8080/index.php/apps/openregister/api/objects/{brp_register_id}/{person_schema_id}?_search=999993653" -u admin:admin
+
+# Find BAG address
+curl "http://localhost:8080/index.php/apps/openregister/api/objects/{bag_register_id}/{nummeraanduiding_schema_id}?_search=1015" -u admin:admin
+```
+
+**Foundation available:**
+- `SettingsService::loadConfiguration()` can load register JSON files from `lib/Settings/` (currently loads `procest_register.json`).
+- The `InitializeSettings` repair step runs on app install/upgrade and calls `loadConfiguration()`.
+- The GGM at `ggm-openregister/` provides full GGM schemas that could serve as a reference or alternative (955 schemas across 12 registers), but they contain no seed data.
+- OpenCatalogi's `publication_register.json` demonstrates the `@self` seed object pattern in `components.objects`.
+
+---
+
+## Specificity Assessment
+
+**This spec is implementation-ready for the data model.** All schemas are fully defined with property names, types, constraints, and examples. Cross-register relationships are specified with concrete scenarios.
+
+**Strengths:**
+- Complete property tables for all 16 schemas across 5 registers
+- Concrete seed data requirements with minimum record counts
+- Cross-register integrity scenarios with specific field-level mappings
+- BSN 11-proef validation requirement with algorithm specification
+- File structure and loading mechanism aligned with existing Procest patterns
+
+**What needs further research before implementation:**
+1. **BSN generation**: A utility function or lookup table of 25+ valid test BSNs in the `9999xxxx` range that pass the 11-proef is needed. This is straightforward to compute.
+2. **BAG identificatie format**: The 16-digit BAG identification numbers follow a `GGGG-TT-NNNNNNNNNN` pattern where GGGG = gemeentecode, TT = object type. Need to verify correct gemeentecodes for the 5 seed municipalities.
+3. **DSO zaaknummer format**: The actual DSO/Omgevingsloket zaaknummer format may differ from the `OLO-YYYY-NNNNN` pattern used here. Need to verify with IPLO documentation.
+4. **ORI entity alignment**: The ORI project has been evolving; need to verify that the Popolo-based model used here matches the current ORI API output format.
+5. **Multiple register loading**: The current `SettingsService::loadConfiguration()` loads one register file (`procest_register.json`). It may need to be extended to iterate over multiple files, or each base register file could be loaded by a separate repair step.
+
+**Open questions:**
+1. Should base register seed data live in Procest or in OpenRegister? The mock-registers spec in `openregister/openspec/specs/mock-registers/` suggests OpenRegister as the home. However, Procest-specific test scenarios (e.g., families for case testing) argue for Procest ownership. **Recommendation**: Put the files in Procest (it owns the test scenarios), but keep the schema definitions compatible with the OpenRegister mock-registers spec.
+2. Should Pipelinq also load these registers, or should it depend on Procest to seed them? **Recommendation**: Procest seeds them; Pipelinq reads them. This avoids duplicate seeding when both apps are installed.
+3. How large should the dataset be? The spec defines minimums (25 BRP, 15 KVK, etc.) but larger datasets (100+ per register) would better test pagination, faceting, and search performance. Consider a `--extended` flag for the seeder.
+4. Should the DSO and ORI registers be in separate files or combined? **Recommendation**: Separate files (one per register) for maintainability and independent loading.
diff --git a/openspec/specs/case-management/spec.md b/openspec/specs/case-management/spec.md
index b4021254..e8c20145 100644
--- a/openspec/specs/case-management/spec.md
+++ b/openspec/specs/case-management/spec.md
@@ -1,917 +1,917 @@
-# Case Management Specification
-
-## Purpose
-
-Case management is the core capability of Procest. A case represents a coherent body of work with a defined lifecycle, initiation, and result. Cases are governed by configurable **case types** that control behavior: allowed statuses, required fields, processing deadlines, retention rules, and more. Cases follow CMMN 1.1 concepts (CasePlanModel) and are semantically typed as `schema:Project`.
-
-**Standards**: CMMN 1.1 (CasePlanModel), Schema.org (`Project`), ZGW (`Zaak`)
-**Feature tier**: MVP (core case CRUD, list, detail, status, deadline), V1 (sub-cases, confidentiality, result types, document checklist, suspension)
-
-## Data Model
-
-### Case Entity
-
-| Property | Type | CMMN/Schema.org | ZGW Mapping | Required |
-|----------|------|----------------|-------------|----------|
-| `title` | string | `schema:name` | `omschrijving` | Yes |
-| `description` | string | `schema:description` | `toelichting` | No |
-| `identifier` | string | `schema:identifier` | `identificatie` | Auto |
-| `caseType` | reference | CMMN CaseDefinition | `zaaktype` | Yes |
-| `status` | reference | CMMN PlanItem lifecycle | `status` | Yes |
-| `result` | reference | CMMN case outcome | `resultaat` | No |
-| `startDate` | date | `schema:startDate` | `startdatum` | Yes |
-| `endDate` | date | `schema:endDate` | `einddatum` | No |
-| `plannedEndDate` | date | -- | `einddatumGepland` | No |
-| `deadline` | date | -- | `uiterlijkeEinddatumAfdoening` | Auto (from caseType) |
-| `confidentiality` | enum | -- | `vertrouwelijkheidaanduiding` | No (default from caseType) |
-| `assignee` | string | CMMN HumanTask.assignee | -- | No |
-| `priority` | enum | `schema:priority` | -- | No |
-| `parentCase` | reference | CMMN CaseTask | `hoofdzaak` | No |
-| `relatedCases` | array | -- | `relevanteAndereZaken` | No |
-| `geometry` | GeoJSON | `schema:geo` | `zaakgeometrie` | No |
-
-### Case Type Behavioral Controls on Cases
-
-- `deadline` is auto-calculated: `startDate` + `caseType.processingDeadline`
-- `confidentiality` defaults from `caseType.confidentiality`
-- `status` MUST reference a status type linked to the case's case type
-- Only role types linked to the case type are allowed for participant assignment
-- Property definitions linked to the case type MUST be satisfied before reaching required statuses
-- Document types linked to the case type define which documents are expected at each status
-
-### Confidentiality Levels
-
-| Level | ZGW Dutch | Description |
-|-------|-----------|-------------|
-| `public` | openbaar | Publicly accessible |
-| `restricted` | beperkt_openbaar | Restricted public access |
-| `internal` | intern | Internal use only |
-| `case_sensitive` | zaakvertrouwelijk | Case-confidential |
-| `confidential` | vertrouwelijk | Confidential |
-| `highly_confidential` | confidentieel | Highly confidential |
-| `secret` | geheim | Secret |
-| `top_secret` | zeer_geheim | Top secret |
-
-## Requirements
-
----
-
-### REQ-CM-01: Case Creation
-
-**Feature tier**: MVP
-
-The system MUST support creating new cases. Each case MUST be linked to a published, valid case type. The case type controls initial defaults and behavioral constraints.
-
-#### Scenario CM-01a: Create a case with case type selection
-
-- GIVEN a user with case management access
-- AND a published case type "Omgevingsvergunning" with `processingDeadline = "P56D"`, `confidentiality = "internal"`, and status types ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
-- WHEN the user opens the "New Case" form and selects case type "Omgevingsvergunning"
-- AND enters title "Bouwvergunning Keizersgracht 100"
-- AND submits the form
-- THEN the system MUST create an OpenRegister object in the `procest` register with the `case` schema
-- AND the `identifier` MUST be auto-generated (format: `YYYY-NNN`, e.g., "2026-042")
-- AND the `startDate` MUST default to the current date
-- AND the `deadline` MUST be auto-calculated as `startDate + P56D` (e.g., 2026-01-15 + 56 days = 2026-03-12)
-- AND the `confidentiality` MUST default to "internal" (inherited from case type)
-- AND the `status` MUST be set to "Ontvangen" (the first status type by `order`)
-- AND a unique `identifier` MUST be auto-generated
-
-#### Scenario CM-01b: Case type is required at creation
-
-- GIVEN a user opening the "New Case" form
-- WHEN the user attempts to submit without selecting a case type
-- THEN the system MUST reject the submission
-- AND the system MUST display a validation error: "Case type is required"
-
-#### Scenario CM-01c: Title is required at creation
-
-- GIVEN a user opening the "New Case" form with case type "Klacht behandeling" selected
-- WHEN the user attempts to submit without entering a title
-- THEN the system MUST reject the submission
-- AND the system MUST display a validation error: "Title is required"
-
-#### Scenario CM-01d: Cannot create case with draft case type
-
-- GIVEN a case type "Bezwaarschrift" with `isDraft = true`
-- WHEN a user attempts to create a case of type "Bezwaarschrift"
-- THEN the system MUST reject the creation
-- AND the system MUST display an error: "Cannot create a case with a draft case type. The case type must be published first."
-
-#### Scenario CM-01e: Cannot create case with expired case type
-
-- GIVEN a case type "Bouwvergunning Oud" with `validUntil = "2025-12-31"`
-- AND today is "2026-02-25"
-- WHEN a user attempts to create a case of this type
-- THEN the system MUST reject the creation
-- AND the system MUST display an error: "Cannot create a case with an expired case type. The case type was valid until 2025-12-31."
-
-#### Scenario CM-01f: Cannot create case with case type not yet valid
-
-- GIVEN a case type "Nieuwe Subsidie" with `validFrom = "2027-01-01"`
-- AND today is "2026-02-25"
-- WHEN a user attempts to create a case of this type
-- THEN the system MUST reject the creation
-- AND the system MUST display an error: "Cannot create a case with a case type that is not yet valid. The case type is valid from 2027-01-01."
-
-#### Scenario CM-01g: Default case type pre-selected
-
-- GIVEN a case type "Omgevingsvergunning" is marked as the default case type in admin settings
-- WHEN a user opens the "New Case" form
-- THEN the case type dropdown MUST pre-select "Omgevingsvergunning"
-- AND the user MAY change the selection to another published, valid case type
-
----
-
-### REQ-CM-02: Case Update
-
-**Feature tier**: MVP
-
-The system MUST support updating case properties. Changes MUST be recorded in the audit trail.
-
-#### Scenario CM-02a: Update case description
-
-- GIVEN an existing case "Bouwvergunning Keizersgracht 100" with identifier "2026-042"
-- WHEN the user updates the description to "Verbouwing woonhuis, 3 bouwlagen, 180 m2"
-- THEN the system MUST update the OpenRegister object
-- AND the audit trail MUST record: user, timestamp, field changed, old value, new value
-
-#### Scenario CM-02b: Update case priority
-
-- GIVEN an existing case with priority "normal"
-- WHEN the handler changes the priority to "high"
-- THEN the system MUST update the `priority` field
-- AND the audit trail MUST record the change
-
-#### Scenario CM-02c: Reassign case handler
-
-- GIVEN a case assigned to "Jan de Vries"
-- WHEN an authorized user reassigns the case to "Maria van den Berg"
-- THEN the `assignee` field MUST be updated to "Maria van den Berg"
-- AND the audit trail MUST record: "Handler changed from Jan de Vries to Maria van den Berg"
-
----
-
-### REQ-CM-03: Case Deletion
-
-**Feature tier**: MVP
-
-The system MUST support deleting cases. Deletion SHOULD be restricted to cases without a final status.
-
-#### Scenario CM-03a: Delete a case in initial status
-
-- GIVEN a case "Testmelding" with status "Ontvangen" and no linked tasks, decisions, or sub-cases
-- WHEN an authorized user deletes the case
-- THEN the system MUST remove the OpenRegister object
-- AND the system MUST display a confirmation dialog before deletion
-
-#### Scenario CM-03b: Warn before deleting case with linked objects
-
-- GIVEN a case with 3 linked tasks and 1 linked decision
-- WHEN an authorized user attempts to delete the case
-- THEN the system MUST display a warning: "This case has 3 tasks and 1 decision. Deleting the case will also remove these linked objects."
-- AND the user MUST confirm before proceeding
-
----
-
-### REQ-CM-04: Case List View
-
-**Feature tier**: MVP
-
-The system MUST provide a list view of all cases with search, sort, filter, and pagination capabilities. See wireframe 3.2 (Case List View) in DESIGN-REFERENCES.md.
-
-#### Scenario CM-04a: Default case list
-
-- GIVEN 24 open cases in the system
-- WHEN the user navigates to the Cases page
-- THEN the system MUST display a table with columns: ID, Title, Type, Status, Deadline, Handler
-- AND the list MUST be paginated at 20 items per page by default
-- AND overdue cases MUST be visually highlighted (red indicator)
-
-#### Scenario CM-04b: Filter by case type
-
-- GIVEN cases of types "Omgevingsvergunning" (10), "Subsidieaanvraag" (7), "Klacht" (4), "Melding" (3)
-- WHEN the user selects filter "Type: Omgevingsvergunning"
-- THEN only the 10 cases of type "Omgevingsvergunning" MUST be shown
-
-#### Scenario CM-04c: Filter by status
-
-- GIVEN cases in statuses "Ontvangen" (8), "In behandeling" (6), "Besluitvorming" (5), "Afgehandeld" (5)
-- WHEN the user selects filter "Status: In behandeling"
-- THEN only the 6 cases with status "In behandeling" MUST be shown
-
-#### Scenario CM-04d: Filter by handler
-
-- GIVEN cases assigned to "Jan de Vries" (8), "Maria van den Berg" (6), unassigned (10)
-- WHEN the user selects filter "Handler: Jan de Vries"
-- THEN only Jan's 8 cases MUST be shown
-
-#### Scenario CM-04e: Filter by priority
-
-- GIVEN cases with priorities "high" (4), "normal" (16), "low" (4)
-- WHEN the user selects filter "Priority: high"
-- THEN only the 4 high-priority cases MUST be shown
-
-#### Scenario CM-04f: Filter overdue cases
-
-- GIVEN 3 cases past their deadline
-- WHEN the user selects filter "Overdue: Yes"
-- THEN only the 3 overdue cases MUST be shown
-
-#### Scenario CM-04g: Search cases by keyword
-
-- GIVEN cases with titles "Bouwvergunning Keizersgracht 100", "Bouwvergunning Prinsengracht 50", "Subsidie innovatie"
-- WHEN the user searches for "Keizersgracht"
-- THEN only "Bouwvergunning Keizersgracht 100" MUST be shown
-- AND search MUST match against `title` and `description` fields
-
-#### Scenario CM-04h: Sort by deadline ascending
-
-- GIVEN multiple cases with different deadlines
-- WHEN the user sorts by "Deadline" ascending
-- THEN cases MUST be ordered with the nearest deadline first
-
-#### Scenario CM-04i: Paginate case list
-
-- GIVEN 24 cases matching the current filters
-- AND page size is 20
-- WHEN the user views the case list
-- THEN page 1 MUST show cases 1-20
-- AND the system MUST display "Showing 20 of 24 cases -- Page 1 of 2"
-- AND a "Next" button MUST navigate to page 2 (cases 21-24)
-
----
-
-### REQ-CM-05: Quick Status Change from List
-
-**Feature tier**: MVP
-
-The system MUST support changing a case's status directly from the case list view without opening the detail page. See wireframe 3.2 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-05a: Quick status change via dropdown
-
-- GIVEN a case "Bouwvergunning Keizersgracht 100" with status "Ontvangen" in the case list
-- AND the case type defines statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
-- WHEN the user clicks the status cell/dropdown for this case
-- THEN a dropdown MUST appear showing only the statuses defined by the case type
-- AND the current status MUST be visually indicated (e.g., checked or highlighted)
-
-#### Scenario CM-05b: Quick status change succeeds
-
-- GIVEN the status dropdown is open for case "2026-042"
-- WHEN the user selects "In behandeling"
-- THEN the case status MUST be updated to "In behandeling"
-- AND the list row MUST update without a full page reload
-- AND the audit trail MUST record the status change
-
-#### Scenario CM-05c: Quick status change blocked by missing properties
-
-- GIVEN a case type "Omgevingsvergunning" with property "Kadastraal nummer" required at status "In behandeling"
-- AND the case has not filled "Kadastraal nummer"
-- WHEN the user attempts a quick status change to "In behandeling"
-- THEN the system MUST reject the change
-- AND display a message: "Cannot advance to 'In behandeling': required property 'Kadastraal nummer' is missing. Open the case to complete the required fields."
-
----
-
-### REQ-CM-06: Case Detail View
-
-**Feature tier**: MVP
-
-The system MUST provide a comprehensive detail view for each case. See wireframe 3.3 (Case Detail View) in DESIGN-REFERENCES.md. The detail view MUST include: status timeline, case info panel, deadline and timing panel, participants panel, custom properties panel, required documents checklist, tasks section, decisions section, activity timeline, and sub-cases section.
-
-#### Scenario CM-06a: Case info panel
-
-- GIVEN a case "Bouwvergunning Keizersgracht 100" of type "Omgevingsvergunning"
-- WHEN the user navigates to the case detail view
-- THEN the case info panel MUST display: title, type, priority, confidentiality level, identifier, and creation date
-- AND a "Change Status" dropdown MUST be available
-
-#### Scenario CM-06b: Deadline and timing panel
-
-- GIVEN a case with `startDate = "2026-01-15"`, `deadline = "2026-03-12"`, `processingDeadline = "P56D"` (from case type)
-- AND today is "2026-02-25" (15 days remaining)
-- WHEN the user views the case detail
-- THEN the deadline panel MUST display: "Started: Jan 15, 2026", "Deadline: Mar 12, 2026"
-- AND the system MUST display "15 days remaining"
-- AND the processing deadline MUST show "56 days"
-- AND the days elapsed MUST show "41"
-
-#### Scenario CM-06c: Deadline countdown -- overdue
-
-- GIVEN a case with `deadline = "2026-02-20"`
-- AND today is "2026-02-25"
-- THEN the system MUST display "5 DAYS OVERDUE" with a red visual indicator
-- AND the deadline text MUST be styled in red/error state
-
-#### Scenario CM-06d: Deadline countdown -- on track
-
-- GIVEN a case with `deadline = "2026-03-15"`
-- AND today is "2026-02-25"
-- THEN the system MUST display "18 days remaining" with a neutral/green indicator
-
-#### Scenario CM-06e: Extension button visibility
-
-- GIVEN a case type with `extensionAllowed = true` and `extensionPeriod = "P28D"`
-- WHEN the user views the deadline panel
-- THEN a "Request Extension" button MUST be visible
-- AND the panel MUST show "Extension: allowed (+28 days)"
-
-#### Scenario CM-06f: Extension button hidden when not allowed
-
-- GIVEN a case type with `extensionAllowed = false`
-- WHEN the user views the deadline panel
-- THEN no "Request Extension" button MUST be displayed
-- AND the panel MUST show "Extension: not allowed"
-
----
-
-### REQ-CM-07: Status Timeline Visualization
-
-**Feature tier**: MVP
-
-The case detail view MUST display a visual status timeline showing all statuses defined by the case type. Passed statuses are filled, the current status is highlighted, and future statuses are greyed out. See wireframe 3.3 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-07a: Status timeline with current status
-
-- GIVEN a case of type "Omgevingsvergunning" with ordered statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
-- AND the case is currently at "In behandeling"
-- WHEN the user views the case detail
-- THEN the status timeline MUST display 4 dots/nodes in order
-- AND "Ontvangen" MUST appear as passed (filled dot with date)
-- AND "In behandeling" MUST appear as current (highlighted/active dot)
-- AND "Besluitvorming" and "Afgehandeld" MUST appear as future (greyed dots)
-
-#### Scenario CM-07b: Status timeline with dates
-
-- GIVEN a case that transitioned from "Ontvangen" (Jan 15) to "In behandeling" (Feb 1)
-- WHEN the user views the status timeline
-- THEN the date "Jan 15" MUST appear beneath the "Ontvangen" node
-- AND the date "Feb 1" MUST appear beneath the "In behandeling" node
-- AND future statuses MUST NOT show dates
-
-#### Scenario CM-07c: Status timeline at final status
-
-- GIVEN a case at status "Afgehandeld" (which has `isFinal = true`)
-- WHEN the user views the status timeline
-- THEN all dots MUST appear as passed/completed (filled)
-- AND the timeline MUST visually indicate the case is complete
-
----
-
-### REQ-CM-08: Participants Panel
-
-**Feature tier**: MVP (handler assignment), V1 (full role types)
-
-The case detail view MUST display assigned participants with their roles. See wireframe 3.3 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-08a: Display participants
-
-- GIVEN a case with roles: Handler = "Jan de Vries", Initiator = "Petra Jansen (Acme Corp)", Advisor = "Dr. K. Bakker"
-- WHEN the user views the participants panel
-- THEN each participant MUST be shown with their role label and name
-- AND the handler MUST have a "Reassign" action
-- AND an "Add Participant" button MUST be displayed
-
-#### Scenario CM-08b: Add participant with role type restriction (V1)
-
-- GIVEN a case of type "Omgevingsvergunning" with allowed role types ["Aanvrager", "Behandelaar", "Technisch adviseur", "Beslisser"]
-- WHEN the user clicks "Add Participant"
-- THEN the role selection MUST only show roles defined by the case type
-- AND the user MUST NOT be able to assign a role type not in the case type's list
-
----
-
-### REQ-CM-09: Custom Properties Panel
-
-**Feature tier**: V1
-
-The case detail view MUST display custom properties defined by the case type. See wireframe 3.3 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-09a: Display custom properties
-
-- GIVEN a case of type "Omgevingsvergunning" with property definitions ["Kadastraal nummer" (text), "Bouwkosten" (number), "Oppervlakte" (number), "Bouwlagen" (number)]
-- AND the case has values: Kadastraal nummer = "AMS04-A-1234", Bouwkosten = 250000, Oppervlakte = 180, Bouwlagen = 3
-- WHEN the user views the custom properties panel
-- THEN all 4 properties MUST be displayed with their values
-- AND an "Edit Properties" button MUST be available
-
-#### Scenario CM-09b: Empty custom properties
-
-- GIVEN a case of type "Omgevingsvergunning" with 4 property definitions
-- AND no property values have been filled
-- WHEN the user views the custom properties panel
-- THEN all 4 properties MUST be displayed with empty/placeholder values
-- AND the panel SHOULD indicate "0 of 4 properties filled"
-
----
-
-### REQ-CM-10: Required Documents Checklist
-
-**Feature tier**: V1
-
-The case detail view MUST display a checklist of required documents defined by the case type, showing which are present and which are missing. See wireframe 3.3 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-10a: Document checklist with mixed completion
-
-- GIVEN a case of type "Omgevingsvergunning" with required document types:
- - "Bouwtekening" (incoming, required at "In behandeling")
- - "Constructieberekening" (incoming, required at "In behandeling")
- - "Situatietekening" (incoming, required at "In behandeling")
- - "Welstandsadvies" (internal, required at "Besluitvorming")
- - "Vergunningsbesluit" (outgoing, required at "Afgehandeld")
-- AND files uploaded: Bouwtekening (Jan 16), Constructieberekening (Jan 20), Situatietekening (Jan 22)
-- WHEN the user views the documents panel
-- THEN the header MUST show "3/5 complete"
-- AND Bouwtekening, Constructieberekening, Situatietekening MUST show a checkmark with upload date
-- AND Welstandsadvies MUST show a missing indicator with "required at: Besluitvorming"
-- AND Vergunningsbesluit MUST show a missing indicator with "required at: Afgehandeld"
-
-#### Scenario CM-10b: All documents present
-
-- GIVEN a case where all 5 required documents have been uploaded
-- WHEN the user views the documents panel
-- THEN the header MUST show "5/5 complete"
-- AND all items MUST show a checkmark
-
-#### Scenario CM-10c: No required documents defined
-
-- GIVEN a case type "Melding" with no document types defined
-- WHEN the user views the case detail
-- THEN the documents panel SHOULD either be hidden or show "No required documents for this case type"
-
----
-
-### REQ-CM-11: Tasks Section
-
-**Feature tier**: MVP
-
-The case detail view MUST display tasks linked to the case. See wireframe 3.3 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-11a: Display tasks with completion count
-
-- GIVEN a case with 5 tasks: 2 completed, 1 active, 2 available
-- WHEN the user views the tasks section
-- THEN the header MUST show "TASKS 3/5" (or similar completion indicator)
-- AND each task MUST show: title, status icon, due date (if set), assignee (if set)
-- AND completed tasks MUST show a checkmark
-- AND the active task MUST be visually distinct (e.g., spinner icon)
-- AND an "Add Task" button MUST be available
-
-#### Scenario CM-11b: No tasks
-
-- GIVEN a case with no linked tasks
-- WHEN the user views the tasks section
-- THEN the section MUST show "No tasks" or an empty state
-- AND the "Add Task" button MUST still be available
-
----
-
-### REQ-CM-12: Decisions Section
-
-**Feature tier**: V1
-
-The case detail view MUST display decisions linked to the case.
-
-#### Scenario CM-12a: Display decisions
-
-- GIVEN a case with 1 decision: "Vergunning verleend" decided on Feb 20 by "Jan de Vries"
-- WHEN the user views the decisions section
-- THEN the decision MUST show: title, decided date, decided by
-- AND an "Add Decision" button MUST be available
-
-#### Scenario CM-12b: No decisions
-
-- GIVEN a case with no decisions
-- WHEN the user views the decisions section
-- THEN the section MUST show "(no decisions yet)"
-- AND an "Add Decision" button MUST be available
-
----
-
-### REQ-CM-13: Activity Timeline
-
-**Feature tier**: MVP
-
-The case detail view MUST display an activity timeline showing all events related to the case in chronological order (newest first). See wireframe 3.3 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-13a: Activity timeline entries
-
-- GIVEN a case "2026-042" with the following events:
- - Feb 25: Task "Review docs" assigned to Jan de Vries
- - Feb 20: Deadline passed (case is now overdue)
- - Feb 1: Status changed to "In behandeling" by Jan de Vries
- - Jan 22: Document "Situatietekening" uploaded by Petra Jansen
- - Jan 15: Case created
-- WHEN the user views the activity timeline
-- THEN all events MUST be displayed in reverse chronological order
-- AND each entry MUST show: date, event description, actor (if applicable)
-- AND deadline-passed events MUST be visually distinct (warning style)
-
-#### Scenario CM-13b: Add note to activity
-
-- GIVEN a case detail view with an activity timeline
-- WHEN the user clicks "Add note" and enters "Wachten op welstandsadvies van externe partij"
-- THEN the note MUST appear in the timeline with the current date and the user's name
-- AND the note MUST be stored via Nextcloud's ICommentsManager
-
----
-
-### REQ-CM-14: Status Change
-
-**Feature tier**: MVP
-
-The system MUST support changing a case's status. Status changes MUST respect case type constraints: only statuses defined by the case type are allowed, required properties MUST be satisfied, and required documents MUST be present.
-
-#### Scenario CM-14a: Valid status change
-
-- GIVEN a case of type "Omgevingsvergunning" currently at "Ontvangen"
-- AND the case type defines statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
-- WHEN the handler changes the status to "In behandeling"
-- THEN the status MUST be updated
-- AND the audit trail MUST record: who (handler name), when (timestamp), from "Ontvangen" to "In behandeling"
-
-#### Scenario CM-14b: Reject status not in case type
-
-- GIVEN a case of type "Omgevingsvergunning" with statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
-- WHEN an API request attempts to set status to "Bezwaar" (not in this case type's list)
-- THEN the system MUST reject the change
-- AND return an error: "Status 'Bezwaar' is not defined for case type 'Omgevingsvergunning'"
-
-#### Scenario CM-14c: Status change blocked by required properties (V1)
-
-- GIVEN a case of type "Omgevingsvergunning"
-- AND property "Kadastraal nummer" has `requiredAtStatus` pointing to "In behandeling"
-- AND the case has not filled "Kadastraal nummer"
-- WHEN the user attempts to change status to "In behandeling"
-- THEN the system MUST reject the change
-- AND display: "Cannot advance to 'In behandeling': required properties missing: Kadastraal nummer"
-
-#### Scenario CM-14d: Status change blocked by required documents (V1)
-
-- GIVEN a case of type "Omgevingsvergunning"
-- AND document type "Welstandsadvies" has `requiredAtStatus` pointing to "Besluitvorming"
-- AND no file of type "Welstandsadvies" has been uploaded
-- WHEN the user attempts to change status to "Besluitvorming"
-- THEN the system MUST reject the change
-- AND display: "Cannot advance to 'Besluitvorming': required documents missing: Welstandsadvies"
-
-#### Scenario CM-14e: Status change triggers initiator notification
-
-- GIVEN a case with an initiator "Petra Jansen"
-- AND the target status type "In behandeling" has `notifyInitiator = true` and `notificationText = "Uw zaak is in behandeling genomen"`
-- WHEN the handler changes the case to "In behandeling"
-- THEN the system MUST send a notification to the initiator
-- AND the notification MUST contain the text "Uw zaak is in behandeling genomen"
-
-#### Scenario CM-14f: Status change to final status sets endDate
-
-- GIVEN a case currently at "Besluitvorming"
-- AND "Afgehandeld" is the final status (`isFinal = true`)
-- WHEN the handler changes the status to "Afgehandeld"
-- THEN the case `endDate` MUST be set to the current date
-- AND the case MUST be marked as closed
-- AND no further status changes SHOULD be allowed without explicit reopening
-
----
-
-### REQ-CM-15: Case Result Recording
-
-**Feature tier**: MVP (basic result), V1 (result types from case type)
-
-The system MUST support recording a result when closing a case.
-
-#### Scenario CM-15a: Record result from case type's allowed results (V1)
-
-- GIVEN a case of type "Omgevingsvergunning" with result types ["Vergunning verleend", "Vergunning geweigerd", "Ingetrokken"]
-- WHEN the handler closes the case and selects result "Vergunning verleend"
-- THEN a Result object MUST be created and linked to the case
-- AND the result MUST reference the "Vergunning verleend" result type
-- AND the result type's archival rules MUST be recorded: `archiveAction = "retain"`, `retentionPeriod = "P20Y"`
-
-#### Scenario CM-15b: Result required at final status
-
-- GIVEN a case type "Omgevingsvergunning" where the final status "Afgehandeld" requires a result
-- WHEN the handler attempts to set status to "Afgehandeld" without selecting a result
-- THEN the system MUST prompt for a result selection
-- AND the result dropdown MUST only show result types defined by the case type
-
-#### Scenario CM-15c: Result triggers archival rules (V1)
-
-- GIVEN a result type "Vergunning geweigerd" with `archiveAction = "destroy"` and `retentionPeriod = "P10Y"` and `retentionDateSource = "case_completed"`
-- WHEN a case is closed with this result
-- THEN the system MUST record: archive action = destroy, retention until = endDate + 10 years
-- AND the audit trail MUST record the archival determination
-
----
-
-### REQ-CM-16: Case Deadline Extension
-
-**Feature tier**: MVP
-
-The system MUST support extending a case's deadline when the case type allows it.
-
-#### Scenario CM-16a: Extend deadline when allowed
-
-- GIVEN a case of type "Omgevingsvergunning" with `extensionAllowed = true` and `extensionPeriod = "P28D"`
-- AND the case has `deadline = "2026-03-12"`
-- WHEN the handler requests an extension
-- THEN the deadline MUST be extended to "2026-04-09" (original + 28 days)
-- AND the audit trail MUST record: "Deadline extended from 2026-03-12 to 2026-04-09 by [handler name]"
-- AND the extension reason SHOULD be captured
-
-#### Scenario CM-16b: Reject extension when not allowed
-
-- GIVEN a case of type "Klacht behandeling" with `extensionAllowed = false`
-- WHEN the handler attempts to extend the deadline
-- THEN the system MUST reject the request
-- AND display: "Deadline extension is not allowed for case type 'Klacht behandeling'"
-
-#### Scenario CM-16c: Extension limit (single extension)
-
-- GIVEN a case that has already been extended once
-- WHEN the handler attempts a second extension
-- THEN the system SHOULD reject the request (default: one extension allowed)
-- AND display: "This case has already been extended"
-
----
-
-### REQ-CM-17: Case Suspension
-
-**Feature tier**: V1
-
-The system SHOULD support suspending a case when the case type allows it. Suspension pauses the deadline countdown.
-
-#### Scenario CM-17a: Suspend a case
-
-- GIVEN a case of type "Omgevingsvergunning" with `suspensionAllowed = true`
-- AND the case has `deadline = "2026-03-12"` and 15 days remaining
-- WHEN the handler suspends the case with reason "Wachten op aanvullende gegevens van aanvrager"
-- THEN the case MUST enter a suspended state
-- AND the deadline countdown MUST pause (remaining days frozen at 15)
-- AND the audit trail MUST record: suspension start, reason, who suspended
-
-#### Scenario CM-17b: Resume a suspended case
-
-- GIVEN a case suspended for 10 days with 15 days remaining at suspension
-- WHEN the handler resumes the case
-- THEN the deadline MUST be recalculated: new deadline = today + 15 remaining days
-- AND the audit trail MUST record: suspension end, total suspended duration (10 days), who resumed
-
-#### Scenario CM-17c: Reject suspension when not allowed
-
-- GIVEN a case of type "Melding" with `suspensionAllowed = false`
-- WHEN the handler attempts to suspend the case
-- THEN the system MUST reject the request
-- AND display: "Suspension is not allowed for case type 'Melding'"
-
----
-
-### REQ-CM-18: Sub-Cases
-
-**Feature tier**: V1
-
-The system SHOULD support parent/child case hierarchies. A sub-case is a full case linked to a parent case.
-
-#### Scenario CM-18a: Create a sub-case
-
-- GIVEN an existing case "Bouwproject Centrum" (identifier "2026-042")
-- WHEN the user clicks "Create Sub-case" and selects case type "Omgevingsvergunning" with title "Vergunning fundering"
-- THEN a new case MUST be created with `parentCase` referencing "2026-042"
-- AND the sub-case MUST have its own lifecycle, deadline, and status independent of the parent
-
-#### Scenario CM-18b: Sub-cases displayed on parent
-
-- GIVEN a parent case "2026-042" with 2 sub-cases: "Vergunning fundering" (active) and "Vergunning gevel" (completed)
-- WHEN the user views the parent case detail
-- THEN the sub-cases section MUST list both sub-cases with their status and deadline
-- AND each sub-case MUST be clickable to navigate to its detail view
-
-#### Scenario CM-18c: Navigate from sub-case to parent
-
-- GIVEN a sub-case "Vergunning fundering" with parent "Bouwproject Centrum"
-- WHEN the user views the sub-case detail
-- THEN a breadcrumb or link MUST be displayed: "Parent case: Bouwproject Centrum (2026-042)"
-- AND clicking it MUST navigate to the parent case detail
-
-#### Scenario CM-18d: Sub-case type restrictions (V1)
-
-- GIVEN a parent case type "Bouwproject" with `subCaseTypes` referencing ["Omgevingsvergunning", "Sloopvergunning"]
-- WHEN the user creates a sub-case
-- THEN the case type selection MUST only show "Omgevingsvergunning" and "Sloopvergunning"
-- AND the user MUST NOT be able to select a case type not in the parent's `subCaseTypes` list
-
----
-
-### REQ-CM-19: Confidentiality Levels
-
-**Feature tier**: V1
-
-The system SHOULD support confidentiality levels on cases, defaulting from the case type.
-
-#### Scenario CM-19a: Inherit confidentiality from case type
-
-- GIVEN a case type "Omgevingsvergunning" with `confidentiality = "internal"`
-- WHEN a new case is created
-- THEN the case `confidentiality` MUST default to "internal"
-
-#### Scenario CM-19b: Override confidentiality on case
-
-- GIVEN a case with default confidentiality "internal"
-- WHEN the handler changes the confidentiality to "confidential"
-- THEN the case `confidentiality` MUST be updated to "confidential"
-- AND the audit trail MUST record the change
-
-#### Scenario CM-19c: Confidentiality level options
-
-- GIVEN the confidentiality enum with 8 levels (public through top_secret)
-- WHEN the user views the confidentiality dropdown on a case
-- THEN all 8 levels MUST be available for selection
-- AND the levels MUST be ordered from least to most restrictive
-
----
-
-### REQ-CM-20: Case Validation Rules
-
-**Feature tier**: MVP
-
-The system MUST enforce validation rules when creating or modifying cases.
-
-#### Scenario CM-20a: Title is required
-
-- GIVEN a case creation or update form
-- WHEN the user submits with an empty title
-- THEN the system MUST reject the submission with error: "Title is required"
-
-#### Scenario CM-20b: Case type is required
-
-- GIVEN a case creation form
-- WHEN the user submits without selecting a case type
-- THEN the system MUST reject the submission with error: "Case type is required"
-
-#### Scenario CM-20c: Case type must be published
-
-- GIVEN a case type "Bezwaarschrift" with `isDraft = true`
-- WHEN a user submits a case creation with this type
-- THEN the system MUST reject with error: "Case type 'Bezwaarschrift' is a draft and cannot be used to create cases"
-
-#### Scenario CM-20d: Case type must be within validity window
-
-- GIVEN a case type with `validFrom = "2026-06-01"` and today is "2026-02-25"
-- WHEN a user submits a case creation with this type
-- THEN the system MUST reject with error: "Case type is not yet valid (valid from 2026-06-01)"
-
-#### Scenario CM-20e: Start date must not be in the future
-
-- GIVEN a case creation form
-- WHEN the user sets startDate to a date in the future
-- THEN the system SHOULD warn but MAY allow (some jurisdictions allow future-dated cases)
-
----
-
-### REQ-CM-21: Case Deadline Countdown Display
-
-**Feature tier**: MVP
-
-The system MUST display deadline countdowns on cases across all views (list, detail, My Work). See wireframes 3.2 and 3.3 in DESIGN-REFERENCES.md.
-
-#### Scenario CM-21a: Days remaining display
-
-- GIVEN a case with `deadline = "2026-03-15"` and today is "2026-02-25"
-- WHEN displayed in the case list or detail view
-- THEN the system MUST show "18 days" (or "18 days remaining")
-- AND the indicator MUST use a neutral/positive style (e.g., no color or green)
-
-#### Scenario CM-21b: Due tomorrow
-
-- GIVEN a case with deadline = tomorrow
-- WHEN displayed in any view
-- THEN the system MUST show "1 day" (or "Due tomorrow")
-- AND the indicator MUST use a warning style (e.g., yellow/amber)
-
-#### Scenario CM-21c: Overdue display
-
-- GIVEN a case with `deadline = "2026-02-20"` and today is "2026-02-25"
-- WHEN displayed in any view
-- THEN the system MUST show "5 days overdue" (or "5d overdue")
-- AND the indicator MUST use an error/danger style (e.g., red)
-
-#### Scenario CM-21d: Due today
-
-- GIVEN a case with deadline = today
-- WHEN displayed in any view
-- THEN the system MUST show "Due today"
-- AND the indicator MUST use a warning style
-
----
-
-### REQ-CM-22: Audit Trail
-
-**Feature tier**: MVP
-
-The system MUST maintain a complete audit trail for all case modifications. The audit trail is published via Nextcloud's Activity system (`OCP\Activity\IManager`).
-
-#### Scenario CM-22a: Status change audit entry
-
-- GIVEN a case "2026-042"
-- WHEN the handler changes status from "Ontvangen" to "In behandeling"
-- THEN the audit trail MUST record: event type "case_status_change", user "Jan de Vries", timestamp, from status "Ontvangen", to status "In behandeling"
-
-#### Scenario CM-22b: Property change audit entry
-
-- GIVEN a case "2026-042"
-- WHEN the user changes description from "Verbouwing" to "Verbouwing woonhuis, 3 bouwlagen"
-- THEN the audit trail MUST record: event type "case_update", user, timestamp, field "description", old value, new value
-
-#### Scenario CM-22c: Deadline extension audit entry
-
-- GIVEN a case "2026-042"
-- WHEN the handler extends the deadline
-- THEN the audit trail MUST record: event type "case_extension", user, timestamp, old deadline, new deadline, reason
-
-#### Scenario CM-22d: Case creation audit entry
-
-- GIVEN a user creating a new case
-- WHEN the case is successfully created
-- THEN the audit trail MUST record: event type "case_created", user, timestamp, case type, initial status, calculated deadline
-
----
-
-## UI References
-
-- **Case List View**: See wireframe 3.2 in DESIGN-REFERENCES.md
-- **Case Detail View**: See wireframe 3.3 in DESIGN-REFERENCES.md (status timeline, info panel, deadline panel, participants, custom properties, document checklist, tasks, decisions, activity timeline, sub-cases)
-- **My Work View**: See wireframe 3.5 in DESIGN-REFERENCES.md (overdue / due this week / upcoming sections)
-- **Dashboard**: See wireframe 3.1 in DESIGN-REFERENCES.md (case count widgets, status distribution, overdue list)
-
-## Dependencies
-
-- **Case Types spec** (`../case-types/spec.md`): Case type MUST be published and valid to create cases. Case type controls statuses, deadlines, confidentiality defaults, document types, property definitions, result types, and role types.
-- **OpenRegister**: All case data is stored as OpenRegister objects in the `procest` register under the `case` schema.
-- **Nextcloud Activity**: Audit trail events are published via `OCP\Activity\IManager`.
-- **Nextcloud Comments**: Case notes use `OCP\Comments\ICommentsManager`.
-- **Nextcloud Files**: Document uploads reference Nextcloud file IDs via `OCP\Files\IRootFolder`.
-
-### Current Implementation Status
-
-**Substantially implemented (MVP).** Core case management functionality is in place.
-
-**Implemented:**
-- Case CRUD via OpenRegister object store (`src/store/modules/object.js` using `createObjectStore` with filesPlugin, auditTrailsPlugin, relationsPlugin).
-- Case list view (`src/views/cases/CaseList.vue`) using `CnIndexPage` with columns, sorting (default by deadline asc), pagination, row click navigation, selectable rows, and `QuickStatusDropdown` for inline status changes.
-- Case detail view (`src/views/cases/CaseDetail.vue`) using `CnDetailPage` with sidebar, save/delete actions, status change dropdown with result prompt for final status.
-- Case creation dialog (`src/views/cases/CaseCreateDialog.vue`) with case type selection.
-- Status timeline visualization (`src/views/cases/components/StatusTimeline.vue`) showing passed/current/future status dots with dates.
-- Quick status change from list (`src/views/cases/components/QuickStatusDropdown.vue`).
-- Deadline panel (`src/views/cases/components/DeadlinePanel.vue`) with countdown, overdue display, extension info and request button.
-- Participants panel (`src/views/cases/components/ParticipantsSection.vue`) with role groups, add participant dialog, handler assignment.
-- Activity timeline (`src/views/cases/components/ActivityTimeline.vue`) with add note, chronological events.
-- Result section (`src/views/cases/components/ResultSection.vue`).
-- Case validation utilities (`src/utils/caseValidation.js`).
-- Case helper utilities (`src/utils/caseHelpers.js`) with `formatDeadlineCountdown`, `isCaseOverdue`, `formatDateShort`.
-- Duration helpers (`src/utils/durationHelpers.js`) for ISO 8601 duration display.
-- ZGW Zaken API compatibility via `ZrcController` (`lib/Controller/ZrcController.php`) and `ZgwZrcRulesService` (`lib/Service/ZgwZrcRulesService.php`) handling zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten, zaakobjecten, klantcontacten.
-- ZGW business rules enforcement (`lib/Service/ZgwBusinessRulesService.php`, `lib/Service/ZgwRulesBase.php`).
-- OpenRegister schemas for case (`case_schema`), status (`status_schema`), statusRecord (`status_record_schema`), role (`role_schema`), result (`result_schema`), caseProperty (`case_property_schema`), caseDocument (`case_document_schema`), caseObject (`case_object_schema`).
-- Router with case routes: `/cases` (list), `/cases/:id` (detail).
-- Overdue case visual highlighting in case list (via `getRowClass` and `getDeadlineClass`).
-
-**Not yet implemented or partial:**
-- REQ-CM-09: Custom properties panel in case detail (schema exists but no property editor UI in case detail).
-- REQ-CM-10: Required documents checklist (document types exist but no checklist UI matching uploaded files against requirements).
-- REQ-CM-14c/d: Status change blocking by required properties or documents (V1).
-- REQ-CM-14e: Status change triggering initiator notification (schema supports it but notification delivery not confirmed).
-- REQ-CM-17: Case suspension with deadline pause/resume (V1).
-- REQ-CM-18: Sub-cases / parent-child relationships (V1).
-- REQ-CM-19: Confidentiality level enforcement (field exists in schema but no access control enforcement).
-- REQ-CM-22: Audit trail via Nextcloud Activity (`OCP\Activity\IManager`) -- not confirmed as implemented; audit trails plugin exists in object store but integration with Nextcloud Activity system unclear.
-- Case search (keyword search against title and description).
-- Filter by priority, handler, overdue status in case list.
-
-### Standards & References
-
-- **ZGW Zaken API (VNG)**: Full compatibility layer via `ZrcController` and `ZgwZrcRulesService` implementing VNG Zaken API patterns (zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten).
-- **CMMN 1.1**: Case modeled as CasePlanModel with HumanTask, Milestone, and case lifecycle concepts.
-- **Schema.org**: Case typed as `schema:Project` with `schema:name`, `schema:identifier`, `schema:startDate`, `schema:endDate`.
-- **ISO 8601**: Duration format for processing deadlines, extension periods.
-- **WCAG AA**: Accessible case list and detail views required.
-- **GEMMA**: Zaakgericht werken reference architecture compliance.
-- **Archiefwet**: Case result types with archival rules (retain/destroy, retention period).
-- **Awb**: Administrative law requirements for case handling deadlines and notifications.
-
-### Specificity Assessment
-
-This is the most detailed spec in the set -- highly implementation-ready with concrete data models, field mappings, and exhaustive scenarios.
-
-**Strengths:** Complete data model with CMMN/Schema.org/ZGW triple mapping. 22 requirements with detailed Gherkin scenarios. Clear feature tier separation. Explicit validation rules.
-
-**Missing/Ambiguous:**
-- No specification of case identifier format generation logic (the spec says `YYYY-NNN` but the implementation may use OpenRegister auto-generation).
-- No specification of how case deletion handles cascade (documents, tasks, decisions, roles).
-- No specification of the "reopen" mechanism after a case reaches final status.
-- Audit trail integration with Nextcloud Activity system needs implementation detail.
-
-**Open questions:**
-1. Is the audit trail stored via Nextcloud Activity (`IManager`) or via OpenRegister's audit trail plugin -- or both?
-2. Should case search use OpenRegister's built-in search or Nextcloud's full-text search?
-3. How are case identifiers guaranteed unique across multiple Nextcloud instances?
+# Case Management Specification
+
+## Purpose
+
+Case management is the core capability of Procest. A case represents a coherent body of work with a defined lifecycle, initiation, and result. Cases are governed by configurable **case types** that control behavior: allowed statuses, required fields, processing deadlines, retention rules, and more. Cases follow CMMN 1.1 concepts (CasePlanModel) and are semantically typed as `schema:Project`.
+
+**Standards**: CMMN 1.1 (CasePlanModel), Schema.org (`Project`), ZGW (`Zaak`)
+**Feature tier**: MVP (core case CRUD, list, detail, status, deadline), V1 (sub-cases, confidentiality, result types, document checklist, suspension)
+
+## Data Model
+
+### Case Entity
+
+| Property | Type | CMMN/Schema.org | ZGW Mapping | Required |
+|----------|------|----------------|-------------|----------|
+| `title` | string | `schema:name` | `omschrijving` | Yes |
+| `description` | string | `schema:description` | `toelichting` | No |
+| `identifier` | string | `schema:identifier` | `identificatie` | Auto |
+| `caseType` | reference | CMMN CaseDefinition | `zaaktype` | Yes |
+| `status` | reference | CMMN PlanItem lifecycle | `status` | Yes |
+| `result` | reference | CMMN case outcome | `resultaat` | No |
+| `startDate` | date | `schema:startDate` | `startdatum` | Yes |
+| `endDate` | date | `schema:endDate` | `einddatum` | No |
+| `plannedEndDate` | date | -- | `einddatumGepland` | No |
+| `deadline` | date | -- | `uiterlijkeEinddatumAfdoening` | Auto (from caseType) |
+| `confidentiality` | enum | -- | `vertrouwelijkheidaanduiding` | No (default from caseType) |
+| `assignee` | string | CMMN HumanTask.assignee | -- | No |
+| `priority` | enum | `schema:priority` | -- | No |
+| `parentCase` | reference | CMMN CaseTask | `hoofdzaak` | No |
+| `relatedCases` | array | -- | `relevanteAndereZaken` | No |
+| `geometry` | GeoJSON | `schema:geo` | `zaakgeometrie` | No |
+
+### Case Type Behavioral Controls on Cases
+
+- `deadline` is auto-calculated: `startDate` + `caseType.processingDeadline`
+- `confidentiality` defaults from `caseType.confidentiality`
+- `status` MUST reference a status type linked to the case's case type
+- Only role types linked to the case type are allowed for participant assignment
+- Property definitions linked to the case type MUST be satisfied before reaching required statuses
+- Document types linked to the case type define which documents are expected at each status
+
+### Confidentiality Levels
+
+| Level | ZGW Dutch | Description |
+|-------|-----------|-------------|
+| `public` | openbaar | Publicly accessible |
+| `restricted` | beperkt_openbaar | Restricted public access |
+| `internal` | intern | Internal use only |
+| `case_sensitive` | zaakvertrouwelijk | Case-confidential |
+| `confidential` | vertrouwelijk | Confidential |
+| `highly_confidential` | confidentieel | Highly confidential |
+| `secret` | geheim | Secret |
+| `top_secret` | zeer_geheim | Top secret |
+
+## Requirements
+
+---
+
+### REQ-CM-01: Case Creation
+
+**Feature tier**: MVP
+
+The system MUST support creating new cases. Each case MUST be linked to a published, valid case type. The case type controls initial defaults and behavioral constraints.
+
+#### Scenario CM-01a: Create a case with case type selection
+
+- GIVEN a user with case management access
+- AND a published case type "Omgevingsvergunning" with `processingDeadline = "P56D"`, `confidentiality = "internal"`, and status types ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
+- WHEN the user opens the "New Case" form and selects case type "Omgevingsvergunning"
+- AND enters title "Bouwvergunning Keizersgracht 100"
+- AND submits the form
+- THEN the system MUST create an OpenRegister object in the `procest` register with the `case` schema
+- AND the `identifier` MUST be auto-generated (format: `YYYY-NNN`, e.g., "2026-042")
+- AND the `startDate` MUST default to the current date
+- AND the `deadline` MUST be auto-calculated as `startDate + P56D` (e.g., 2026-01-15 + 56 days = 2026-03-12)
+- AND the `confidentiality` MUST default to "internal" (inherited from case type)
+- AND the `status` MUST be set to "Ontvangen" (the first status type by `order`)
+- AND a unique `identifier` MUST be auto-generated
+
+#### Scenario CM-01b: Case type is required at creation
+
+- GIVEN a user opening the "New Case" form
+- WHEN the user attempts to submit without selecting a case type
+- THEN the system MUST reject the submission
+- AND the system MUST display a validation error: "Case type is required"
+
+#### Scenario CM-01c: Title is required at creation
+
+- GIVEN a user opening the "New Case" form with case type "Klacht behandeling" selected
+- WHEN the user attempts to submit without entering a title
+- THEN the system MUST reject the submission
+- AND the system MUST display a validation error: "Title is required"
+
+#### Scenario CM-01d: Cannot create case with draft case type
+
+- GIVEN a case type "Bezwaarschrift" with `isDraft = true`
+- WHEN a user attempts to create a case of type "Bezwaarschrift"
+- THEN the system MUST reject the creation
+- AND the system MUST display an error: "Cannot create a case with a draft case type. The case type must be published first."
+
+#### Scenario CM-01e: Cannot create case with expired case type
+
+- GIVEN a case type "Bouwvergunning Oud" with `validUntil = "2025-12-31"`
+- AND today is "2026-02-25"
+- WHEN a user attempts to create a case of this type
+- THEN the system MUST reject the creation
+- AND the system MUST display an error: "Cannot create a case with an expired case type. The case type was valid until 2025-12-31."
+
+#### Scenario CM-01f: Cannot create case with case type not yet valid
+
+- GIVEN a case type "Nieuwe Subsidie" with `validFrom = "2027-01-01"`
+- AND today is "2026-02-25"
+- WHEN a user attempts to create a case of this type
+- THEN the system MUST reject the creation
+- AND the system MUST display an error: "Cannot create a case with a case type that is not yet valid. The case type is valid from 2027-01-01."
+
+#### Scenario CM-01g: Default case type pre-selected
+
+- GIVEN a case type "Omgevingsvergunning" is marked as the default case type in admin settings
+- WHEN a user opens the "New Case" form
+- THEN the case type dropdown MUST pre-select "Omgevingsvergunning"
+- AND the user MAY change the selection to another published, valid case type
+
+---
+
+### REQ-CM-02: Case Update
+
+**Feature tier**: MVP
+
+The system MUST support updating case properties. Changes MUST be recorded in the audit trail.
+
+#### Scenario CM-02a: Update case description
+
+- GIVEN an existing case "Bouwvergunning Keizersgracht 100" with identifier "2026-042"
+- WHEN the user updates the description to "Verbouwing woonhuis, 3 bouwlagen, 180 m2"
+- THEN the system MUST update the OpenRegister object
+- AND the audit trail MUST record: user, timestamp, field changed, old value, new value
+
+#### Scenario CM-02b: Update case priority
+
+- GIVEN an existing case with priority "normal"
+- WHEN the handler changes the priority to "high"
+- THEN the system MUST update the `priority` field
+- AND the audit trail MUST record the change
+
+#### Scenario CM-02c: Reassign case handler
+
+- GIVEN a case assigned to "Jan de Vries"
+- WHEN an authorized user reassigns the case to "Maria van den Berg"
+- THEN the `assignee` field MUST be updated to "Maria van den Berg"
+- AND the audit trail MUST record: "Handler changed from Jan de Vries to Maria van den Berg"
+
+---
+
+### REQ-CM-03: Case Deletion
+
+**Feature tier**: MVP
+
+The system MUST support deleting cases. Deletion SHOULD be restricted to cases without a final status.
+
+#### Scenario CM-03a: Delete a case in initial status
+
+- GIVEN a case "Testmelding" with status "Ontvangen" and no linked tasks, decisions, or sub-cases
+- WHEN an authorized user deletes the case
+- THEN the system MUST remove the OpenRegister object
+- AND the system MUST display a confirmation dialog before deletion
+
+#### Scenario CM-03b: Warn before deleting case with linked objects
+
+- GIVEN a case with 3 linked tasks and 1 linked decision
+- WHEN an authorized user attempts to delete the case
+- THEN the system MUST display a warning: "This case has 3 tasks and 1 decision. Deleting the case will also remove these linked objects."
+- AND the user MUST confirm before proceeding
+
+---
+
+### REQ-CM-04: Case List View
+
+**Feature tier**: MVP
+
+The system MUST provide a list view of all cases with search, sort, filter, and pagination capabilities. See wireframe 3.2 (Case List View) in DESIGN-REFERENCES.md.
+
+#### Scenario CM-04a: Default case list
+
+- GIVEN 24 open cases in the system
+- WHEN the user navigates to the Cases page
+- THEN the system MUST display a table with columns: ID, Title, Type, Status, Deadline, Handler
+- AND the list MUST be paginated at 20 items per page by default
+- AND overdue cases MUST be visually highlighted (red indicator)
+
+#### Scenario CM-04b: Filter by case type
+
+- GIVEN cases of types "Omgevingsvergunning" (10), "Subsidieaanvraag" (7), "Klacht" (4), "Melding" (3)
+- WHEN the user selects filter "Type: Omgevingsvergunning"
+- THEN only the 10 cases of type "Omgevingsvergunning" MUST be shown
+
+#### Scenario CM-04c: Filter by status
+
+- GIVEN cases in statuses "Ontvangen" (8), "In behandeling" (6), "Besluitvorming" (5), "Afgehandeld" (5)
+- WHEN the user selects filter "Status: In behandeling"
+- THEN only the 6 cases with status "In behandeling" MUST be shown
+
+#### Scenario CM-04d: Filter by handler
+
+- GIVEN cases assigned to "Jan de Vries" (8), "Maria van den Berg" (6), unassigned (10)
+- WHEN the user selects filter "Handler: Jan de Vries"
+- THEN only Jan's 8 cases MUST be shown
+
+#### Scenario CM-04e: Filter by priority
+
+- GIVEN cases with priorities "high" (4), "normal" (16), "low" (4)
+- WHEN the user selects filter "Priority: high"
+- THEN only the 4 high-priority cases MUST be shown
+
+#### Scenario CM-04f: Filter overdue cases
+
+- GIVEN 3 cases past their deadline
+- WHEN the user selects filter "Overdue: Yes"
+- THEN only the 3 overdue cases MUST be shown
+
+#### Scenario CM-04g: Search cases by keyword
+
+- GIVEN cases with titles "Bouwvergunning Keizersgracht 100", "Bouwvergunning Prinsengracht 50", "Subsidie innovatie"
+- WHEN the user searches for "Keizersgracht"
+- THEN only "Bouwvergunning Keizersgracht 100" MUST be shown
+- AND search MUST match against `title` and `description` fields
+
+#### Scenario CM-04h: Sort by deadline ascending
+
+- GIVEN multiple cases with different deadlines
+- WHEN the user sorts by "Deadline" ascending
+- THEN cases MUST be ordered with the nearest deadline first
+
+#### Scenario CM-04i: Paginate case list
+
+- GIVEN 24 cases matching the current filters
+- AND page size is 20
+- WHEN the user views the case list
+- THEN page 1 MUST show cases 1-20
+- AND the system MUST display "Showing 20 of 24 cases -- Page 1 of 2"
+- AND a "Next" button MUST navigate to page 2 (cases 21-24)
+
+---
+
+### REQ-CM-05: Quick Status Change from List
+
+**Feature tier**: MVP
+
+The system MUST support changing a case's status directly from the case list view without opening the detail page. See wireframe 3.2 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-05a: Quick status change via dropdown
+
+- GIVEN a case "Bouwvergunning Keizersgracht 100" with status "Ontvangen" in the case list
+- AND the case type defines statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
+- WHEN the user clicks the status cell/dropdown for this case
+- THEN a dropdown MUST appear showing only the statuses defined by the case type
+- AND the current status MUST be visually indicated (e.g., checked or highlighted)
+
+#### Scenario CM-05b: Quick status change succeeds
+
+- GIVEN the status dropdown is open for case "2026-042"
+- WHEN the user selects "In behandeling"
+- THEN the case status MUST be updated to "In behandeling"
+- AND the list row MUST update without a full page reload
+- AND the audit trail MUST record the status change
+
+#### Scenario CM-05c: Quick status change blocked by missing properties
+
+- GIVEN a case type "Omgevingsvergunning" with property "Kadastraal nummer" required at status "In behandeling"
+- AND the case has not filled "Kadastraal nummer"
+- WHEN the user attempts a quick status change to "In behandeling"
+- THEN the system MUST reject the change
+- AND display a message: "Cannot advance to 'In behandeling': required property 'Kadastraal nummer' is missing. Open the case to complete the required fields."
+
+---
+
+### REQ-CM-06: Case Detail View
+
+**Feature tier**: MVP
+
+The system MUST provide a comprehensive detail view for each case. See wireframe 3.3 (Case Detail View) in DESIGN-REFERENCES.md. The detail view MUST include: status timeline, case info panel, deadline and timing panel, participants panel, custom properties panel, required documents checklist, tasks section, decisions section, activity timeline, and sub-cases section.
+
+#### Scenario CM-06a: Case info panel
+
+- GIVEN a case "Bouwvergunning Keizersgracht 100" of type "Omgevingsvergunning"
+- WHEN the user navigates to the case detail view
+- THEN the case info panel MUST display: title, type, priority, confidentiality level, identifier, and creation date
+- AND a "Change Status" dropdown MUST be available
+
+#### Scenario CM-06b: Deadline and timing panel
+
+- GIVEN a case with `startDate = "2026-01-15"`, `deadline = "2026-03-12"`, `processingDeadline = "P56D"` (from case type)
+- AND today is "2026-02-25" (15 days remaining)
+- WHEN the user views the case detail
+- THEN the deadline panel MUST display: "Started: Jan 15, 2026", "Deadline: Mar 12, 2026"
+- AND the system MUST display "15 days remaining"
+- AND the processing deadline MUST show "56 days"
+- AND the days elapsed MUST show "41"
+
+#### Scenario CM-06c: Deadline countdown -- overdue
+
+- GIVEN a case with `deadline = "2026-02-20"`
+- AND today is "2026-02-25"
+- THEN the system MUST display "5 DAYS OVERDUE" with a red visual indicator
+- AND the deadline text MUST be styled in red/error state
+
+#### Scenario CM-06d: Deadline countdown -- on track
+
+- GIVEN a case with `deadline = "2026-03-15"`
+- AND today is "2026-02-25"
+- THEN the system MUST display "18 days remaining" with a neutral/green indicator
+
+#### Scenario CM-06e: Extension button visibility
+
+- GIVEN a case type with `extensionAllowed = true` and `extensionPeriod = "P28D"`
+- WHEN the user views the deadline panel
+- THEN a "Request Extension" button MUST be visible
+- AND the panel MUST show "Extension: allowed (+28 days)"
+
+#### Scenario CM-06f: Extension button hidden when not allowed
+
+- GIVEN a case type with `extensionAllowed = false`
+- WHEN the user views the deadline panel
+- THEN no "Request Extension" button MUST be displayed
+- AND the panel MUST show "Extension: not allowed"
+
+---
+
+### REQ-CM-07: Status Timeline Visualization
+
+**Feature tier**: MVP
+
+The case detail view MUST display a visual status timeline showing all statuses defined by the case type. Passed statuses are filled, the current status is highlighted, and future statuses are greyed out. See wireframe 3.3 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-07a: Status timeline with current status
+
+- GIVEN a case of type "Omgevingsvergunning" with ordered statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
+- AND the case is currently at "In behandeling"
+- WHEN the user views the case detail
+- THEN the status timeline MUST display 4 dots/nodes in order
+- AND "Ontvangen" MUST appear as passed (filled dot with date)
+- AND "In behandeling" MUST appear as current (highlighted/active dot)
+- AND "Besluitvorming" and "Afgehandeld" MUST appear as future (greyed dots)
+
+#### Scenario CM-07b: Status timeline with dates
+
+- GIVEN a case that transitioned from "Ontvangen" (Jan 15) to "In behandeling" (Feb 1)
+- WHEN the user views the status timeline
+- THEN the date "Jan 15" MUST appear beneath the "Ontvangen" node
+- AND the date "Feb 1" MUST appear beneath the "In behandeling" node
+- AND future statuses MUST NOT show dates
+
+#### Scenario CM-07c: Status timeline at final status
+
+- GIVEN a case at status "Afgehandeld" (which has `isFinal = true`)
+- WHEN the user views the status timeline
+- THEN all dots MUST appear as passed/completed (filled)
+- AND the timeline MUST visually indicate the case is complete
+
+---
+
+### REQ-CM-08: Participants Panel
+
+**Feature tier**: MVP (handler assignment), V1 (full role types)
+
+The case detail view MUST display assigned participants with their roles. See wireframe 3.3 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-08a: Display participants
+
+- GIVEN a case with roles: Handler = "Jan de Vries", Initiator = "Petra Jansen (Acme Corp)", Advisor = "Dr. K. Bakker"
+- WHEN the user views the participants panel
+- THEN each participant MUST be shown with their role label and name
+- AND the handler MUST have a "Reassign" action
+- AND an "Add Participant" button MUST be displayed
+
+#### Scenario CM-08b: Add participant with role type restriction (V1)
+
+- GIVEN a case of type "Omgevingsvergunning" with allowed role types ["Aanvrager", "Behandelaar", "Technisch adviseur", "Beslisser"]
+- WHEN the user clicks "Add Participant"
+- THEN the role selection MUST only show roles defined by the case type
+- AND the user MUST NOT be able to assign a role type not in the case type's list
+
+---
+
+### REQ-CM-09: Custom Properties Panel
+
+**Feature tier**: V1
+
+The case detail view MUST display custom properties defined by the case type. See wireframe 3.3 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-09a: Display custom properties
+
+- GIVEN a case of type "Omgevingsvergunning" with property definitions ["Kadastraal nummer" (text), "Bouwkosten" (number), "Oppervlakte" (number), "Bouwlagen" (number)]
+- AND the case has values: Kadastraal nummer = "AMS04-A-1234", Bouwkosten = 250000, Oppervlakte = 180, Bouwlagen = 3
+- WHEN the user views the custom properties panel
+- THEN all 4 properties MUST be displayed with their values
+- AND an "Edit Properties" button MUST be available
+
+#### Scenario CM-09b: Empty custom properties
+
+- GIVEN a case of type "Omgevingsvergunning" with 4 property definitions
+- AND no property values have been filled
+- WHEN the user views the custom properties panel
+- THEN all 4 properties MUST be displayed with empty/placeholder values
+- AND the panel SHOULD indicate "0 of 4 properties filled"
+
+---
+
+### REQ-CM-10: Required Documents Checklist
+
+**Feature tier**: V1
+
+The case detail view MUST display a checklist of required documents defined by the case type, showing which are present and which are missing. See wireframe 3.3 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-10a: Document checklist with mixed completion
+
+- GIVEN a case of type "Omgevingsvergunning" with required document types:
+ - "Bouwtekening" (incoming, required at "In behandeling")
+ - "Constructieberekening" (incoming, required at "In behandeling")
+ - "Situatietekening" (incoming, required at "In behandeling")
+ - "Welstandsadvies" (internal, required at "Besluitvorming")
+ - "Vergunningsbesluit" (outgoing, required at "Afgehandeld")
+- AND files uploaded: Bouwtekening (Jan 16), Constructieberekening (Jan 20), Situatietekening (Jan 22)
+- WHEN the user views the documents panel
+- THEN the header MUST show "3/5 complete"
+- AND Bouwtekening, Constructieberekening, Situatietekening MUST show a checkmark with upload date
+- AND Welstandsadvies MUST show a missing indicator with "required at: Besluitvorming"
+- AND Vergunningsbesluit MUST show a missing indicator with "required at: Afgehandeld"
+
+#### Scenario CM-10b: All documents present
+
+- GIVEN a case where all 5 required documents have been uploaded
+- WHEN the user views the documents panel
+- THEN the header MUST show "5/5 complete"
+- AND all items MUST show a checkmark
+
+#### Scenario CM-10c: No required documents defined
+
+- GIVEN a case type "Melding" with no document types defined
+- WHEN the user views the case detail
+- THEN the documents panel SHOULD either be hidden or show "No required documents for this case type"
+
+---
+
+### REQ-CM-11: Tasks Section
+
+**Feature tier**: MVP
+
+The case detail view MUST display tasks linked to the case. See wireframe 3.3 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-11a: Display tasks with completion count
+
+- GIVEN a case with 5 tasks: 2 completed, 1 active, 2 available
+- WHEN the user views the tasks section
+- THEN the header MUST show "TASKS 3/5" (or similar completion indicator)
+- AND each task MUST show: title, status icon, due date (if set), assignee (if set)
+- AND completed tasks MUST show a checkmark
+- AND the active task MUST be visually distinct (e.g., spinner icon)
+- AND an "Add Task" button MUST be available
+
+#### Scenario CM-11b: No tasks
+
+- GIVEN a case with no linked tasks
+- WHEN the user views the tasks section
+- THEN the section MUST show "No tasks" or an empty state
+- AND the "Add Task" button MUST still be available
+
+---
+
+### REQ-CM-12: Decisions Section
+
+**Feature tier**: V1
+
+The case detail view MUST display decisions linked to the case.
+
+#### Scenario CM-12a: Display decisions
+
+- GIVEN a case with 1 decision: "Vergunning verleend" decided on Feb 20 by "Jan de Vries"
+- WHEN the user views the decisions section
+- THEN the decision MUST show: title, decided date, decided by
+- AND an "Add Decision" button MUST be available
+
+#### Scenario CM-12b: No decisions
+
+- GIVEN a case with no decisions
+- WHEN the user views the decisions section
+- THEN the section MUST show "(no decisions yet)"
+- AND an "Add Decision" button MUST be available
+
+---
+
+### REQ-CM-13: Activity Timeline
+
+**Feature tier**: MVP
+
+The case detail view MUST display an activity timeline showing all events related to the case in chronological order (newest first). See wireframe 3.3 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-13a: Activity timeline entries
+
+- GIVEN a case "2026-042" with the following events:
+ - Feb 25: Task "Review docs" assigned to Jan de Vries
+ - Feb 20: Deadline passed (case is now overdue)
+ - Feb 1: Status changed to "In behandeling" by Jan de Vries
+ - Jan 22: Document "Situatietekening" uploaded by Petra Jansen
+ - Jan 15: Case created
+- WHEN the user views the activity timeline
+- THEN all events MUST be displayed in reverse chronological order
+- AND each entry MUST show: date, event description, actor (if applicable)
+- AND deadline-passed events MUST be visually distinct (warning style)
+
+#### Scenario CM-13b: Add note to activity
+
+- GIVEN a case detail view with an activity timeline
+- WHEN the user clicks "Add note" and enters "Wachten op welstandsadvies van externe partij"
+- THEN the note MUST appear in the timeline with the current date and the user's name
+- AND the note MUST be stored via Nextcloud's ICommentsManager
+
+---
+
+### REQ-CM-14: Status Change
+
+**Feature tier**: MVP
+
+The system MUST support changing a case's status. Status changes MUST respect case type constraints: only statuses defined by the case type are allowed, required properties MUST be satisfied, and required documents MUST be present.
+
+#### Scenario CM-14a: Valid status change
+
+- GIVEN a case of type "Omgevingsvergunning" currently at "Ontvangen"
+- AND the case type defines statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
+- WHEN the handler changes the status to "In behandeling"
+- THEN the status MUST be updated
+- AND the audit trail MUST record: who (handler name), when (timestamp), from "Ontvangen" to "In behandeling"
+
+#### Scenario CM-14b: Reject status not in case type
+
+- GIVEN a case of type "Omgevingsvergunning" with statuses ["Ontvangen", "In behandeling", "Besluitvorming", "Afgehandeld"]
+- WHEN an API request attempts to set status to "Bezwaar" (not in this case type's list)
+- THEN the system MUST reject the change
+- AND return an error: "Status 'Bezwaar' is not defined for case type 'Omgevingsvergunning'"
+
+#### Scenario CM-14c: Status change blocked by required properties (V1)
+
+- GIVEN a case of type "Omgevingsvergunning"
+- AND property "Kadastraal nummer" has `requiredAtStatus` pointing to "In behandeling"
+- AND the case has not filled "Kadastraal nummer"
+- WHEN the user attempts to change status to "In behandeling"
+- THEN the system MUST reject the change
+- AND display: "Cannot advance to 'In behandeling': required properties missing: Kadastraal nummer"
+
+#### Scenario CM-14d: Status change blocked by required documents (V1)
+
+- GIVEN a case of type "Omgevingsvergunning"
+- AND document type "Welstandsadvies" has `requiredAtStatus` pointing to "Besluitvorming"
+- AND no file of type "Welstandsadvies" has been uploaded
+- WHEN the user attempts to change status to "Besluitvorming"
+- THEN the system MUST reject the change
+- AND display: "Cannot advance to 'Besluitvorming': required documents missing: Welstandsadvies"
+
+#### Scenario CM-14e: Status change triggers initiator notification
+
+- GIVEN a case with an initiator "Petra Jansen"
+- AND the target status type "In behandeling" has `notifyInitiator = true` and `notificationText = "Uw zaak is in behandeling genomen"`
+- WHEN the handler changes the case to "In behandeling"
+- THEN the system MUST send a notification to the initiator
+- AND the notification MUST contain the text "Uw zaak is in behandeling genomen"
+
+#### Scenario CM-14f: Status change to final status sets endDate
+
+- GIVEN a case currently at "Besluitvorming"
+- AND "Afgehandeld" is the final status (`isFinal = true`)
+- WHEN the handler changes the status to "Afgehandeld"
+- THEN the case `endDate` MUST be set to the current date
+- AND the case MUST be marked as closed
+- AND no further status changes SHOULD be allowed without explicit reopening
+
+---
+
+### REQ-CM-15: Case Result Recording
+
+**Feature tier**: MVP (basic result), V1 (result types from case type)
+
+The system MUST support recording a result when closing a case.
+
+#### Scenario CM-15a: Record result from case type's allowed results (V1)
+
+- GIVEN a case of type "Omgevingsvergunning" with result types ["Vergunning verleend", "Vergunning geweigerd", "Ingetrokken"]
+- WHEN the handler closes the case and selects result "Vergunning verleend"
+- THEN a Result object MUST be created and linked to the case
+- AND the result MUST reference the "Vergunning verleend" result type
+- AND the result type's archival rules MUST be recorded: `archiveAction = "retain"`, `retentionPeriod = "P20Y"`
+
+#### Scenario CM-15b: Result required at final status
+
+- GIVEN a case type "Omgevingsvergunning" where the final status "Afgehandeld" requires a result
+- WHEN the handler attempts to set status to "Afgehandeld" without selecting a result
+- THEN the system MUST prompt for a result selection
+- AND the result dropdown MUST only show result types defined by the case type
+
+#### Scenario CM-15c: Result triggers archival rules (V1)
+
+- GIVEN a result type "Vergunning geweigerd" with `archiveAction = "destroy"` and `retentionPeriod = "P10Y"` and `retentionDateSource = "case_completed"`
+- WHEN a case is closed with this result
+- THEN the system MUST record: archive action = destroy, retention until = endDate + 10 years
+- AND the audit trail MUST record the archival determination
+
+---
+
+### REQ-CM-16: Case Deadline Extension
+
+**Feature tier**: MVP
+
+The system MUST support extending a case's deadline when the case type allows it.
+
+#### Scenario CM-16a: Extend deadline when allowed
+
+- GIVEN a case of type "Omgevingsvergunning" with `extensionAllowed = true` and `extensionPeriod = "P28D"`
+- AND the case has `deadline = "2026-03-12"`
+- WHEN the handler requests an extension
+- THEN the deadline MUST be extended to "2026-04-09" (original + 28 days)
+- AND the audit trail MUST record: "Deadline extended from 2026-03-12 to 2026-04-09 by [handler name]"
+- AND the extension reason SHOULD be captured
+
+#### Scenario CM-16b: Reject extension when not allowed
+
+- GIVEN a case of type "Klacht behandeling" with `extensionAllowed = false`
+- WHEN the handler attempts to extend the deadline
+- THEN the system MUST reject the request
+- AND display: "Deadline extension is not allowed for case type 'Klacht behandeling'"
+
+#### Scenario CM-16c: Extension limit (single extension)
+
+- GIVEN a case that has already been extended once
+- WHEN the handler attempts a second extension
+- THEN the system SHOULD reject the request (default: one extension allowed)
+- AND display: "This case has already been extended"
+
+---
+
+### REQ-CM-17: Case Suspension
+
+**Feature tier**: V1
+
+The system SHOULD support suspending a case when the case type allows it. Suspension pauses the deadline countdown.
+
+#### Scenario CM-17a: Suspend a case
+
+- GIVEN a case of type "Omgevingsvergunning" with `suspensionAllowed = true`
+- AND the case has `deadline = "2026-03-12"` and 15 days remaining
+- WHEN the handler suspends the case with reason "Wachten op aanvullende gegevens van aanvrager"
+- THEN the case MUST enter a suspended state
+- AND the deadline countdown MUST pause (remaining days frozen at 15)
+- AND the audit trail MUST record: suspension start, reason, who suspended
+
+#### Scenario CM-17b: Resume a suspended case
+
+- GIVEN a case suspended for 10 days with 15 days remaining at suspension
+- WHEN the handler resumes the case
+- THEN the deadline MUST be recalculated: new deadline = today + 15 remaining days
+- AND the audit trail MUST record: suspension end, total suspended duration (10 days), who resumed
+
+#### Scenario CM-17c: Reject suspension when not allowed
+
+- GIVEN a case of type "Melding" with `suspensionAllowed = false`
+- WHEN the handler attempts to suspend the case
+- THEN the system MUST reject the request
+- AND display: "Suspension is not allowed for case type 'Melding'"
+
+---
+
+### REQ-CM-18: Sub-Cases
+
+**Feature tier**: V1
+
+The system SHOULD support parent/child case hierarchies. A sub-case is a full case linked to a parent case.
+
+#### Scenario CM-18a: Create a sub-case
+
+- GIVEN an existing case "Bouwproject Centrum" (identifier "2026-042")
+- WHEN the user clicks "Create Sub-case" and selects case type "Omgevingsvergunning" with title "Vergunning fundering"
+- THEN a new case MUST be created with `parentCase` referencing "2026-042"
+- AND the sub-case MUST have its own lifecycle, deadline, and status independent of the parent
+
+#### Scenario CM-18b: Sub-cases displayed on parent
+
+- GIVEN a parent case "2026-042" with 2 sub-cases: "Vergunning fundering" (active) and "Vergunning gevel" (completed)
+- WHEN the user views the parent case detail
+- THEN the sub-cases section MUST list both sub-cases with their status and deadline
+- AND each sub-case MUST be clickable to navigate to its detail view
+
+#### Scenario CM-18c: Navigate from sub-case to parent
+
+- GIVEN a sub-case "Vergunning fundering" with parent "Bouwproject Centrum"
+- WHEN the user views the sub-case detail
+- THEN a breadcrumb or link MUST be displayed: "Parent case: Bouwproject Centrum (2026-042)"
+- AND clicking it MUST navigate to the parent case detail
+
+#### Scenario CM-18d: Sub-case type restrictions (V1)
+
+- GIVEN a parent case type "Bouwproject" with `subCaseTypes` referencing ["Omgevingsvergunning", "Sloopvergunning"]
+- WHEN the user creates a sub-case
+- THEN the case type selection MUST only show "Omgevingsvergunning" and "Sloopvergunning"
+- AND the user MUST NOT be able to select a case type not in the parent's `subCaseTypes` list
+
+---
+
+### REQ-CM-19: Confidentiality Levels
+
+**Feature tier**: V1
+
+The system SHOULD support confidentiality levels on cases, defaulting from the case type.
+
+#### Scenario CM-19a: Inherit confidentiality from case type
+
+- GIVEN a case type "Omgevingsvergunning" with `confidentiality = "internal"`
+- WHEN a new case is created
+- THEN the case `confidentiality` MUST default to "internal"
+
+#### Scenario CM-19b: Override confidentiality on case
+
+- GIVEN a case with default confidentiality "internal"
+- WHEN the handler changes the confidentiality to "confidential"
+- THEN the case `confidentiality` MUST be updated to "confidential"
+- AND the audit trail MUST record the change
+
+#### Scenario CM-19c: Confidentiality level options
+
+- GIVEN the confidentiality enum with 8 levels (public through top_secret)
+- WHEN the user views the confidentiality dropdown on a case
+- THEN all 8 levels MUST be available for selection
+- AND the levels MUST be ordered from least to most restrictive
+
+---
+
+### REQ-CM-20: Case Validation Rules
+
+**Feature tier**: MVP
+
+The system MUST enforce validation rules when creating or modifying cases.
+
+#### Scenario CM-20a: Title is required
+
+- GIVEN a case creation or update form
+- WHEN the user submits with an empty title
+- THEN the system MUST reject the submission with error: "Title is required"
+
+#### Scenario CM-20b: Case type is required
+
+- GIVEN a case creation form
+- WHEN the user submits without selecting a case type
+- THEN the system MUST reject the submission with error: "Case type is required"
+
+#### Scenario CM-20c: Case type must be published
+
+- GIVEN a case type "Bezwaarschrift" with `isDraft = true`
+- WHEN a user submits a case creation with this type
+- THEN the system MUST reject with error: "Case type 'Bezwaarschrift' is a draft and cannot be used to create cases"
+
+#### Scenario CM-20d: Case type must be within validity window
+
+- GIVEN a case type with `validFrom = "2026-06-01"` and today is "2026-02-25"
+- WHEN a user submits a case creation with this type
+- THEN the system MUST reject with error: "Case type is not yet valid (valid from 2026-06-01)"
+
+#### Scenario CM-20e: Start date must not be in the future
+
+- GIVEN a case creation form
+- WHEN the user sets startDate to a date in the future
+- THEN the system SHOULD warn but MAY allow (some jurisdictions allow future-dated cases)
+
+---
+
+### REQ-CM-21: Case Deadline Countdown Display
+
+**Feature tier**: MVP
+
+The system MUST display deadline countdowns on cases across all views (list, detail, My Work). See wireframes 3.2 and 3.3 in DESIGN-REFERENCES.md.
+
+#### Scenario CM-21a: Days remaining display
+
+- GIVEN a case with `deadline = "2026-03-15"` and today is "2026-02-25"
+- WHEN displayed in the case list or detail view
+- THEN the system MUST show "18 days" (or "18 days remaining")
+- AND the indicator MUST use a neutral/positive style (e.g., no color or green)
+
+#### Scenario CM-21b: Due tomorrow
+
+- GIVEN a case with deadline = tomorrow
+- WHEN displayed in any view
+- THEN the system MUST show "1 day" (or "Due tomorrow")
+- AND the indicator MUST use a warning style (e.g., yellow/amber)
+
+#### Scenario CM-21c: Overdue display
+
+- GIVEN a case with `deadline = "2026-02-20"` and today is "2026-02-25"
+- WHEN displayed in any view
+- THEN the system MUST show "5 days overdue" (or "5d overdue")
+- AND the indicator MUST use an error/danger style (e.g., red)
+
+#### Scenario CM-21d: Due today
+
+- GIVEN a case with deadline = today
+- WHEN displayed in any view
+- THEN the system MUST show "Due today"
+- AND the indicator MUST use a warning style
+
+---
+
+### REQ-CM-22: Audit Trail
+
+**Feature tier**: MVP
+
+The system MUST maintain a complete audit trail for all case modifications. The audit trail is published via Nextcloud's Activity system (`OCP\Activity\IManager`).
+
+#### Scenario CM-22a: Status change audit entry
+
+- GIVEN a case "2026-042"
+- WHEN the handler changes status from "Ontvangen" to "In behandeling"
+- THEN the audit trail MUST record: event type "case_status_change", user "Jan de Vries", timestamp, from status "Ontvangen", to status "In behandeling"
+
+#### Scenario CM-22b: Property change audit entry
+
+- GIVEN a case "2026-042"
+- WHEN the user changes description from "Verbouwing" to "Verbouwing woonhuis, 3 bouwlagen"
+- THEN the audit trail MUST record: event type "case_update", user, timestamp, field "description", old value, new value
+
+#### Scenario CM-22c: Deadline extension audit entry
+
+- GIVEN a case "2026-042"
+- WHEN the handler extends the deadline
+- THEN the audit trail MUST record: event type "case_extension", user, timestamp, old deadline, new deadline, reason
+
+#### Scenario CM-22d: Case creation audit entry
+
+- GIVEN a user creating a new case
+- WHEN the case is successfully created
+- THEN the audit trail MUST record: event type "case_created", user, timestamp, case type, initial status, calculated deadline
+
+---
+
+## UI References
+
+- **Case List View**: See wireframe 3.2 in DESIGN-REFERENCES.md
+- **Case Detail View**: See wireframe 3.3 in DESIGN-REFERENCES.md (status timeline, info panel, deadline panel, participants, custom properties, document checklist, tasks, decisions, activity timeline, sub-cases)
+- **My Work View**: See wireframe 3.5 in DESIGN-REFERENCES.md (overdue / due this week / upcoming sections)
+- **Dashboard**: See wireframe 3.1 in DESIGN-REFERENCES.md (case count widgets, status distribution, overdue list)
+
+## Dependencies
+
+- **Case Types spec** (`../case-types/spec.md`): Case type MUST be published and valid to create cases. Case type controls statuses, deadlines, confidentiality defaults, document types, property definitions, result types, and role types.
+- **OpenRegister**: All case data is stored as OpenRegister objects in the `procest` register under the `case` schema.
+- **Nextcloud Activity**: Audit trail events are published via `OCP\Activity\IManager`.
+- **Nextcloud Comments**: Case notes use `OCP\Comments\ICommentsManager`.
+- **Nextcloud Files**: Document uploads reference Nextcloud file IDs via `OCP\Files\IRootFolder`.
+
+### Current Implementation Status
+
+**Substantially implemented (MVP).** Core case management functionality is in place.
+
+**Implemented:**
+- Case CRUD via OpenRegister object store (`src/store/modules/object.js` using `createObjectStore` with filesPlugin, auditTrailsPlugin, relationsPlugin).
+- Case list view (`src/views/cases/CaseList.vue`) using `CnIndexPage` with columns, sorting (default by deadline asc), pagination, row click navigation, selectable rows, and `QuickStatusDropdown` for inline status changes.
+- Case detail view (`src/views/cases/CaseDetail.vue`) using `CnDetailPage` with sidebar, save/delete actions, status change dropdown with result prompt for final status.
+- Case creation dialog (`src/views/cases/CaseCreateDialog.vue`) with case type selection.
+- Status timeline visualization (`src/views/cases/components/StatusTimeline.vue`) showing passed/current/future status dots with dates.
+- Quick status change from list (`src/views/cases/components/QuickStatusDropdown.vue`).
+- Deadline panel (`src/views/cases/components/DeadlinePanel.vue`) with countdown, overdue display, extension info and request button.
+- Participants panel (`src/views/cases/components/ParticipantsSection.vue`) with role groups, add participant dialog, handler assignment.
+- Activity timeline (`src/views/cases/components/ActivityTimeline.vue`) with add note, chronological events.
+- Result section (`src/views/cases/components/ResultSection.vue`).
+- Case validation utilities (`src/utils/caseValidation.js`).
+- Case helper utilities (`src/utils/caseHelpers.js`) with `formatDeadlineCountdown`, `isCaseOverdue`, `formatDateShort`.
+- Duration helpers (`src/utils/durationHelpers.js`) for ISO 8601 duration display.
+- ZGW Zaken API compatibility via `ZrcController` (`lib/Controller/ZrcController.php`) and `ZgwZrcRulesService` (`lib/Service/ZgwZrcRulesService.php`) handling zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten, zaakobjecten, klantcontacten.
+- ZGW business rules enforcement (`lib/Service/ZgwBusinessRulesService.php`, `lib/Service/ZgwRulesBase.php`).
+- OpenRegister schemas for case (`case_schema`), status (`status_schema`), statusRecord (`status_record_schema`), role (`role_schema`), result (`result_schema`), caseProperty (`case_property_schema`), caseDocument (`case_document_schema`), caseObject (`case_object_schema`).
+- Router with case routes: `/cases` (list), `/cases/:id` (detail).
+- Overdue case visual highlighting in case list (via `getRowClass` and `getDeadlineClass`).
+
+**Not yet implemented or partial:**
+- REQ-CM-09: Custom properties panel in case detail (schema exists but no property editor UI in case detail).
+- REQ-CM-10: Required documents checklist (document types exist but no checklist UI matching uploaded files against requirements).
+- REQ-CM-14c/d: Status change blocking by required properties or documents (V1).
+- REQ-CM-14e: Status change triggering initiator notification (schema supports it but notification delivery not confirmed).
+- REQ-CM-17: Case suspension with deadline pause/resume (V1).
+- REQ-CM-18: Sub-cases / parent-child relationships (V1).
+- REQ-CM-19: Confidentiality level enforcement (field exists in schema but no access control enforcement).
+- REQ-CM-22: Audit trail via Nextcloud Activity (`OCP\Activity\IManager`) -- not confirmed as implemented; audit trails plugin exists in object store but integration with Nextcloud Activity system unclear.
+- Case search (keyword search against title and description).
+- Filter by priority, handler, overdue status in case list.
+
+### Standards & References
+
+- **ZGW Zaken API (VNG)**: Full compatibility layer via `ZrcController` and `ZgwZrcRulesService` implementing VNG Zaken API patterns (zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten).
+- **CMMN 1.1**: Case modeled as CasePlanModel with HumanTask, Milestone, and case lifecycle concepts.
+- **Schema.org**: Case typed as `schema:Project` with `schema:name`, `schema:identifier`, `schema:startDate`, `schema:endDate`.
+- **ISO 8601**: Duration format for processing deadlines, extension periods.
+- **WCAG AA**: Accessible case list and detail views required.
+- **GEMMA**: Zaakgericht werken reference architecture compliance.
+- **Archiefwet**: Case result types with archival rules (retain/destroy, retention period).
+- **Awb**: Administrative law requirements for case handling deadlines and notifications.
+
+### Specificity Assessment
+
+This is the most detailed spec in the set -- highly implementation-ready with concrete data models, field mappings, and exhaustive scenarios.
+
+**Strengths:** Complete data model with CMMN/Schema.org/ZGW triple mapping. 22 requirements with detailed Gherkin scenarios. Clear feature tier separation. Explicit validation rules.
+
+**Missing/Ambiguous:**
+- No specification of case identifier format generation logic (the spec says `YYYY-NNN` but the implementation may use OpenRegister auto-generation).
+- No specification of how case deletion handles cascade (documents, tasks, decisions, roles).
+- No specification of the "reopen" mechanism after a case reaches final status.
+- Audit trail integration with Nextcloud Activity system needs implementation detail.
+
+**Open questions:**
+1. Is the audit trail stored via Nextcloud Activity (`IManager`) or via OpenRegister's audit trail plugin -- or both?
+2. Should case search use OpenRegister's built-in search or Nextcloud's full-text search?
+3. How are case identifiers guaranteed unique across multiple Nextcloud instances?
diff --git a/openspec/specs/case-types/spec.md b/openspec/specs/case-types/spec.md
index b9f5ec5b..e3097260 100644
--- a/openspec/specs/case-types/spec.md
+++ b/openspec/specs/case-types/spec.md
@@ -1,913 +1,913 @@
-# Case Type System Specification
-
-## Purpose
-
-Case types are configurable definitions that control the behavior of cases. A case type determines which statuses are allowed, what roles can be assigned, which custom fields are required, processing deadlines, confidentiality defaults, and archival rules. This is the international equivalent of ZGW's `ZaakType`, modeled after CMMN 1.1 `CaseDefinition` concepts.
-
-Case types form a hierarchy where the CaseType is the central configuration entity:
-
-```
-CaseType
-├── StatusType[] — Allowed lifecycle phases (ordered)
-├── ResultType[] — Allowed outcomes (with archival rules)
-├── RoleType[] — Allowed participant roles
-├── PropertyDefinition[] — Required custom data fields
-├── DocumentType[] — Required document types
-├── DecisionType[] — Allowed decision types
-└── subCaseTypes[] — Allowed sub-case types
-```
-
-**Standards**: CMMN 1.1 (CaseDefinition), ZGW Catalogi API (ZaakType), Schema.org (`PropertyValueSpecification`)
-**Feature tier**: MVP (core type CRUD, statuses, deadlines, draft/published, validity), V1 (result types, role types, property definitions, document types, decision types, confidentiality, suspension/extension)
-
-## Data Model
-
-### Case Type Entity
-
-| Property | Type | CMMN / Schema.org | ZGW Mapping | Required |
-|----------|------|-------------------|-------------|----------|
-| `title` | string | `schema:name` | `zaaktype_omschrijving` | Yes |
-| `description` | string | `schema:description` | `toelichting` | No |
-| `identifier` | string | `schema:identifier` | `identificatie` | Auto |
-| `purpose` | string | -- | `doel` | Yes |
-| `trigger` | string | -- | `aanleiding` | Yes |
-| `subject` | string | -- | `onderwerp` | Yes |
-| `initiatorAction` | string | -- | `handeling_initiator` | Yes |
-| `handlerAction` | string | -- | `handeling_behandelaar` | Yes |
-| `origin` | enum: internal, external | -- | `indicatie_intern_of_extern` | Yes |
-| `processingDeadline` | duration (ISO 8601) | CMMN TimerEventListener | `doorlooptijd_behandeling` | Yes |
-| `serviceTarget` | duration (ISO 8601) | -- | `servicenorm_behandeling` | No |
-| `suspensionAllowed` | boolean | -- | `opschorting_en_aanhouding_mogelijk` | Yes |
-| `extensionAllowed` | boolean | -- | `verlenging_mogelijk` | Yes |
-| `extensionPeriod` | duration (ISO 8601) | -- | `verlengingstermijn` | Conditional (required if extensionAllowed) |
-| `confidentiality` | enum | -- | `vertrouwelijkheidaanduiding` | Yes |
-| `publicationRequired` | boolean | -- | `publicatie_indicatie` | Yes |
-| `publicationText` | string | -- | `publicatietekst` | No |
-| `responsibleUnit` | string | -- | `verantwoordelijke` | Yes |
-| `referenceProcess` | string | -- | `referentieproces_naam` | No |
-| `isDraft` | boolean | -- | `concept` | No (default: true) |
-| `validFrom` | date | -- | `datum_begin_geldigheid` | Yes |
-| `validUntil` | date | -- | `datum_einde_geldigheid` | No |
-| `keywords` | string[] | -- | `trefwoorden` | No |
-| `subCaseTypes` | reference[] | CMMN CaseTask | `deelzaaktypen` | No |
-
-### Status Type Entity
-
-| Property | Type | Source | ZGW Mapping | Required |
-|----------|------|--------|-------------|----------|
-| `name` | string | `schema:name` | `statustype_omschrijving` | Yes |
-| `description` | string | `schema:description` | `toelichting` | No |
-| `caseType` | reference | Parent case type | `zaaktype` | Yes |
-| `order` | integer (1-9999) | CMMN Milestone sequence | `statustypevolgnummer` | Yes |
-| `isFinal` | boolean | CMMN terminal state | (last in order) | No (default: false) |
-| `targetDuration` | duration | -- | `doorlooptijd` | No |
-| `notifyInitiator` | boolean | -- | `informeren` | No (default: false) |
-| `notificationText` | string | -- | `statustekst` | No |
-
-### Result Type Entity (V1)
-
-| Property | Type | Source | ZGW Mapping | Required |
-|----------|------|--------|-------------|----------|
-| `name` | string | `schema:name` | `omschrijving` | Yes |
-| `description` | string | `schema:description` | `toelichting` | No |
-| `caseType` | reference | Parent case type | `zaaktype` | Yes |
-| `archiveAction` | enum: retain, destroy | -- | `archiefnominatie` | No |
-| `retentionPeriod` | duration (ISO 8601) | -- | `archiefactietermijn` | No |
-| `retentionDateSource` | enum | -- | `afleidingswijze` | No |
-
-### Role Type Entity (V1)
-
-| Property | Type | Source | ZGW Mapping | Required |
-|----------|------|--------|-------------|----------|
-| `name` | string | `schema:roleName` | `omschrijving` | Yes |
-| `caseType` | reference | Parent case type | `zaaktype` | Yes |
-| `genericRole` | enum | -- | `omschrijvingGeneriek` | Yes |
-
-### Property Definition Entity (V1)
-
-| Property | Type | Source | ZGW Mapping | Required |
-|----------|------|--------|-------------|----------|
-| `name` | string | `schema:name` | `eigenschapnaam` | Yes |
-| `definition` | string | `schema:description` | `definitie` | Yes |
-| `caseType` | reference | Parent case type | `zaaktype` | Yes |
-| `format` | enum: text, number, date, datetime | -- | `formaat` | Yes |
-| `maxLength` | integer | -- | `lengte` | No |
-| `allowedValues` | string[] | -- | `waardenverzameling` | No |
-| `requiredAtStatus` | reference | Status at which this must be filled | `statustype` | No |
-
-### Document Type Entity (V1)
-
-| Property | Type | Source | ZGW Mapping | Required |
-|----------|------|--------|-------------|----------|
-| `name` | string | `schema:name` | `omschrijving` | Yes |
-| `category` | string | -- | `informatieobjectcategorie` | Yes |
-| `caseType` | reference | Parent case type | `zaaktype` | Yes |
-| `direction` | enum: incoming, internal, outgoing | -- | `richting` | Yes |
-| `order` | integer | -- | `volgnummer` | Yes |
-| `confidentiality` | enum | -- | `vertrouwelijkheidaanduiding` | No |
-| `requiredAtStatus` | reference | Status requiring this document | `statustype` | No |
-
-### Decision Type Entity (V1)
-
-| Property | Type | Source | ZGW Mapping | Required |
-|----------|------|--------|-------------|----------|
-| `name` | string | `schema:name` | `omschrijving` | Yes |
-| `description` | string | `schema:description` | `toelichting` | No |
-| `category` | string | -- | `besluitcategorie` | No |
-| `objectionPeriod` | duration (ISO 8601) | -- | `reactietermijn` | No |
-| `publicationRequired` | boolean | -- | `publicatie_indicatie` | Yes |
-| `publicationPeriod` | duration (ISO 8601) | -- | `publicatietermijn` | No |
-
-## Requirements
-
----
-
-### REQ-CT-01: Case Type CRUD
-
-**Feature tier**: MVP
-
-The system MUST support creating, reading, updating, and deleting case types. Case types are managed by admins via the Nextcloud admin settings page. See wireframe 3.6 (Admin Settings -- Case Type Management) in DESIGN-REFERENCES.md.
-
-#### Scenario CT-01a: Create a case type
-
-- GIVEN an admin on the Procest settings page
-- WHEN they click "Add Case Type" and fill in:
- - Title: "Omgevingsvergunning"
- - Purpose: "Beoordelen bouwplannen"
- - Trigger: "Aanvraag van burger/bedrijf"
- - Subject: "Bouw- en verbouwactiviteiten"
- - Processing deadline: "P56D" (56 days)
- - Origin: "external"
- - Confidentiality: "internal"
- - Responsible unit: "Afdeling Vergunningen, Gemeente Amsterdam"
- - Valid from: "2026-01-01"
-- AND submits the form
-- THEN the system MUST create an OpenRegister object in the `procest` register with the `caseType` schema
-- AND `isDraft` MUST default to `true`
-- AND a unique `identifier` MUST be auto-generated
-
-#### Scenario CT-01b: Update a case type
-
-- GIVEN an existing case type "Omgevingsvergunning"
-- WHEN the admin changes the `processingDeadline` from "P56D" to "P42D"
-- THEN the system MUST update the OpenRegister object
-- AND the change MUST NOT affect existing cases (only new cases use the updated deadline)
-
-#### Scenario CT-01c: Delete a case type with no active cases
-
-- GIVEN a case type "Testtype" that has no cases associated with it
-- WHEN the admin deletes the case type
-- THEN the system MUST remove the case type and all linked sub-types (status types, result types, role types, property definitions, document types, decision types)
-- AND a confirmation dialog MUST be shown before deletion
-
-#### Scenario CT-01d: Delete a case type with active cases -- blocked
-
-- GIVEN a case type "Omgevingsvergunning" with 10 active cases
-- WHEN the admin attempts to delete the case type
-- THEN the system MUST reject the deletion
-- AND display: "Cannot delete case type 'Omgevingsvergunning': 10 active cases are using this type. Close or reassign all cases first."
-
-#### Scenario CT-01e: Case type list display
-
-- GIVEN case types: "Omgevingsvergunning" (published, default), "Subsidieaanvraag" (published), "Klacht behandeling" (published), "Bezwaarschrift" (draft)
-- WHEN the admin views the case type list
-- THEN each case type MUST display: title, status (Published/Draft), deadline, number of statuses, number of result types, validity period
-- AND the default case type MUST be visually indicated (e.g., star icon)
-- AND draft types MUST be visually distinct (e.g., warning badge)
-
----
-
-### REQ-CT-02: Case Type Draft/Published Lifecycle
-
-**Feature tier**: MVP
-
-The system MUST support a draft/published lifecycle for case types. Draft case types MUST NOT be usable for creating cases.
-
-#### Scenario CT-02a: New case type defaults to draft
-
-- GIVEN an admin creating a new case type
-- WHEN the case type is created
-- THEN `isDraft` MUST be `true` by default
-- AND the case type MUST show a "DRAFT" badge in the admin list
-
-#### Scenario CT-02b: Publish a case type -- success
-
-- GIVEN a draft case type "Subsidieaanvraag" with:
- - All required fields filled (title, purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit, validFrom)
- - At least one status type defined: "Ontvangen" (order 1), "In behandeling" (order 2), "Afgerond" (order 3, isFinal = true)
-- WHEN the admin sets `isDraft = false`
-- THEN the case type MUST become "Published"
-- AND the case type MUST become available for creating new cases
-
-#### Scenario CT-02c: Publish a case type -- blocked, no status types
-
-- GIVEN a draft case type "Bezwaarschrift" with no status types defined
-- WHEN the admin attempts to publish (set `isDraft = false`)
-- THEN the system MUST reject the publication
-- AND display: "Cannot publish case type 'Bezwaarschrift': at least one status type must be defined"
-
-#### Scenario CT-02d: Publish a case type -- blocked, no final status
-
-- GIVEN a draft case type with 2 status types, neither marked `isFinal = true`
-- WHEN the admin attempts to publish
-- THEN the system MUST reject the publication
-- AND display: "Cannot publish case type: at least one status type must be marked as final"
-
-#### Scenario CT-02e: Publish a case type -- blocked, validFrom not set
-
-- GIVEN a draft case type with `validFrom` not set
-- WHEN the admin attempts to publish
-- THEN the system MUST reject the publication
-- AND display: "Cannot publish case type: 'Valid from' date must be set"
-
-#### Scenario CT-02f: Unpublish a case type
-
-- GIVEN a published case type "Klacht behandeling" with 3 active cases
-- WHEN the admin sets `isDraft = true` (unpublish)
-- THEN the system MUST warn: "Unpublishing this case type will prevent new cases from being created. 3 existing cases will continue to function."
-- AND if confirmed, the case type MUST revert to draft
-- AND existing cases MUST NOT be affected
-
----
-
-### REQ-CT-03: Case Type Validity Periods
-
-**Feature tier**: MVP
-
-The system MUST support validity windows on case types. Cases can only be created with case types that are within their validity window.
-
-#### Scenario CT-03a: Case type within validity window
-
-- GIVEN a case type "Omgevingsvergunning" with `validFrom = "2026-01-01"` and `validUntil = "2027-12-31"`
-- AND today is "2026-06-15"
-- WHEN a user views the case type in the creation dropdown
-- THEN the case type MUST be available for selection
-
-#### Scenario CT-03b: Case type expired
-
-- GIVEN a case type "Bouwvergunning 2024" with `validUntil = "2025-12-31"`
-- AND today is "2026-02-25"
-- WHEN a user views the case creation form
-- THEN this case type MUST NOT appear in the dropdown (or MUST appear greyed out with "Expired" label)
-- AND if selected via API, the system MUST reject with: "Case type 'Bouwvergunning 2024' expired on 2025-12-31"
-
-#### Scenario CT-03c: Case type not yet valid
-
-- GIVEN a case type "Nieuwe Subsidie 2027" with `validFrom = "2027-01-01"`
-- AND today is "2026-02-25"
-- WHEN a user views the case creation form
-- THEN this case type MUST NOT appear in the dropdown (or MUST appear greyed out with "Not yet valid" label)
-
-#### Scenario CT-03d: Case type with no end date
-
-- GIVEN a case type "Klacht behandeling" with `validFrom = "2026-01-01"` and `validUntil` not set
-- AND today is "2030-12-31"
-- WHEN a user views the case creation form
-- THEN the case type MUST be available (no expiry)
-
-#### Scenario CT-03e: Validity displayed in admin list
-
-- GIVEN case types with varying validity periods
-- WHEN the admin views the case type list
-- THEN each type MUST display its validity range: "Valid: Jan 2026 -- Dec 2027" or "Valid: Jan 2026 -- (no end)"
-
----
-
-### REQ-CT-04: Status Type Management
-
-**Feature tier**: MVP
-
-The system MUST support defining ordered status types for each case type. Status types control the lifecycle phases a case can go through. See wireframe 3.7 (Admin Settings -- Case Type Detail) in DESIGN-REFERENCES.md.
-
-#### Scenario CT-04a: Add status types to a case type
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin adds the following status types:
- 1. "Ontvangen" (order: 1)
- 2. "In behandeling" (order: 2, notifyInitiator: true, notificationText: "Uw zaak is in behandeling genomen")
- 3. "Besluitvorming" (order: 3)
- 4. "Afgehandeld" (order: 4, isFinal: true, notifyInitiator: true, notificationText: "Uw zaak is afgehandeld")
-- THEN each status type MUST be created as an OpenRegister object linked to the case type
-- AND they MUST be ordered by the `order` field
-- AND the admin MUST see the ordered list with drag handles for reordering
-
-#### Scenario CT-04b: Reorder status types via drag
-
-- GIVEN a case type with status types in order: [Ontvangen(1), In behandeling(2), Besluitvorming(3), Afgehandeld(4)]
-- WHEN the admin drags "Besluitvorming" before "In behandeling"
-- THEN the `order` values MUST be recalculated: [Ontvangen(1), Besluitvorming(2), In behandeling(3), Afgehandeld(4)]
-- AND the change MUST be persisted
-
-#### Scenario CT-04c: Edit a status type
-
-- GIVEN a status type "In behandeling" (order 2) on case type "Omgevingsvergunning"
-- WHEN the admin changes `notifyInitiator` from false to true and sets `notificationText` to "Uw zaak is in behandeling genomen"
-- THEN the status type MUST be updated
-- AND the change MUST apply to future status transitions (not retroactive)
-
-#### Scenario CT-04d: Delete a status type
-
-- GIVEN a case type "Omgevingsvergunning" with 4 status types
-- AND no active cases are currently at the status "Besluitvorming"
-- WHEN the admin deletes the "Besluitvorming" status type
-- THEN the status type MUST be removed
-- AND the remaining status types MUST retain their relative order
-
-#### Scenario CT-04e: Cannot delete status type in use
-
-- GIVEN a case type "Omgevingsvergunning"
-- AND 3 active cases are currently at status "In behandeling"
-- WHEN the admin attempts to delete "In behandeling"
-- THEN the system MUST reject the deletion
-- AND display: "Cannot delete status type 'In behandeling': 3 active cases are currently at this status"
-
-#### Scenario CT-04f: At least one final status required
-
-- GIVEN a case type with 3 status types, one marked `isFinal = true`
-- WHEN the admin attempts to unmark the final status (set `isFinal = false`)
-- AND no other status is marked as final
-- THEN the system MUST reject the change
-- AND display: "At least one status type must be marked as final"
-
-#### Scenario CT-04g: Status type order is required
-
-- GIVEN an admin adding a new status type
-- WHEN they submit without setting the `order` field
-- THEN the system MUST reject the submission
-- AND display: "Order is required for status types"
-
-#### Scenario CT-04h: Status type name is required
-
-- GIVEN an admin adding a new status type
-- WHEN they submit with an empty `name`
-- THEN the system MUST reject the submission
-- AND display: "Status type name is required"
-
-#### Scenario CT-04i: Status type notification fields
-
-- GIVEN a status type with `notifyInitiator = true`
-- WHEN displayed in the admin edit view
-- THEN the notification checkbox MUST be checked
-- AND the notification text field MUST be visible and editable
-- AND the notification text SHOULD be displayed below the status name in the ordered list
-
----
-
-### REQ-CT-05: Processing Deadline Configuration
-
-**Feature tier**: MVP
-
-The system MUST support configuring a processing deadline on each case type. The deadline is an ISO 8601 duration that controls automatic deadline calculation on cases.
-
-#### Scenario CT-05a: Set processing deadline
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin sets `processingDeadline = "P56D"` (56 days)
-- THEN the system MUST store the duration in ISO 8601 format
-- AND the admin UI MUST display this as "56 days"
-
-#### Scenario CT-05b: Invalid processing deadline format
-
-- GIVEN a case type in edit mode
-- WHEN the admin enters "56 days" (not ISO 8601) as the processing deadline
-- THEN the system MUST reject the input
-- AND display: "Processing deadline must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks)"
-
-#### Scenario CT-05c: Service target (optional)
-
-- GIVEN a case type "Omgevingsvergunning" with `processingDeadline = "P56D"`
-- WHEN the admin also sets `serviceTarget = "P42D"` (42 days)
-- THEN the service target MUST be stored separately
-- AND cases SHOULD display both the service target and the hard deadline
-
-#### Scenario CT-05d: Deadline calculation on case creation
-
-- GIVEN a case type with `processingDeadline = "P56D"`
-- WHEN a case is created with `startDate = "2026-03-01"`
-- THEN the case `deadline` MUST be calculated as "2026-04-26" (March 1 + 56 days)
-
----
-
-### REQ-CT-06: Extension and Suspension Configuration
-
-**Feature tier**: MVP (extension), V1 (suspension)
-
-The system MUST support configuring extension and suspension rules on case types.
-
-#### Scenario CT-06a: Enable extension with period
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin sets `extensionAllowed = true` and `extensionPeriod = "P28D"`
-- THEN cases of this type MUST allow one deadline extension of 28 days
-
-#### Scenario CT-06b: Extension period required when extension allowed
-
-- GIVEN a case type with `extensionAllowed = true`
-- WHEN the admin leaves `extensionPeriod` empty
-- THEN the system MUST reject the save
-- AND display: "Extension period is required when extension is allowed"
-
-#### Scenario CT-06c: Disable extension
-
-- GIVEN a case type "Klacht behandeling" in edit mode
-- WHEN the admin sets `extensionAllowed = false`
-- THEN the `extensionPeriod` field MUST be hidden or disabled
-- AND cases of this type MUST NOT allow deadline extensions
-
-#### Scenario CT-06d: Enable suspension (V1)
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin sets `suspensionAllowed = true`
-- THEN cases of this type MUST allow suspension (pausing the deadline countdown)
-
-#### Scenario CT-06e: Disable suspension (V1)
-
-- GIVEN a case type "Melding" with `suspensionAllowed = false`
-- WHEN a handler attempts to suspend a case of this type
-- THEN the system MUST reject the suspension
-
----
-
-### REQ-CT-07: Result Type Management
-
-**Feature tier**: V1
-
-The system SHOULD support defining result types with archival rules for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
-
-#### Scenario CT-07a: Add result types to a case type
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin adds result types:
- - "Vergunning verleend" (archiveAction: retain, retentionPeriod: P20Y, retentionDateSource: case_completed)
- - "Vergunning geweigerd" (archiveAction: destroy, retentionPeriod: P10Y, retentionDateSource: case_completed)
- - "Ingetrokken" (archiveAction: destroy, retentionPeriod: P5Y, retentionDateSource: case_completed)
-- THEN each result type MUST be created as an OpenRegister object linked to the case type
-- AND the admin list MUST display: name, archive action, retention period
-
-#### Scenario CT-07b: Edit a result type
-
-- GIVEN a result type "Vergunning verleend" with `retentionPeriod = "P20Y"`
-- WHEN the admin changes `retentionPeriod` to "P25Y"
-- THEN the result type MUST be updated
-- AND the change MUST apply to future case closures only
-
-#### Scenario CT-07c: Delete a result type
-
-- GIVEN a result type "Ingetrokken" not referenced by any closed cases
-- WHEN the admin deletes it
-- THEN the result type MUST be removed from the case type
-
-#### Scenario CT-07d: Delete result type in use -- blocked
-
-- GIVEN a result type "Vergunning verleend" referenced by 5 closed cases
-- WHEN the admin attempts to delete it
-- THEN the system MUST reject the deletion
-- AND display: "Cannot delete result type 'Vergunning verleend': referenced by 5 closed cases"
-
-#### Scenario CT-07e: Retention date source options
-
-- GIVEN the result type edit form
-- WHEN the admin selects the `retentionDateSource` dropdown
-- THEN the options MUST include: case_completed, decision_effective, decision_expiry, fixed_period, related_case, parent_case, custom_property, custom_date
-
----
-
-### REQ-CT-08: Role Type Management
-
-**Feature tier**: V1
-
-The system SHOULD support defining allowed role types for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
-
-#### Scenario CT-08a: Add role types to a case type
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin adds role types:
- - "Aanvrager" (genericRole: initiator)
- - "Behandelaar" (genericRole: handler)
- - "Technisch adviseur" (genericRole: advisor)
- - "Beslisser" (genericRole: decision_maker)
-- THEN each role type MUST be created as an OpenRegister object linked to the case type
-- AND the admin list MUST display: name, generic role
-
-#### Scenario CT-08b: Generic role options
-
-- GIVEN the role type creation form
-- WHEN the admin selects the `genericRole` dropdown
-- THEN the options MUST include: initiator, handler, advisor, decision_maker, stakeholder, coordinator, contact, co_initiator
-
-#### Scenario CT-08c: Role types restrict case role assignment
-
-- GIVEN a case of type "Omgevingsvergunning" with role types ["Aanvrager", "Behandelaar", "Technisch adviseur", "Beslisser"]
-- WHEN a user adds a participant to the case
-- THEN the role selection MUST only show roles from the case type's role type list
-- AND the user MUST NOT be able to assign "Zaakcoordinator" if it is not defined
-
-#### Scenario CT-08d: Edit a role type
-
-- GIVEN a role type "Technisch adviseur" with genericRole "advisor"
-- WHEN the admin renames it to "Externe adviseur"
-- THEN the name MUST be updated
-- AND existing role assignments on cases MUST reflect the new name
-
-#### Scenario CT-08e: Delete a role type not in use
-
-- GIVEN a role type "Beslisser" not assigned on any active cases
-- WHEN the admin deletes it
-- THEN the role type MUST be removed from the case type
-
----
-
-### REQ-CT-09: Property Definition Management
-
-**Feature tier**: V1
-
-The system SHOULD support defining custom field requirements for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
-
-#### Scenario CT-09a: Add property definitions
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin adds property definitions:
- - "Kadastraal nummer" (format: text, maxLength: 20, requiredAtStatus: "In behandeling")
- - "Bouwkosten" (format: number, requiredAtStatus: "Besluitvorming")
- - "Oppervlakte" (format: number, no requiredAtStatus)
- - "Bouwlagen" (format: number, no requiredAtStatus)
-- THEN each property definition MUST be created as an OpenRegister object linked to the case type
-- AND the admin list MUST display: name, format, max length (if set), required at status (if set)
-
-#### Scenario CT-09b: Property format options
-
-- GIVEN the property definition creation form
-- WHEN the admin selects the `format` dropdown
-- THEN the options MUST include: text, number, date, datetime
-
-#### Scenario CT-09c: Property with allowed values (enum)
-
-- GIVEN the admin creating a property definition "Bouwtype"
-- WHEN they set `allowedValues = ["Nieuwbouw", "Verbouw", "Uitbreiding", "Renovatie"]`
-- THEN cases of this type MUST only accept values from this list for the "Bouwtype" field
-
-#### Scenario CT-09d: Property required at status blocks status change
-
-- GIVEN a property "Kadastraal nummer" with `requiredAtStatus` referencing "In behandeling"
-- AND a case that has not filled this property
-- WHEN the user attempts to advance the case to "In behandeling"
-- THEN the system MUST reject the status change
-- AND display: "Cannot advance to 'In behandeling': required property 'Kadastraal nummer' is missing"
-
-#### Scenario CT-09e: Property with maxLength validation
-
-- GIVEN a property "Kadastraal nummer" with `maxLength = 20`
-- WHEN a user enters a value with 25 characters
-- THEN the system MUST reject the input
-- AND display: "Value exceeds maximum length of 20 characters"
-
-#### Scenario CT-09f: Delete a property definition
-
-- GIVEN a property definition "Oppervlakte" on case type "Omgevingsvergunning"
-- WHEN the admin deletes it
-- THEN the property definition MUST be removed
-- AND existing property values on cases SHOULD be preserved (not deleted) but the field SHOULD no longer appear for new cases
-
----
-
-### REQ-CT-10: Document Type Management
-
-**Feature tier**: V1
-
-The system SHOULD support defining required document types for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
-
-#### Scenario CT-10a: Add document types
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin adds document types:
- - "Bouwtekening" (category: "Tekening", direction: incoming, order: 1, requiredAtStatus: "In behandeling")
- - "Constructieberekening" (category: "Tekening", direction: incoming, order: 2, requiredAtStatus: "In behandeling")
- - "Situatietekening" (category: "Tekening", direction: incoming, order: 3, requiredAtStatus: "In behandeling")
- - "Welstandsadvies" (category: "Advies", direction: internal, order: 4, requiredAtStatus: "Besluitvorming")
- - "Vergunningsbesluit" (category: "Besluit", direction: outgoing, order: 5, requiredAtStatus: "Afgehandeld")
-- THEN each document type MUST be created as an OpenRegister object linked to the case type
-- AND the admin list MUST display: name, direction, required at status
-
-#### Scenario CT-10b: Direction options
-
-- GIVEN the document type creation form
-- WHEN the admin selects the `direction` dropdown
-- THEN the options MUST include: incoming, internal, outgoing
-
-#### Scenario CT-10c: Document type required at status blocks status change
-
-- GIVEN a document type "Welstandsadvies" with `requiredAtStatus` referencing "Besluitvorming"
-- AND a case that has no "Welstandsadvies" file uploaded
-- WHEN the user attempts to advance the case to "Besluitvorming"
-- THEN the system MUST reject the status change
-- AND display: "Cannot advance to 'Besluitvorming': required document 'Welstandsadvies' is missing"
-
-#### Scenario CT-10d: Document type with confidentiality
-
-- GIVEN a document type "Vergunningsbesluit" with `confidentiality = "case_sensitive"`
-- WHEN a file of this type is uploaded to a case
-- THEN the file SHOULD inherit the confidentiality level "case_sensitive"
-
-#### Scenario CT-10e: Delete a document type
-
-- GIVEN a document type "Situatietekening" on case type "Omgevingsvergunning"
-- WHEN the admin deletes it
-- THEN the document type MUST be removed from the case type
-- AND existing uploaded files MUST NOT be deleted (files remain, only the requirement is removed)
-
----
-
-### REQ-CT-11: Decision Type Management
-
-**Feature tier**: V1
-
-The system SHOULD support defining decision types for each case type.
-
-#### Scenario CT-11a: Add decision types
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin adds a decision type:
- - Name: "Vergunningsbesluit"
- - Category: "Vergunning"
- - Objection period: "P42D" (42 days)
- - Publication required: true
- - Publication period: "P14D" (14 days)
-- THEN the decision type MUST be created as an OpenRegister object linked to the case type
-
-#### Scenario CT-11b: Decision type restricts case decisions
-
-- GIVEN a case of type "Omgevingsvergunning" with decision type "Vergunningsbesluit"
-- WHEN a user creates a decision on the case
-- THEN the decision type selection MUST only show types defined by the case type
-
-#### Scenario CT-11c: Decision type with objection period
-
-- GIVEN a decision type "Vergunningsbesluit" with `objectionPeriod = "P42D"`
-- WHEN a decision of this type is recorded with `effectiveDate = "2026-03-01"`
-- THEN the system SHOULD calculate and display the objection deadline: "2026-04-12"
-
----
-
-### REQ-CT-12: Confidentiality Default
-
-**Feature tier**: V1
-
-The system SHOULD support confidentiality defaults on case types. Cases inherit the case type's confidentiality level.
-
-#### Scenario CT-12a: Set confidentiality default
-
-- GIVEN a case type "Omgevingsvergunning" in edit mode
-- WHEN the admin sets `confidentiality = "internal"`
-- THEN new cases of this type MUST default to confidentiality "internal"
-
-#### Scenario CT-12b: Confidentiality level options
-
-- GIVEN the case type confidentiality dropdown
-- WHEN the admin opens the dropdown
-- THEN the options MUST include: public, restricted, internal, case_sensitive, confidential, highly_confidential, secret, top_secret
-- AND the options MUST be ordered from least to most restrictive
-
-#### Scenario CT-12c: Overriding confidentiality on a case
-
-- GIVEN a case type with `confidentiality = "internal"`
-- AND a case created with this type (default "internal")
-- WHEN the handler changes the case confidentiality to "confidential"
-- THEN the case MUST update to "confidential"
-- AND the audit trail MUST record the change
-
----
-
-### REQ-CT-13: Default Case Type Selection
-
-**Feature tier**: MVP
-
-The system MUST support selecting a default case type in admin settings. The default case type is pre-selected when creating new cases.
-
-#### Scenario CT-13a: Set default case type
-
-- GIVEN case types "Omgevingsvergunning" (published), "Subsidieaanvraag" (published), "Klacht" (published)
-- WHEN the admin marks "Omgevingsvergunning" as the default
-- THEN "Omgevingsvergunning" MUST appear with a visual indicator (e.g., star) in the admin list
-- AND the "New Case" form MUST pre-select "Omgevingsvergunning"
-
-#### Scenario CT-13b: Only published case types can be default
-
-- GIVEN a draft case type "Bezwaarschrift"
-- WHEN the admin attempts to mark it as default
-- THEN the system MUST reject the action
-- AND display: "Only published case types can be set as default"
-
-#### Scenario CT-13c: Change default case type
-
-- GIVEN "Omgevingsvergunning" is the current default
-- WHEN the admin sets "Subsidieaanvraag" as the new default
-- THEN "Subsidieaanvraag" MUST become the default
-- AND "Omgevingsvergunning" MUST lose its default status (only one default at a time)
-
----
-
-### REQ-CT-14: Case Type Validation Rules
-
-**Feature tier**: MVP
-
-The system MUST enforce validation rules when creating or modifying case types.
-
-#### Scenario CT-14a: Title is required
-
-- GIVEN a case type creation form
-- WHEN the admin submits with an empty title
-- THEN the system MUST reject with error: "Title is required"
-
-#### Scenario CT-14b: Processing deadline is required
-
-- GIVEN a case type creation form
-- WHEN the admin submits without a processing deadline
-- THEN the system MUST reject with error: "Processing deadline is required"
-
-#### Scenario CT-14c: Processing deadline must be valid ISO 8601 duration
-
-- GIVEN a case type in edit mode
-- WHEN the admin enters "two months" as the processing deadline
-- THEN the system MUST reject with error: "Processing deadline must be a valid ISO 8601 duration (e.g., P56D, P8W, P2M)"
-
-#### Scenario CT-14d: Valid ISO 8601 durations accepted
-
-- GIVEN a case type in edit mode
-- WHEN the admin enters any of: "P56D" (56 days), "P8W" (8 weeks), "P2M" (2 months), "P1Y" (1 year)
-- THEN the system MUST accept the input
-- AND display the human-readable equivalent
-
-#### Scenario CT-14e: Required fields for case type
-
-- GIVEN a case type creation form
-- WHEN the admin leaves any of these fields empty: purpose, trigger, subject, origin, confidentiality, responsibleUnit
-- THEN the system MUST reject the submission
-- AND display validation errors for each missing required field
-
-#### Scenario CT-14f: ValidUntil must be after validFrom
-
-- GIVEN a case type with `validFrom = "2026-01-01"`
-- WHEN the admin sets `validUntil = "2025-12-31"` (before validFrom)
-- THEN the system MUST reject with error: "'Valid until' must be after 'Valid from'"
-
-#### Scenario CT-14g: Extension period required when extension allowed
-
-- GIVEN a case type with `extensionAllowed = true`
-- WHEN the admin leaves `extensionPeriod` empty
-- THEN the system MUST reject with error: "Extension period is required when extension is allowed"
-
----
-
-### REQ-CT-15: Case Type Admin UI Tabs
-
-**Feature tier**: MVP (General, Statuses), V1 (Results, Roles, Properties, Docs)
-
-The case type edit page MUST be organized into tabs for managing the type and its sub-types. See wireframe 3.7 in DESIGN-REFERENCES.md.
-
-#### Scenario CT-15a: Tab layout
-
-- GIVEN the admin editing a case type "Omgevingsvergunning"
-- WHEN the edit page loads
-- THEN the page MUST display tabs: General, Statuses, Results, Roles, Properties, Docs
-- AND the "General" tab MUST be active by default
-- AND a "Save" button MUST be visible at the top
-
-#### Scenario CT-15b: General tab content
-
-- GIVEN the admin on the "General" tab
-- THEN the tab MUST display editable fields for: title, description, purpose, trigger, subject, processing deadline (with ISO 8601 helper), service target, extension allowed (with conditional period), suspension allowed, origin, confidentiality, publication required (with conditional text), valid from, valid until, status (published/draft)
-
-#### Scenario CT-15c: Statuses tab content
-
-- GIVEN the admin on the "Statuses" tab
-- THEN the tab MUST display an ordered list of status types with drag handles
-- AND each status type MUST show: order number, name, isFinal checkbox, notifyInitiator checkbox (with conditional text field)
-- AND an "Add" button MUST be available
-
-#### Scenario CT-15d: Results tab content (V1)
-
-- GIVEN the admin on the "Results" tab
-- THEN the tab MUST display a list of result types
-- AND each result type MUST show: name, archive action, retention period
-- AND an "Add" button MUST be available
-
-#### Scenario CT-15e: Roles tab content (V1)
-
-- GIVEN the admin on the "Roles" tab
-- THEN the tab MUST display a list of role types
-- AND each role type MUST show: name, generic role
-- AND an "Add" button MUST be available
-
-#### Scenario CT-15f: Properties tab content (V1)
-
-- GIVEN the admin on the "Properties" tab
-- THEN the tab MUST display a list of property definitions
-- AND each property MUST show: name, format, max length (if set), required at status (if set)
-- AND an "Add" button MUST be available
-
-#### Scenario CT-15g: Docs tab content (V1)
-
-- GIVEN the admin on the "Docs" tab
-- THEN the tab MUST display a list of document types
-- AND each document type MUST show: name, direction (incoming/internal/outgoing), required at status (if set)
-- AND an "Add" button MUST be available
-
----
-
-### REQ-CT-16: Case Type Error Scenarios
-
-**Feature tier**: MVP
-
-The system MUST handle error scenarios gracefully for case type operations.
-
-#### Scenario CT-16a: Publish incomplete case type
-
-- GIVEN a case type with title and processing deadline filled but no purpose, trigger, or subject
-- WHEN the admin attempts to publish
-- THEN the system MUST reject with validation errors listing all missing required fields
-
-#### Scenario CT-16b: Add status type without order
-
-- GIVEN an admin adding a status type to a case type
-- WHEN they submit without setting the `order` field
-- THEN the system MUST either reject with "Order is required" or auto-assign the next sequential order number
-
-#### Scenario CT-16c: Duplicate status type order
-
-- GIVEN a case type with status type "Ontvangen" at order 1
-- WHEN the admin adds a new status type "Intake" also at order 1
-- THEN the system MUST reject with error: "A status type with order 1 already exists. Each status type must have a unique order."
-
-#### Scenario CT-16d: Delete case type with closed cases
-
-- GIVEN a case type "Subsidieaanvraag" with 5 closed cases and 0 active cases
-- WHEN the admin attempts to delete the case type
-- THEN the system MUST warn: "This case type is referenced by 5 closed cases. Deleting it will remove the type reference from those cases."
-- AND if confirmed, the deletion SHOULD proceed
-
----
-
-## UI References
-
-- **Case Type List**: See wireframe 3.6 in DESIGN-REFERENCES.md (admin settings, case type cards with status/deadline/validity)
-- **Case Type Editor**: See wireframe 3.7 in DESIGN-REFERENCES.md (tabbed interface: General, Statuses, Results, Roles, Properties, Docs)
-
-## Dependencies
-
-- **Case Management spec** (`../case-management/spec.md`): Cases reference case types for behavioral controls (statuses, deadlines, confidentiality, document requirements, property requirements, result types, role types).
-- **OpenRegister**: All case type data is stored as OpenRegister objects in the `procest` register under the respective schemas (caseType, statusType, resultType, roleType, propertyDefinition, documentType, decisionType).
-- **Nextcloud Admin Settings**: Case type management is exposed via the Nextcloud admin settings panel (`OCA\Procest\Settings\AdminSettings`).
-
-### Current Implementation Status
-
-**Substantially implemented (MVP).** Core case type CRUD and status type management are functional.
-
-**Implemented:**
-- Case type CRUD via OpenRegister object store -- create, read, update, delete case types as OpenRegister objects in the `procest` register with the `caseType` schema.
-- Case type list display (`src/views/settings/CaseTypeList.vue`) with title, isDraft badge (Draft/Published), processing deadline (formatted via `durationHelpers.js`), validity period, default star icon, delete action, set-as-default action (published only).
-- Case type detail/edit with tabbed interface (`src/views/settings/CaseTypeDetail.vue`) -- General and Statuses tabs implemented. Publish/unpublish buttons with validation error display.
-- General tab (`src/views/settings/tabs/GeneralTab.vue`) with all core fields: title, description, purpose, trigger, subject, processing deadline, service target, extension allowed/period, suspension allowed, origin, confidentiality, publication required/text, valid from, valid until.
-- Statuses tab (`src/views/settings/tabs/StatusesTab.vue`) with ordered status type list, drag-and-drop reorder, inline editing, add/delete, isFinal checkbox, notifyInitiator toggle with notification text.
-- Draft/published lifecycle with publish validation (publish errors displayed in UI).
-- Default case type selection stored via `SettingsService` config key `default_case_type`.
-- Case type validation utilities (`src/utils/caseTypeValidation.js`).
-- All case type sub-entity schemas defined in `procest_register.json` and mapped in `SettingsService::SLUG_TO_CONFIG_KEY`: caseType, statusType, resultType, roleType, propertyDefinition, documentType, decisionType.
-- ZGW Catalogi API compatibility via `ZtcController` (`lib/Controller/ZtcController.php`) and `ZgwZtcRulesService` (`lib/Service/ZgwZtcRulesService.php`).
-
-**Not yet implemented (V1):**
-- REQ-CT-07: Result type management tab (schema exists, no UI).
-- REQ-CT-08: Role type management tab (schema exists, no UI).
-- REQ-CT-09: Property definition management tab (schema exists, no UI).
-- REQ-CT-10: Document type management tab (schema exists, no UI).
-- REQ-CT-11: Decision type management (schema exists, no UI).
-- REQ-CT-12: Confidentiality default enforcement on case creation (field exists, enforcement unclear).
-- Backend validation for publish prerequisites (at least one status type, at least one final status, validFrom set).
-- Delete case type blocking when active cases reference it.
-- Status type name uniqueness validation within a case type.
-- Duplicate order number detection and auto-renumbering.
-
-### Standards & References
-
-- **ZGW Catalogi API (VNG)**: Direct mapping to ZaakType, StatusType, ResultaatType, RolType, Eigenschap, InformatieObjectType, BesluitType. The `ZtcController` implements ZGW Catalogi API endpoints.
-- **CMMN 1.1**: CaseDefinition concept for case type, Milestone sequence for status types, TimerEventListener for processing deadlines.
-- **Schema.org**: `PropertyValueSpecification` for property definitions.
-- **ISO 8601**: Duration format for all time-based fields (processingDeadline, extensionPeriod, retentionPeriod, objectionPeriod, publicationPeriod).
-- **GEMMA**: ZaakType configuration follows GEMMA zaakgericht werken reference architecture.
-- **Archiefwet / Selectielijst**: Result types with archiveAction (retain/destroy) and retentionPeriod follow Dutch archiving legislation.
-
-### Specificity Assessment
-
-This is a comprehensive, highly detailed spec that is implementation-ready for both MVP and V1. It includes complete data models with field-level ZGW mappings.
-
-**Strengths:** Exhaustive data model tables with type/required/mapping columns. 16 requirements with detailed scenarios. Clear feature tier separation. Validation rules explicitly specified.
-
-**Missing/Ambiguous:**
-- No specification of how sub-entity schemas (statusType, resultType, etc.) relate to each other via OpenRegister references (reference resolution mechanics).
-- No specification of bulk operations (e.g., import multiple status types at once).
-- Case type versioning strategy not specified -- can a published type be edited in-place or must it be versioned?
-- No specification of case type search/filter in the admin list.
-
-**Open questions:**
-1. Should editing a published case type require unpublishing first, or can it be edited in-place with a warning?
-2. How should the system handle changes to a case type that affect existing cases (e.g., removing a status type that cases are currently at)?
-3. Should the `subCaseTypes` field enforce a tree structure (no cycles) and how is this validated?
+# Case Type System Specification
+
+## Purpose
+
+Case types are configurable definitions that control the behavior of cases. A case type determines which statuses are allowed, what roles can be assigned, which custom fields are required, processing deadlines, confidentiality defaults, and archival rules. This is the international equivalent of ZGW's `ZaakType`, modeled after CMMN 1.1 `CaseDefinition` concepts.
+
+Case types form a hierarchy where the CaseType is the central configuration entity:
+
+```
+CaseType
+├── StatusType[] — Allowed lifecycle phases (ordered)
+├── ResultType[] — Allowed outcomes (with archival rules)
+├── RoleType[] — Allowed participant roles
+├── PropertyDefinition[] — Required custom data fields
+├── DocumentType[] — Required document types
+├── DecisionType[] — Allowed decision types
+└── subCaseTypes[] — Allowed sub-case types
+```
+
+**Standards**: CMMN 1.1 (CaseDefinition), ZGW Catalogi API (ZaakType), Schema.org (`PropertyValueSpecification`)
+**Feature tier**: MVP (core type CRUD, statuses, deadlines, draft/published, validity), V1 (result types, role types, property definitions, document types, decision types, confidentiality, suspension/extension)
+
+## Data Model
+
+### Case Type Entity
+
+| Property | Type | CMMN / Schema.org | ZGW Mapping | Required |
+|----------|------|-------------------|-------------|----------|
+| `title` | string | `schema:name` | `zaaktype_omschrijving` | Yes |
+| `description` | string | `schema:description` | `toelichting` | No |
+| `identifier` | string | `schema:identifier` | `identificatie` | Auto |
+| `purpose` | string | -- | `doel` | Yes |
+| `trigger` | string | -- | `aanleiding` | Yes |
+| `subject` | string | -- | `onderwerp` | Yes |
+| `initiatorAction` | string | -- | `handeling_initiator` | Yes |
+| `handlerAction` | string | -- | `handeling_behandelaar` | Yes |
+| `origin` | enum: internal, external | -- | `indicatie_intern_of_extern` | Yes |
+| `processingDeadline` | duration (ISO 8601) | CMMN TimerEventListener | `doorlooptijd_behandeling` | Yes |
+| `serviceTarget` | duration (ISO 8601) | -- | `servicenorm_behandeling` | No |
+| `suspensionAllowed` | boolean | -- | `opschorting_en_aanhouding_mogelijk` | Yes |
+| `extensionAllowed` | boolean | -- | `verlenging_mogelijk` | Yes |
+| `extensionPeriod` | duration (ISO 8601) | -- | `verlengingstermijn` | Conditional (required if extensionAllowed) |
+| `confidentiality` | enum | -- | `vertrouwelijkheidaanduiding` | Yes |
+| `publicationRequired` | boolean | -- | `publicatie_indicatie` | Yes |
+| `publicationText` | string | -- | `publicatietekst` | No |
+| `responsibleUnit` | string | -- | `verantwoordelijke` | Yes |
+| `referenceProcess` | string | -- | `referentieproces_naam` | No |
+| `isDraft` | boolean | -- | `concept` | No (default: true) |
+| `validFrom` | date | -- | `datum_begin_geldigheid` | Yes |
+| `validUntil` | date | -- | `datum_einde_geldigheid` | No |
+| `keywords` | string[] | -- | `trefwoorden` | No |
+| `subCaseTypes` | reference[] | CMMN CaseTask | `deelzaaktypen` | No |
+
+### Status Type Entity
+
+| Property | Type | Source | ZGW Mapping | Required |
+|----------|------|--------|-------------|----------|
+| `name` | string | `schema:name` | `statustype_omschrijving` | Yes |
+| `description` | string | `schema:description` | `toelichting` | No |
+| `caseType` | reference | Parent case type | `zaaktype` | Yes |
+| `order` | integer (1-9999) | CMMN Milestone sequence | `statustypevolgnummer` | Yes |
+| `isFinal` | boolean | CMMN terminal state | (last in order) | No (default: false) |
+| `targetDuration` | duration | -- | `doorlooptijd` | No |
+| `notifyInitiator` | boolean | -- | `informeren` | No (default: false) |
+| `notificationText` | string | -- | `statustekst` | No |
+
+### Result Type Entity (V1)
+
+| Property | Type | Source | ZGW Mapping | Required |
+|----------|------|--------|-------------|----------|
+| `name` | string | `schema:name` | `omschrijving` | Yes |
+| `description` | string | `schema:description` | `toelichting` | No |
+| `caseType` | reference | Parent case type | `zaaktype` | Yes |
+| `archiveAction` | enum: retain, destroy | -- | `archiefnominatie` | No |
+| `retentionPeriod` | duration (ISO 8601) | -- | `archiefactietermijn` | No |
+| `retentionDateSource` | enum | -- | `afleidingswijze` | No |
+
+### Role Type Entity (V1)
+
+| Property | Type | Source | ZGW Mapping | Required |
+|----------|------|--------|-------------|----------|
+| `name` | string | `schema:roleName` | `omschrijving` | Yes |
+| `caseType` | reference | Parent case type | `zaaktype` | Yes |
+| `genericRole` | enum | -- | `omschrijvingGeneriek` | Yes |
+
+### Property Definition Entity (V1)
+
+| Property | Type | Source | ZGW Mapping | Required |
+|----------|------|--------|-------------|----------|
+| `name` | string | `schema:name` | `eigenschapnaam` | Yes |
+| `definition` | string | `schema:description` | `definitie` | Yes |
+| `caseType` | reference | Parent case type | `zaaktype` | Yes |
+| `format` | enum: text, number, date, datetime | -- | `formaat` | Yes |
+| `maxLength` | integer | -- | `lengte` | No |
+| `allowedValues` | string[] | -- | `waardenverzameling` | No |
+| `requiredAtStatus` | reference | Status at which this must be filled | `statustype` | No |
+
+### Document Type Entity (V1)
+
+| Property | Type | Source | ZGW Mapping | Required |
+|----------|------|--------|-------------|----------|
+| `name` | string | `schema:name` | `omschrijving` | Yes |
+| `category` | string | -- | `informatieobjectcategorie` | Yes |
+| `caseType` | reference | Parent case type | `zaaktype` | Yes |
+| `direction` | enum: incoming, internal, outgoing | -- | `richting` | Yes |
+| `order` | integer | -- | `volgnummer` | Yes |
+| `confidentiality` | enum | -- | `vertrouwelijkheidaanduiding` | No |
+| `requiredAtStatus` | reference | Status requiring this document | `statustype` | No |
+
+### Decision Type Entity (V1)
+
+| Property | Type | Source | ZGW Mapping | Required |
+|----------|------|--------|-------------|----------|
+| `name` | string | `schema:name` | `omschrijving` | Yes |
+| `description` | string | `schema:description` | `toelichting` | No |
+| `category` | string | -- | `besluitcategorie` | No |
+| `objectionPeriod` | duration (ISO 8601) | -- | `reactietermijn` | No |
+| `publicationRequired` | boolean | -- | `publicatie_indicatie` | Yes |
+| `publicationPeriod` | duration (ISO 8601) | -- | `publicatietermijn` | No |
+
+## Requirements
+
+---
+
+### REQ-CT-01: Case Type CRUD
+
+**Feature tier**: MVP
+
+The system MUST support creating, reading, updating, and deleting case types. Case types are managed by admins via the Nextcloud admin settings page. See wireframe 3.6 (Admin Settings -- Case Type Management) in DESIGN-REFERENCES.md.
+
+#### Scenario CT-01a: Create a case type
+
+- GIVEN an admin on the Procest settings page
+- WHEN they click "Add Case Type" and fill in:
+ - Title: "Omgevingsvergunning"
+ - Purpose: "Beoordelen bouwplannen"
+ - Trigger: "Aanvraag van burger/bedrijf"
+ - Subject: "Bouw- en verbouwactiviteiten"
+ - Processing deadline: "P56D" (56 days)
+ - Origin: "external"
+ - Confidentiality: "internal"
+ - Responsible unit: "Afdeling Vergunningen, Gemeente Amsterdam"
+ - Valid from: "2026-01-01"
+- AND submits the form
+- THEN the system MUST create an OpenRegister object in the `procest` register with the `caseType` schema
+- AND `isDraft` MUST default to `true`
+- AND a unique `identifier` MUST be auto-generated
+
+#### Scenario CT-01b: Update a case type
+
+- GIVEN an existing case type "Omgevingsvergunning"
+- WHEN the admin changes the `processingDeadline` from "P56D" to "P42D"
+- THEN the system MUST update the OpenRegister object
+- AND the change MUST NOT affect existing cases (only new cases use the updated deadline)
+
+#### Scenario CT-01c: Delete a case type with no active cases
+
+- GIVEN a case type "Testtype" that has no cases associated with it
+- WHEN the admin deletes the case type
+- THEN the system MUST remove the case type and all linked sub-types (status types, result types, role types, property definitions, document types, decision types)
+- AND a confirmation dialog MUST be shown before deletion
+
+#### Scenario CT-01d: Delete a case type with active cases -- blocked
+
+- GIVEN a case type "Omgevingsvergunning" with 10 active cases
+- WHEN the admin attempts to delete the case type
+- THEN the system MUST reject the deletion
+- AND display: "Cannot delete case type 'Omgevingsvergunning': 10 active cases are using this type. Close or reassign all cases first."
+
+#### Scenario CT-01e: Case type list display
+
+- GIVEN case types: "Omgevingsvergunning" (published, default), "Subsidieaanvraag" (published), "Klacht behandeling" (published), "Bezwaarschrift" (draft)
+- WHEN the admin views the case type list
+- THEN each case type MUST display: title, status (Published/Draft), deadline, number of statuses, number of result types, validity period
+- AND the default case type MUST be visually indicated (e.g., star icon)
+- AND draft types MUST be visually distinct (e.g., warning badge)
+
+---
+
+### REQ-CT-02: Case Type Draft/Published Lifecycle
+
+**Feature tier**: MVP
+
+The system MUST support a draft/published lifecycle for case types. Draft case types MUST NOT be usable for creating cases.
+
+#### Scenario CT-02a: New case type defaults to draft
+
+- GIVEN an admin creating a new case type
+- WHEN the case type is created
+- THEN `isDraft` MUST be `true` by default
+- AND the case type MUST show a "DRAFT" badge in the admin list
+
+#### Scenario CT-02b: Publish a case type -- success
+
+- GIVEN a draft case type "Subsidieaanvraag" with:
+ - All required fields filled (title, purpose, trigger, subject, processingDeadline, origin, confidentiality, responsibleUnit, validFrom)
+ - At least one status type defined: "Ontvangen" (order 1), "In behandeling" (order 2), "Afgerond" (order 3, isFinal = true)
+- WHEN the admin sets `isDraft = false`
+- THEN the case type MUST become "Published"
+- AND the case type MUST become available for creating new cases
+
+#### Scenario CT-02c: Publish a case type -- blocked, no status types
+
+- GIVEN a draft case type "Bezwaarschrift" with no status types defined
+- WHEN the admin attempts to publish (set `isDraft = false`)
+- THEN the system MUST reject the publication
+- AND display: "Cannot publish case type 'Bezwaarschrift': at least one status type must be defined"
+
+#### Scenario CT-02d: Publish a case type -- blocked, no final status
+
+- GIVEN a draft case type with 2 status types, neither marked `isFinal = true`
+- WHEN the admin attempts to publish
+- THEN the system MUST reject the publication
+- AND display: "Cannot publish case type: at least one status type must be marked as final"
+
+#### Scenario CT-02e: Publish a case type -- blocked, validFrom not set
+
+- GIVEN a draft case type with `validFrom` not set
+- WHEN the admin attempts to publish
+- THEN the system MUST reject the publication
+- AND display: "Cannot publish case type: 'Valid from' date must be set"
+
+#### Scenario CT-02f: Unpublish a case type
+
+- GIVEN a published case type "Klacht behandeling" with 3 active cases
+- WHEN the admin sets `isDraft = true` (unpublish)
+- THEN the system MUST warn: "Unpublishing this case type will prevent new cases from being created. 3 existing cases will continue to function."
+- AND if confirmed, the case type MUST revert to draft
+- AND existing cases MUST NOT be affected
+
+---
+
+### REQ-CT-03: Case Type Validity Periods
+
+**Feature tier**: MVP
+
+The system MUST support validity windows on case types. Cases can only be created with case types that are within their validity window.
+
+#### Scenario CT-03a: Case type within validity window
+
+- GIVEN a case type "Omgevingsvergunning" with `validFrom = "2026-01-01"` and `validUntil = "2027-12-31"`
+- AND today is "2026-06-15"
+- WHEN a user views the case type in the creation dropdown
+- THEN the case type MUST be available for selection
+
+#### Scenario CT-03b: Case type expired
+
+- GIVEN a case type "Bouwvergunning 2024" with `validUntil = "2025-12-31"`
+- AND today is "2026-02-25"
+- WHEN a user views the case creation form
+- THEN this case type MUST NOT appear in the dropdown (or MUST appear greyed out with "Expired" label)
+- AND if selected via API, the system MUST reject with: "Case type 'Bouwvergunning 2024' expired on 2025-12-31"
+
+#### Scenario CT-03c: Case type not yet valid
+
+- GIVEN a case type "Nieuwe Subsidie 2027" with `validFrom = "2027-01-01"`
+- AND today is "2026-02-25"
+- WHEN a user views the case creation form
+- THEN this case type MUST NOT appear in the dropdown (or MUST appear greyed out with "Not yet valid" label)
+
+#### Scenario CT-03d: Case type with no end date
+
+- GIVEN a case type "Klacht behandeling" with `validFrom = "2026-01-01"` and `validUntil` not set
+- AND today is "2030-12-31"
+- WHEN a user views the case creation form
+- THEN the case type MUST be available (no expiry)
+
+#### Scenario CT-03e: Validity displayed in admin list
+
+- GIVEN case types with varying validity periods
+- WHEN the admin views the case type list
+- THEN each type MUST display its validity range: "Valid: Jan 2026 -- Dec 2027" or "Valid: Jan 2026 -- (no end)"
+
+---
+
+### REQ-CT-04: Status Type Management
+
+**Feature tier**: MVP
+
+The system MUST support defining ordered status types for each case type. Status types control the lifecycle phases a case can go through. See wireframe 3.7 (Admin Settings -- Case Type Detail) in DESIGN-REFERENCES.md.
+
+#### Scenario CT-04a: Add status types to a case type
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin adds the following status types:
+ 1. "Ontvangen" (order: 1)
+ 2. "In behandeling" (order: 2, notifyInitiator: true, notificationText: "Uw zaak is in behandeling genomen")
+ 3. "Besluitvorming" (order: 3)
+ 4. "Afgehandeld" (order: 4, isFinal: true, notifyInitiator: true, notificationText: "Uw zaak is afgehandeld")
+- THEN each status type MUST be created as an OpenRegister object linked to the case type
+- AND they MUST be ordered by the `order` field
+- AND the admin MUST see the ordered list with drag handles for reordering
+
+#### Scenario CT-04b: Reorder status types via drag
+
+- GIVEN a case type with status types in order: [Ontvangen(1), In behandeling(2), Besluitvorming(3), Afgehandeld(4)]
+- WHEN the admin drags "Besluitvorming" before "In behandeling"
+- THEN the `order` values MUST be recalculated: [Ontvangen(1), Besluitvorming(2), In behandeling(3), Afgehandeld(4)]
+- AND the change MUST be persisted
+
+#### Scenario CT-04c: Edit a status type
+
+- GIVEN a status type "In behandeling" (order 2) on case type "Omgevingsvergunning"
+- WHEN the admin changes `notifyInitiator` from false to true and sets `notificationText` to "Uw zaak is in behandeling genomen"
+- THEN the status type MUST be updated
+- AND the change MUST apply to future status transitions (not retroactive)
+
+#### Scenario CT-04d: Delete a status type
+
+- GIVEN a case type "Omgevingsvergunning" with 4 status types
+- AND no active cases are currently at the status "Besluitvorming"
+- WHEN the admin deletes the "Besluitvorming" status type
+- THEN the status type MUST be removed
+- AND the remaining status types MUST retain their relative order
+
+#### Scenario CT-04e: Cannot delete status type in use
+
+- GIVEN a case type "Omgevingsvergunning"
+- AND 3 active cases are currently at status "In behandeling"
+- WHEN the admin attempts to delete "In behandeling"
+- THEN the system MUST reject the deletion
+- AND display: "Cannot delete status type 'In behandeling': 3 active cases are currently at this status"
+
+#### Scenario CT-04f: At least one final status required
+
+- GIVEN a case type with 3 status types, one marked `isFinal = true`
+- WHEN the admin attempts to unmark the final status (set `isFinal = false`)
+- AND no other status is marked as final
+- THEN the system MUST reject the change
+- AND display: "At least one status type must be marked as final"
+
+#### Scenario CT-04g: Status type order is required
+
+- GIVEN an admin adding a new status type
+- WHEN they submit without setting the `order` field
+- THEN the system MUST reject the submission
+- AND display: "Order is required for status types"
+
+#### Scenario CT-04h: Status type name is required
+
+- GIVEN an admin adding a new status type
+- WHEN they submit with an empty `name`
+- THEN the system MUST reject the submission
+- AND display: "Status type name is required"
+
+#### Scenario CT-04i: Status type notification fields
+
+- GIVEN a status type with `notifyInitiator = true`
+- WHEN displayed in the admin edit view
+- THEN the notification checkbox MUST be checked
+- AND the notification text field MUST be visible and editable
+- AND the notification text SHOULD be displayed below the status name in the ordered list
+
+---
+
+### REQ-CT-05: Processing Deadline Configuration
+
+**Feature tier**: MVP
+
+The system MUST support configuring a processing deadline on each case type. The deadline is an ISO 8601 duration that controls automatic deadline calculation on cases.
+
+#### Scenario CT-05a: Set processing deadline
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin sets `processingDeadline = "P56D"` (56 days)
+- THEN the system MUST store the duration in ISO 8601 format
+- AND the admin UI MUST display this as "56 days"
+
+#### Scenario CT-05b: Invalid processing deadline format
+
+- GIVEN a case type in edit mode
+- WHEN the admin enters "56 days" (not ISO 8601) as the processing deadline
+- THEN the system MUST reject the input
+- AND display: "Processing deadline must be a valid ISO 8601 duration (e.g., P56D for 56 days, P8W for 8 weeks)"
+
+#### Scenario CT-05c: Service target (optional)
+
+- GIVEN a case type "Omgevingsvergunning" with `processingDeadline = "P56D"`
+- WHEN the admin also sets `serviceTarget = "P42D"` (42 days)
+- THEN the service target MUST be stored separately
+- AND cases SHOULD display both the service target and the hard deadline
+
+#### Scenario CT-05d: Deadline calculation on case creation
+
+- GIVEN a case type with `processingDeadline = "P56D"`
+- WHEN a case is created with `startDate = "2026-03-01"`
+- THEN the case `deadline` MUST be calculated as "2026-04-26" (March 1 + 56 days)
+
+---
+
+### REQ-CT-06: Extension and Suspension Configuration
+
+**Feature tier**: MVP (extension), V1 (suspension)
+
+The system MUST support configuring extension and suspension rules on case types.
+
+#### Scenario CT-06a: Enable extension with period
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin sets `extensionAllowed = true` and `extensionPeriod = "P28D"`
+- THEN cases of this type MUST allow one deadline extension of 28 days
+
+#### Scenario CT-06b: Extension period required when extension allowed
+
+- GIVEN a case type with `extensionAllowed = true`
+- WHEN the admin leaves `extensionPeriod` empty
+- THEN the system MUST reject the save
+- AND display: "Extension period is required when extension is allowed"
+
+#### Scenario CT-06c: Disable extension
+
+- GIVEN a case type "Klacht behandeling" in edit mode
+- WHEN the admin sets `extensionAllowed = false`
+- THEN the `extensionPeriod` field MUST be hidden or disabled
+- AND cases of this type MUST NOT allow deadline extensions
+
+#### Scenario CT-06d: Enable suspension (V1)
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin sets `suspensionAllowed = true`
+- THEN cases of this type MUST allow suspension (pausing the deadline countdown)
+
+#### Scenario CT-06e: Disable suspension (V1)
+
+- GIVEN a case type "Melding" with `suspensionAllowed = false`
+- WHEN a handler attempts to suspend a case of this type
+- THEN the system MUST reject the suspension
+
+---
+
+### REQ-CT-07: Result Type Management
+
+**Feature tier**: V1
+
+The system SHOULD support defining result types with archival rules for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
+
+#### Scenario CT-07a: Add result types to a case type
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin adds result types:
+ - "Vergunning verleend" (archiveAction: retain, retentionPeriod: P20Y, retentionDateSource: case_completed)
+ - "Vergunning geweigerd" (archiveAction: destroy, retentionPeriod: P10Y, retentionDateSource: case_completed)
+ - "Ingetrokken" (archiveAction: destroy, retentionPeriod: P5Y, retentionDateSource: case_completed)
+- THEN each result type MUST be created as an OpenRegister object linked to the case type
+- AND the admin list MUST display: name, archive action, retention period
+
+#### Scenario CT-07b: Edit a result type
+
+- GIVEN a result type "Vergunning verleend" with `retentionPeriod = "P20Y"`
+- WHEN the admin changes `retentionPeriod` to "P25Y"
+- THEN the result type MUST be updated
+- AND the change MUST apply to future case closures only
+
+#### Scenario CT-07c: Delete a result type
+
+- GIVEN a result type "Ingetrokken" not referenced by any closed cases
+- WHEN the admin deletes it
+- THEN the result type MUST be removed from the case type
+
+#### Scenario CT-07d: Delete result type in use -- blocked
+
+- GIVEN a result type "Vergunning verleend" referenced by 5 closed cases
+- WHEN the admin attempts to delete it
+- THEN the system MUST reject the deletion
+- AND display: "Cannot delete result type 'Vergunning verleend': referenced by 5 closed cases"
+
+#### Scenario CT-07e: Retention date source options
+
+- GIVEN the result type edit form
+- WHEN the admin selects the `retentionDateSource` dropdown
+- THEN the options MUST include: case_completed, decision_effective, decision_expiry, fixed_period, related_case, parent_case, custom_property, custom_date
+
+---
+
+### REQ-CT-08: Role Type Management
+
+**Feature tier**: V1
+
+The system SHOULD support defining allowed role types for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
+
+#### Scenario CT-08a: Add role types to a case type
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin adds role types:
+ - "Aanvrager" (genericRole: initiator)
+ - "Behandelaar" (genericRole: handler)
+ - "Technisch adviseur" (genericRole: advisor)
+ - "Beslisser" (genericRole: decision_maker)
+- THEN each role type MUST be created as an OpenRegister object linked to the case type
+- AND the admin list MUST display: name, generic role
+
+#### Scenario CT-08b: Generic role options
+
+- GIVEN the role type creation form
+- WHEN the admin selects the `genericRole` dropdown
+- THEN the options MUST include: initiator, handler, advisor, decision_maker, stakeholder, coordinator, contact, co_initiator
+
+#### Scenario CT-08c: Role types restrict case role assignment
+
+- GIVEN a case of type "Omgevingsvergunning" with role types ["Aanvrager", "Behandelaar", "Technisch adviseur", "Beslisser"]
+- WHEN a user adds a participant to the case
+- THEN the role selection MUST only show roles from the case type's role type list
+- AND the user MUST NOT be able to assign "Zaakcoordinator" if it is not defined
+
+#### Scenario CT-08d: Edit a role type
+
+- GIVEN a role type "Technisch adviseur" with genericRole "advisor"
+- WHEN the admin renames it to "Externe adviseur"
+- THEN the name MUST be updated
+- AND existing role assignments on cases MUST reflect the new name
+
+#### Scenario CT-08e: Delete a role type not in use
+
+- GIVEN a role type "Beslisser" not assigned on any active cases
+- WHEN the admin deletes it
+- THEN the role type MUST be removed from the case type
+
+---
+
+### REQ-CT-09: Property Definition Management
+
+**Feature tier**: V1
+
+The system SHOULD support defining custom field requirements for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
+
+#### Scenario CT-09a: Add property definitions
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin adds property definitions:
+ - "Kadastraal nummer" (format: text, maxLength: 20, requiredAtStatus: "In behandeling")
+ - "Bouwkosten" (format: number, requiredAtStatus: "Besluitvorming")
+ - "Oppervlakte" (format: number, no requiredAtStatus)
+ - "Bouwlagen" (format: number, no requiredAtStatus)
+- THEN each property definition MUST be created as an OpenRegister object linked to the case type
+- AND the admin list MUST display: name, format, max length (if set), required at status (if set)
+
+#### Scenario CT-09b: Property format options
+
+- GIVEN the property definition creation form
+- WHEN the admin selects the `format` dropdown
+- THEN the options MUST include: text, number, date, datetime
+
+#### Scenario CT-09c: Property with allowed values (enum)
+
+- GIVEN the admin creating a property definition "Bouwtype"
+- WHEN they set `allowedValues = ["Nieuwbouw", "Verbouw", "Uitbreiding", "Renovatie"]`
+- THEN cases of this type MUST only accept values from this list for the "Bouwtype" field
+
+#### Scenario CT-09d: Property required at status blocks status change
+
+- GIVEN a property "Kadastraal nummer" with `requiredAtStatus` referencing "In behandeling"
+- AND a case that has not filled this property
+- WHEN the user attempts to advance the case to "In behandeling"
+- THEN the system MUST reject the status change
+- AND display: "Cannot advance to 'In behandeling': required property 'Kadastraal nummer' is missing"
+
+#### Scenario CT-09e: Property with maxLength validation
+
+- GIVEN a property "Kadastraal nummer" with `maxLength = 20`
+- WHEN a user enters a value with 25 characters
+- THEN the system MUST reject the input
+- AND display: "Value exceeds maximum length of 20 characters"
+
+#### Scenario CT-09f: Delete a property definition
+
+- GIVEN a property definition "Oppervlakte" on case type "Omgevingsvergunning"
+- WHEN the admin deletes it
+- THEN the property definition MUST be removed
+- AND existing property values on cases SHOULD be preserved (not deleted) but the field SHOULD no longer appear for new cases
+
+---
+
+### REQ-CT-10: Document Type Management
+
+**Feature tier**: V1
+
+The system SHOULD support defining required document types for each case type. See wireframe 3.7 in DESIGN-REFERENCES.md.
+
+#### Scenario CT-10a: Add document types
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin adds document types:
+ - "Bouwtekening" (category: "Tekening", direction: incoming, order: 1, requiredAtStatus: "In behandeling")
+ - "Constructieberekening" (category: "Tekening", direction: incoming, order: 2, requiredAtStatus: "In behandeling")
+ - "Situatietekening" (category: "Tekening", direction: incoming, order: 3, requiredAtStatus: "In behandeling")
+ - "Welstandsadvies" (category: "Advies", direction: internal, order: 4, requiredAtStatus: "Besluitvorming")
+ - "Vergunningsbesluit" (category: "Besluit", direction: outgoing, order: 5, requiredAtStatus: "Afgehandeld")
+- THEN each document type MUST be created as an OpenRegister object linked to the case type
+- AND the admin list MUST display: name, direction, required at status
+
+#### Scenario CT-10b: Direction options
+
+- GIVEN the document type creation form
+- WHEN the admin selects the `direction` dropdown
+- THEN the options MUST include: incoming, internal, outgoing
+
+#### Scenario CT-10c: Document type required at status blocks status change
+
+- GIVEN a document type "Welstandsadvies" with `requiredAtStatus` referencing "Besluitvorming"
+- AND a case that has no "Welstandsadvies" file uploaded
+- WHEN the user attempts to advance the case to "Besluitvorming"
+- THEN the system MUST reject the status change
+- AND display: "Cannot advance to 'Besluitvorming': required document 'Welstandsadvies' is missing"
+
+#### Scenario CT-10d: Document type with confidentiality
+
+- GIVEN a document type "Vergunningsbesluit" with `confidentiality = "case_sensitive"`
+- WHEN a file of this type is uploaded to a case
+- THEN the file SHOULD inherit the confidentiality level "case_sensitive"
+
+#### Scenario CT-10e: Delete a document type
+
+- GIVEN a document type "Situatietekening" on case type "Omgevingsvergunning"
+- WHEN the admin deletes it
+- THEN the document type MUST be removed from the case type
+- AND existing uploaded files MUST NOT be deleted (files remain, only the requirement is removed)
+
+---
+
+### REQ-CT-11: Decision Type Management
+
+**Feature tier**: V1
+
+The system SHOULD support defining decision types for each case type.
+
+#### Scenario CT-11a: Add decision types
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin adds a decision type:
+ - Name: "Vergunningsbesluit"
+ - Category: "Vergunning"
+ - Objection period: "P42D" (42 days)
+ - Publication required: true
+ - Publication period: "P14D" (14 days)
+- THEN the decision type MUST be created as an OpenRegister object linked to the case type
+
+#### Scenario CT-11b: Decision type restricts case decisions
+
+- GIVEN a case of type "Omgevingsvergunning" with decision type "Vergunningsbesluit"
+- WHEN a user creates a decision on the case
+- THEN the decision type selection MUST only show types defined by the case type
+
+#### Scenario CT-11c: Decision type with objection period
+
+- GIVEN a decision type "Vergunningsbesluit" with `objectionPeriod = "P42D"`
+- WHEN a decision of this type is recorded with `effectiveDate = "2026-03-01"`
+- THEN the system SHOULD calculate and display the objection deadline: "2026-04-12"
+
+---
+
+### REQ-CT-12: Confidentiality Default
+
+**Feature tier**: V1
+
+The system SHOULD support confidentiality defaults on case types. Cases inherit the case type's confidentiality level.
+
+#### Scenario CT-12a: Set confidentiality default
+
+- GIVEN a case type "Omgevingsvergunning" in edit mode
+- WHEN the admin sets `confidentiality = "internal"`
+- THEN new cases of this type MUST default to confidentiality "internal"
+
+#### Scenario CT-12b: Confidentiality level options
+
+- GIVEN the case type confidentiality dropdown
+- WHEN the admin opens the dropdown
+- THEN the options MUST include: public, restricted, internal, case_sensitive, confidential, highly_confidential, secret, top_secret
+- AND the options MUST be ordered from least to most restrictive
+
+#### Scenario CT-12c: Overriding confidentiality on a case
+
+- GIVEN a case type with `confidentiality = "internal"`
+- AND a case created with this type (default "internal")
+- WHEN the handler changes the case confidentiality to "confidential"
+- THEN the case MUST update to "confidential"
+- AND the audit trail MUST record the change
+
+---
+
+### REQ-CT-13: Default Case Type Selection
+
+**Feature tier**: MVP
+
+The system MUST support selecting a default case type in admin settings. The default case type is pre-selected when creating new cases.
+
+#### Scenario CT-13a: Set default case type
+
+- GIVEN case types "Omgevingsvergunning" (published), "Subsidieaanvraag" (published), "Klacht" (published)
+- WHEN the admin marks "Omgevingsvergunning" as the default
+- THEN "Omgevingsvergunning" MUST appear with a visual indicator (e.g., star) in the admin list
+- AND the "New Case" form MUST pre-select "Omgevingsvergunning"
+
+#### Scenario CT-13b: Only published case types can be default
+
+- GIVEN a draft case type "Bezwaarschrift"
+- WHEN the admin attempts to mark it as default
+- THEN the system MUST reject the action
+- AND display: "Only published case types can be set as default"
+
+#### Scenario CT-13c: Change default case type
+
+- GIVEN "Omgevingsvergunning" is the current default
+- WHEN the admin sets "Subsidieaanvraag" as the new default
+- THEN "Subsidieaanvraag" MUST become the default
+- AND "Omgevingsvergunning" MUST lose its default status (only one default at a time)
+
+---
+
+### REQ-CT-14: Case Type Validation Rules
+
+**Feature tier**: MVP
+
+The system MUST enforce validation rules when creating or modifying case types.
+
+#### Scenario CT-14a: Title is required
+
+- GIVEN a case type creation form
+- WHEN the admin submits with an empty title
+- THEN the system MUST reject with error: "Title is required"
+
+#### Scenario CT-14b: Processing deadline is required
+
+- GIVEN a case type creation form
+- WHEN the admin submits without a processing deadline
+- THEN the system MUST reject with error: "Processing deadline is required"
+
+#### Scenario CT-14c: Processing deadline must be valid ISO 8601 duration
+
+- GIVEN a case type in edit mode
+- WHEN the admin enters "two months" as the processing deadline
+- THEN the system MUST reject with error: "Processing deadline must be a valid ISO 8601 duration (e.g., P56D, P8W, P2M)"
+
+#### Scenario CT-14d: Valid ISO 8601 durations accepted
+
+- GIVEN a case type in edit mode
+- WHEN the admin enters any of: "P56D" (56 days), "P8W" (8 weeks), "P2M" (2 months), "P1Y" (1 year)
+- THEN the system MUST accept the input
+- AND display the human-readable equivalent
+
+#### Scenario CT-14e: Required fields for case type
+
+- GIVEN a case type creation form
+- WHEN the admin leaves any of these fields empty: purpose, trigger, subject, origin, confidentiality, responsibleUnit
+- THEN the system MUST reject the submission
+- AND display validation errors for each missing required field
+
+#### Scenario CT-14f: ValidUntil must be after validFrom
+
+- GIVEN a case type with `validFrom = "2026-01-01"`
+- WHEN the admin sets `validUntil = "2025-12-31"` (before validFrom)
+- THEN the system MUST reject with error: "'Valid until' must be after 'Valid from'"
+
+#### Scenario CT-14g: Extension period required when extension allowed
+
+- GIVEN a case type with `extensionAllowed = true`
+- WHEN the admin leaves `extensionPeriod` empty
+- THEN the system MUST reject with error: "Extension period is required when extension is allowed"
+
+---
+
+### REQ-CT-15: Case Type Admin UI Tabs
+
+**Feature tier**: MVP (General, Statuses), V1 (Results, Roles, Properties, Docs)
+
+The case type edit page MUST be organized into tabs for managing the type and its sub-types. See wireframe 3.7 in DESIGN-REFERENCES.md.
+
+#### Scenario CT-15a: Tab layout
+
+- GIVEN the admin editing a case type "Omgevingsvergunning"
+- WHEN the edit page loads
+- THEN the page MUST display tabs: General, Statuses, Results, Roles, Properties, Docs
+- AND the "General" tab MUST be active by default
+- AND a "Save" button MUST be visible at the top
+
+#### Scenario CT-15b: General tab content
+
+- GIVEN the admin on the "General" tab
+- THEN the tab MUST display editable fields for: title, description, purpose, trigger, subject, processing deadline (with ISO 8601 helper), service target, extension allowed (with conditional period), suspension allowed, origin, confidentiality, publication required (with conditional text), valid from, valid until, status (published/draft)
+
+#### Scenario CT-15c: Statuses tab content
+
+- GIVEN the admin on the "Statuses" tab
+- THEN the tab MUST display an ordered list of status types with drag handles
+- AND each status type MUST show: order number, name, isFinal checkbox, notifyInitiator checkbox (with conditional text field)
+- AND an "Add" button MUST be available
+
+#### Scenario CT-15d: Results tab content (V1)
+
+- GIVEN the admin on the "Results" tab
+- THEN the tab MUST display a list of result types
+- AND each result type MUST show: name, archive action, retention period
+- AND an "Add" button MUST be available
+
+#### Scenario CT-15e: Roles tab content (V1)
+
+- GIVEN the admin on the "Roles" tab
+- THEN the tab MUST display a list of role types
+- AND each role type MUST show: name, generic role
+- AND an "Add" button MUST be available
+
+#### Scenario CT-15f: Properties tab content (V1)
+
+- GIVEN the admin on the "Properties" tab
+- THEN the tab MUST display a list of property definitions
+- AND each property MUST show: name, format, max length (if set), required at status (if set)
+- AND an "Add" button MUST be available
+
+#### Scenario CT-15g: Docs tab content (V1)
+
+- GIVEN the admin on the "Docs" tab
+- THEN the tab MUST display a list of document types
+- AND each document type MUST show: name, direction (incoming/internal/outgoing), required at status (if set)
+- AND an "Add" button MUST be available
+
+---
+
+### REQ-CT-16: Case Type Error Scenarios
+
+**Feature tier**: MVP
+
+The system MUST handle error scenarios gracefully for case type operations.
+
+#### Scenario CT-16a: Publish incomplete case type
+
+- GIVEN a case type with title and processing deadline filled but no purpose, trigger, or subject
+- WHEN the admin attempts to publish
+- THEN the system MUST reject with validation errors listing all missing required fields
+
+#### Scenario CT-16b: Add status type without order
+
+- GIVEN an admin adding a status type to a case type
+- WHEN they submit without setting the `order` field
+- THEN the system MUST either reject with "Order is required" or auto-assign the next sequential order number
+
+#### Scenario CT-16c: Duplicate status type order
+
+- GIVEN a case type with status type "Ontvangen" at order 1
+- WHEN the admin adds a new status type "Intake" also at order 1
+- THEN the system MUST reject with error: "A status type with order 1 already exists. Each status type must have a unique order."
+
+#### Scenario CT-16d: Delete case type with closed cases
+
+- GIVEN a case type "Subsidieaanvraag" with 5 closed cases and 0 active cases
+- WHEN the admin attempts to delete the case type
+- THEN the system MUST warn: "This case type is referenced by 5 closed cases. Deleting it will remove the type reference from those cases."
+- AND if confirmed, the deletion SHOULD proceed
+
+---
+
+## UI References
+
+- **Case Type List**: See wireframe 3.6 in DESIGN-REFERENCES.md (admin settings, case type cards with status/deadline/validity)
+- **Case Type Editor**: See wireframe 3.7 in DESIGN-REFERENCES.md (tabbed interface: General, Statuses, Results, Roles, Properties, Docs)
+
+## Dependencies
+
+- **Case Management spec** (`../case-management/spec.md`): Cases reference case types for behavioral controls (statuses, deadlines, confidentiality, document requirements, property requirements, result types, role types).
+- **OpenRegister**: All case type data is stored as OpenRegister objects in the `procest` register under the respective schemas (caseType, statusType, resultType, roleType, propertyDefinition, documentType, decisionType).
+- **Nextcloud Admin Settings**: Case type management is exposed via the Nextcloud admin settings panel (`OCA\Procest\Settings\AdminSettings`).
+
+### Current Implementation Status
+
+**Substantially implemented (MVP).** Core case type CRUD and status type management are functional.
+
+**Implemented:**
+- Case type CRUD via OpenRegister object store -- create, read, update, delete case types as OpenRegister objects in the `procest` register with the `caseType` schema.
+- Case type list display (`src/views/settings/CaseTypeList.vue`) with title, isDraft badge (Draft/Published), processing deadline (formatted via `durationHelpers.js`), validity period, default star icon, delete action, set-as-default action (published only).
+- Case type detail/edit with tabbed interface (`src/views/settings/CaseTypeDetail.vue`) -- General and Statuses tabs implemented. Publish/unpublish buttons with validation error display.
+- General tab (`src/views/settings/tabs/GeneralTab.vue`) with all core fields: title, description, purpose, trigger, subject, processing deadline, service target, extension allowed/period, suspension allowed, origin, confidentiality, publication required/text, valid from, valid until.
+- Statuses tab (`src/views/settings/tabs/StatusesTab.vue`) with ordered status type list, drag-and-drop reorder, inline editing, add/delete, isFinal checkbox, notifyInitiator toggle with notification text.
+- Draft/published lifecycle with publish validation (publish errors displayed in UI).
+- Default case type selection stored via `SettingsService` config key `default_case_type`.
+- Case type validation utilities (`src/utils/caseTypeValidation.js`).
+- All case type sub-entity schemas defined in `procest_register.json` and mapped in `SettingsService::SLUG_TO_CONFIG_KEY`: caseType, statusType, resultType, roleType, propertyDefinition, documentType, decisionType.
+- ZGW Catalogi API compatibility via `ZtcController` (`lib/Controller/ZtcController.php`) and `ZgwZtcRulesService` (`lib/Service/ZgwZtcRulesService.php`).
+
+**Not yet implemented (V1):**
+- REQ-CT-07: Result type management tab (schema exists, no UI).
+- REQ-CT-08: Role type management tab (schema exists, no UI).
+- REQ-CT-09: Property definition management tab (schema exists, no UI).
+- REQ-CT-10: Document type management tab (schema exists, no UI).
+- REQ-CT-11: Decision type management (schema exists, no UI).
+- REQ-CT-12: Confidentiality default enforcement on case creation (field exists, enforcement unclear).
+- Backend validation for publish prerequisites (at least one status type, at least one final status, validFrom set).
+- Delete case type blocking when active cases reference it.
+- Status type name uniqueness validation within a case type.
+- Duplicate order number detection and auto-renumbering.
+
+### Standards & References
+
+- **ZGW Catalogi API (VNG)**: Direct mapping to ZaakType, StatusType, ResultaatType, RolType, Eigenschap, InformatieObjectType, BesluitType. The `ZtcController` implements ZGW Catalogi API endpoints.
+- **CMMN 1.1**: CaseDefinition concept for case type, Milestone sequence for status types, TimerEventListener for processing deadlines.
+- **Schema.org**: `PropertyValueSpecification` for property definitions.
+- **ISO 8601**: Duration format for all time-based fields (processingDeadline, extensionPeriod, retentionPeriod, objectionPeriod, publicationPeriod).
+- **GEMMA**: ZaakType configuration follows GEMMA zaakgericht werken reference architecture.
+- **Archiefwet / Selectielijst**: Result types with archiveAction (retain/destroy) and retentionPeriod follow Dutch archiving legislation.
+
+### Specificity Assessment
+
+This is a comprehensive, highly detailed spec that is implementation-ready for both MVP and V1. It includes complete data models with field-level ZGW mappings.
+
+**Strengths:** Exhaustive data model tables with type/required/mapping columns. 16 requirements with detailed scenarios. Clear feature tier separation. Validation rules explicitly specified.
+
+**Missing/Ambiguous:**
+- No specification of how sub-entity schemas (statusType, resultType, etc.) relate to each other via OpenRegister references (reference resolution mechanics).
+- No specification of bulk operations (e.g., import multiple status types at once).
+- Case type versioning strategy not specified -- can a published type be edited in-place or must it be versioned?
+- No specification of case type search/filter in the admin list.
+
+**Open questions:**
+1. Should editing a published case type require unpublishing first, or can it be edited in-place with a warning?
+2. How should the system handle changes to a case type that affect existing cases (e.g., removing a status type that cases are currently at)?
+3. Should the `subCaseTypes` field enforce a tree structure (no cycles) and how is this validated?
diff --git a/openspec/specs/dashboard/spec.md b/openspec/specs/dashboard/spec.md
index 71313180..9fe69e0e 100644
--- a/openspec/specs/dashboard/spec.md
+++ b/openspec/specs/dashboard/spec.md
@@ -1,385 +1,385 @@
-# Dashboard Specification
-
-## Purpose
-
-The dashboard is the landing page of the Procest app. It provides an at-a-glance overview of case management activity: KPI cards with headline metrics, status and type distribution charts, an overdue cases panel, a personal workload preview, a recent activity feed, and quick actions. The dashboard aggregates data across all cases visible to the current user (respecting RBAC via OpenRegister).
-
-**Feature tiers**: MVP (KPI cards, status chart, overdue panel, my work preview, activity feed, quick actions, empty state, refresh); V1 (average processing time KPI, case type breakdown chart)
-
-## Data Sources
-
-All dashboard data comes from OpenRegister queries against the `procest` register:
-- **Cases**: schema `case` — filtered by non-final status for "open", by `deadline < today` for "overdue", by `endDate` within current month for "completed this month"
-- **Tasks**: schema `task` — filtered by `assignee == currentUser` and status `available` or `active`
-- **Activity**: Nextcloud Activity API (`OCP\Activity\IManager`) — filtered by app `procest`, last 10 events
-
-## Requirements
-
-### REQ-DASH-001: KPI Cards Row [MVP]
-
-The dashboard MUST display a row of four KPI cards at the top, providing headline metrics for the current user's case management workload.
-
-#### Scenario: Open cases count with today indicator
-- GIVEN there are 24 cases with non-final status visible to the current user
-- AND 3 of those cases were created today (startDate == today)
-- WHEN the user views the dashboard
-- THEN the system MUST display a KPI card titled "Open Cases"
-- AND the card MUST show the count "24"
-- AND the card MUST show a sub-label "+3 today"
-- AND the count MUST only include cases whose current status is not marked `isFinal`
-
-#### Scenario: Overdue cases count with action indicator
-- GIVEN there are 3 cases where `deadline < today` and status is not final
-- WHEN the user views the dashboard
-- THEN the system MUST display a KPI card titled "Overdue"
-- AND the card MUST show the count "3"
-- AND the card MUST show a warning sub-label (e.g., "action needed") to indicate urgency
-- AND clicking the card SHOULD navigate to a filtered view showing only overdue cases
-
-#### Scenario: Completed this month with average processing days
-- GIVEN 12 cases reached a final status during the current calendar month
-- AND those 12 cases had an average duration of 18 days (from `startDate` to `endDate`)
-- WHEN the user views the dashboard
-- THEN the system MUST display a KPI card titled "Completed This Month"
-- AND the card MUST show the count "12"
-- AND the card MUST show a sub-label "avg 18 days"
-
-#### Scenario: My tasks count with due-today indicator
-- GIVEN the current user has 7 tasks assigned with status `available` or `active`
-- AND 2 of those tasks have `dueDate == today`
-- WHEN the user views the dashboard
-- THEN the system MUST display a KPI card titled "My Tasks"
-- AND the card MUST show the count "7"
-- AND the card MUST show a sub-label "2 due today"
-
-#### Scenario: Zero values in KPI cards
-- GIVEN no cases exist in the system
-- WHEN the user views the dashboard
-- THEN each KPI card MUST show "0" as the count
-- AND sub-labels MUST either show "0 today" / "none" or be omitted gracefully
-- AND the cards MUST NOT show errors or broken layouts
-
-### REQ-DASH-002: Cases by Status Chart [MVP]
-
-The dashboard MUST display a horizontal bar chart showing the distribution of open cases across status types.
-
-#### Scenario: Status distribution with multiple statuses
-- GIVEN open cases distributed as: Ontvangen (8), In behandeling (6), Besluitvorming (5), Bezwaar (3), Afgehandeld today (2)
-- WHEN the user views the dashboard
-- THEN the system MUST display a horizontal bar chart titled "Cases by Status"
-- AND each bar MUST show the status name on the left and the count on the right
-- AND bars MUST be ordered by count (descending) or by status order (ascending) -- the implementation SHOULD use status order from case types for consistency
-- AND each bar's length MUST be proportional to its count relative to the maximum
-
-#### Scenario: Statuses with zero cases
-- GIVEN a status type "Bezwaar" exists but no cases currently have that status
-- WHEN the user views the status chart
-- THEN the system MAY omit statuses with zero cases from the chart
-- OR the system MAY show them with an empty bar and count "0"
-
-#### Scenario: Multiple case types with same-named statuses
-- GIVEN case type "Omgevingsvergunning" has status "In behandeling" (3 cases)
-- AND case type "Subsidieaanvraag" also has status "In behandeling" (4 cases)
-- WHEN the user views the status chart
-- THEN the system MUST aggregate cases by status name across case types
-- AND the chart MUST show "In behandeling" with count 7
-
-### REQ-DASH-003: Cases by Type Chart [V1]
-
-The dashboard SHOULD display a bar chart showing the distribution of open cases by case type.
-
-#### Scenario: Case type distribution
-- GIVEN open cases distributed as: Omgevingsvergunning (10), Subsidieaanvraag (7), Klacht (4), Melding (3)
-- WHEN the user views the dashboard
-- THEN the system MUST display a bar chart titled "Cases by Type"
-- AND each bar MUST show the case type title and the count
-- AND bars MUST be ordered by count descending
-
-#### Scenario: Case type with no open cases
-- GIVEN a published case type "Bezwaarschrift" exists but has no open cases
-- WHEN the user views the case type chart
-- THEN the system MAY omit types with zero open cases
-- OR the system MAY show them with a zero-count bar
-
-### REQ-DASH-004: Overdue Cases Panel [MVP]
-
-The dashboard MUST display a panel listing cases that have exceeded their processing deadline.
-
-#### Scenario: Overdue cases list with details
-- GIVEN the following overdue cases:
- | identifier | title | caseType | daysOverdue | assignee |
- |------------|--------------------------|----------------------|-------------|----------|
- | 2024-042 | Bouwvergunning Keizersgr | Omgevingsvergunning | 5 | Jan |
- | 2024-038 | Subsidie innovatie | Subsidieaanvraag | 2 | Maria |
-- AND case #2024-045 "Klacht behandeling" is due tomorrow (not yet overdue)
-- WHEN the user views the dashboard
-- THEN the system MUST display an "Overdue Cases" panel
-- AND the panel MUST list each overdue case showing: identifier, title, case type, days overdue, and handler name
-- AND cases MUST be sorted by days overdue descending (most overdue first)
-- AND case #2024-045 MUST NOT appear in this panel (it is not yet overdue)
-
-#### Scenario: Overdue case visual severity
-- GIVEN a case that is 5 days overdue
-- AND a case that is due tomorrow (1 day remaining)
-- WHEN the user views the overdue panel
-- THEN overdue cases MUST be displayed with a red indicator
-- AND cases due within 1 day MAY be displayed with a yellow/warning indicator in a separate "at risk" section or alongside overdue cases
-
-#### Scenario: Overdue panel with "view all" link
-- GIVEN there are 8 overdue cases
-- WHEN the user views the dashboard
-- THEN the panel MUST show all overdue cases (or a scrollable list if many)
-- AND the panel MUST include a "View all overdue" link that navigates to the case list filtered by overdue status
-
-#### Scenario: No overdue cases
-- GIVEN all open cases have `deadline >= today`
-- WHEN the user views the dashboard
-- THEN the overdue panel MUST display a positive message (e.g., "No overdue cases") or be hidden
-- AND the KPI card for overdue MUST show "0"
-
-### REQ-DASH-005: My Work Preview [MVP]
-
-The dashboard MUST display a preview of the current user's personal workload, showing the top 5 most urgent items.
-
-#### Scenario: My Work preview shows top 5 items
-- GIVEN the current user is handler on 3 cases and has 4 tasks assigned
-- WHEN the user views the dashboard
-- THEN the system MUST display a "My Work" preview panel showing the top 5 items
-- AND items MUST be sorted by priority (urgent first), then deadline/dueDate (soonest first)
-- AND each item MUST show: entity type badge ([CASE] or [TASK]), title, case type or parent case reference, deadline/dueDate, and overdue status if applicable
-
-#### Scenario: My Work preview link to full view
-- GIVEN the My Work preview is displayed
-- WHEN the user clicks "View all my work"
-- THEN the system MUST navigate to the full My Work view
-
-#### Scenario: My Work preview with no items
-- GIVEN the current user has no assigned cases or tasks
-- WHEN the user views the dashboard
-- THEN the My Work preview MUST display a message such as "No items assigned to you"
-
-### REQ-DASH-006: Recent Activity Feed [MVP]
-
-The dashboard MUST display a feed of the last 10 case management events.
-
-#### Scenario: Activity feed shows recent events
-- GIVEN the following recent events occurred:
- 1. Case #042 status changed to "In behandeling" by Jan (10 min ago)
- 2. Decision recorded on Case #036 "Vergunning verleend" by Maria (1 hour ago)
- 3. Task "Review docs" completed by Pieter (2 hours ago)
- 4. Document "Situatietekening" uploaded on Case #042 (yesterday)
-- WHEN the user views the dashboard
-- THEN the system MUST display a "Recent Activity" feed
-- AND the feed MUST show the last 10 events ordered by timestamp descending (most recent first)
-- AND each event MUST show: event description, actor name, and relative timestamp
-- AND the event types displayed MUST include: status changes, task completions, decisions, document uploads
-
-#### Scenario: Activity feed "view all" link
-- GIVEN the activity feed is displayed
-- WHEN the user clicks "View all activity"
-- THEN the system MUST navigate to a full activity view or the Nextcloud activity app filtered to Procest events
-
-#### Scenario: Activity feed with no events
-- GIVEN no Procest activity events have been recorded
-- WHEN the user views the dashboard
-- THEN the activity feed MUST display a message such as "No recent activity"
-
-### REQ-DASH-007: Quick Actions [MVP]
-
-The dashboard MUST provide quick action buttons for common case management tasks.
-
-#### Scenario: New Case button
-- GIVEN the user is on the dashboard
-- WHEN they click the "+ New Case" button
-- THEN the system MUST navigate to the case creation form
-- AND the case creation form MUST pre-select the default case type (if one is configured)
-
-#### Scenario: Quick action visibility
-- GIVEN the user is on the dashboard
-- THEN the "+ New Case" button MUST be prominently visible, placed in the top-right area of the dashboard or header bar
-
-### REQ-DASH-008: Dashboard Data Scope [MVP]
-
-The dashboard MUST aggregate data across all cases visible to the current user, respecting RBAC.
-
-#### Scenario: Dashboard respects user permissions
-- GIVEN user "Jan" has access to 20 cases via RBAC
-- AND user "Maria" has access to 15 cases (some overlapping with Jan's)
-- WHEN Jan views the dashboard
-- THEN all counts, charts, and panels MUST reflect only the 20 cases Jan can access
-- AND the system MUST NOT expose data from cases Jan cannot access
-
-#### Scenario: Admin sees all cases
-- GIVEN an admin user has access to all 50 cases in the system
-- WHEN the admin views the dashboard
-- THEN all dashboard metrics MUST reflect all 50 cases
-
-### REQ-DASH-009: Empty State [MVP]
-
-The dashboard MUST display a helpful setup message when no cases exist.
-
-#### Scenario: Fresh installation with no data
-- GIVEN Procest was just installed and no cases or case types exist
-- WHEN the user views the dashboard
-- THEN the system MUST display an empty state with:
- - A friendly message explaining what Procest does (e.g., "Welcome to Procest - Case Management for Nextcloud")
- - A call-to-action to create the first case type (for admins) or inform non-admins that the app needs configuration
- - Helpful guidance or a link to documentation
-- AND all KPI cards MUST show "0" without errors
-- AND charts MUST either be hidden or show an empty state
-
-#### Scenario: Cases exist but user has no access
-- GIVEN cases exist but the current user has no RBAC access to any of them
-- WHEN the user views the dashboard
-- THEN the dashboard MUST show zero values and empty panels
-- AND the system SHOULD display a message such as "You have no cases assigned yet"
-
-### REQ-DASH-010: Dashboard Refresh Behavior [MVP]
-
-The dashboard MUST load data on mount and support manual refresh.
-
-#### Scenario: Dashboard loads data on mount
-- GIVEN the user navigates to the dashboard
-- WHEN the dashboard component mounts
-- THEN the system MUST fetch all dashboard data (KPI metrics, chart data, overdue list, my work items, activity feed) from the API
-- AND the system SHOULD show loading skeletons or spinners while data is being fetched
-- AND the system MUST NOT display stale data from a previous session
-
-#### Scenario: Manual refresh button
-- GIVEN the user is viewing the dashboard
-- WHEN they click the refresh button
-- THEN the system MUST re-fetch all dashboard data from the API
-- AND the system SHOULD show a brief loading indicator during refresh
-- AND the data displayed MUST reflect the current state after refresh completes
-
-#### Scenario: API error during dashboard load
-- GIVEN the OpenRegister API is temporarily unavailable
-- WHEN the user navigates to the dashboard
-- THEN the system MUST display an error message (e.g., "Unable to load dashboard data")
-- AND the system MUST provide a retry option
-- AND the system MUST NOT display partial or misleading data
-
-### REQ-DASH-011: Average Processing Time KPI [V1]
-
-The dashboard SHOULD display the average processing time across completed cases.
-
-#### Scenario: Average processing time calculation
-- GIVEN 12 cases were completed this month with durations: 14, 16, 18, 20, 22, 15, 17, 19, 21, 13, 19, 22 days
-- WHEN the user views the dashboard
-- THEN the "Completed This Month" KPI card MUST show the average duration as "avg 18 days"
-- AND the average MUST be calculated as the arithmetic mean of `endDate - startDate` for all cases completed in the current calendar month
-
-#### Scenario: No completed cases this month
-- GIVEN no cases have reached a final status in the current calendar month
-- WHEN the user views the dashboard
-- THEN the "Completed This Month" KPI card MUST show "0"
-- AND the average sub-label MUST show "no data" or be omitted
-
-### REQ-DASH-012: Error Scenarios [MVP]
-
-The dashboard MUST handle error conditions gracefully.
-
-#### Scenario: Dashboard for user with no permissions
-- GIVEN a user who is authenticated but has no RBAC permissions for any cases
-- WHEN they view the dashboard
-- THEN the system MUST display zero values in all KPI cards
-- AND the system MUST NOT show error messages related to permissions
-- AND the system SHOULD display a helpful message (e.g., "No cases assigned to you yet")
-
-#### Scenario: Partial data load failure
-- GIVEN the cases API returns data but the activity API fails
-- WHEN the user views the dashboard
-- THEN the system MUST display the available data (KPI cards, charts)
-- AND the failed section (activity feed) MUST show a localized error message with a retry option
-- AND the system MUST NOT block the entire dashboard due to a single section failure
-
-#### Scenario: Dashboard with deleted case type
-- GIVEN a case references a case type that has been deleted or is no longer valid
-- WHEN the user views the dashboard
-- THEN the case MUST still be counted in KPI metrics and charts
-- AND the case type name SHOULD fall back to "Unknown type" or the stored identifier
-- AND the system MUST NOT crash or show an unhandled error
-
-### REQ-DASH-013: Dashboard Layout [MVP]
-
-The dashboard MUST follow the layout structure defined in the design reference (DESIGN-REFERENCES.md section 3.1).
-
-#### Scenario: Layout structure
-- GIVEN the user views the dashboard
-- THEN the page MUST display the following sections in order:
- 1. KPI cards row (4 cards: Open Cases, Overdue, Completed This Month, My Tasks)
- 2. Two-column layout below the KPI row:
- - Left column: Cases by Status chart, Cases by Type chart (V1), My Work preview
- - Right column: Overdue Cases panel, Recent Activity feed
-- AND the layout MUST be responsive, collapsing to a single column on narrow viewports
-
-#### Scenario: Navigation header
-- GIVEN the user is on the dashboard
-- THEN the navigation MUST include tabs or links for: Dashboard, Cases, Tasks, Decisions, My Work, and Settings (admin only)
-- AND the Dashboard tab MUST be visually marked as active
-
-## Non-Functional Requirements
-
-- **Performance**: Dashboard MUST load within 2 seconds for up to 1000 cases. Individual API calls SHOULD complete within 500ms.
-- **Accessibility**: All KPI cards MUST have appropriate ARIA labels. Charts MUST have text alternatives. The dashboard MUST meet WCAG AA standards.
-- **Localization**: All labels, messages, and date formatting MUST support English and Dutch localization.
-- **Caching**: Dashboard data MAY be cached client-side for up to 60 seconds to reduce API load, but MUST be refreshable on demand.
-
-### Current Implementation Status
-
-**Substantially implemented (MVP).** The dashboard is fully functional with KPI cards, status chart, My Work preview, and quick actions.
-
-**Implemented:**
-- Dashboard page (`src/views/Dashboard.vue`) using `CnDashboardPage` from `@conduction/nextcloud-vue` with configurable grid layout.
-- KPI cards row (4 cards): Open Cases (with count), Overdue (with warning styling when > 0), Completed This Month (count), My Tasks (count). Cards use material design icons (FolderOpen, AlertCircle, CheckCircle, ClipboardCheckOutline).
-- Cases by Status horizontal bar chart with proportional bar widths, status labels, counts, and color coding. Empty state: "No open cases".
-- My Work preview panel showing top 5 items (cases and tasks) with entity type badges ([CASE]/[TASK]), title, reference, deadline text, overdue highlighting. "View all my work" link navigates to MyWork route.
-- Quick actions: "+ New Case" button (primary) and "+ New Task" button in header area. Refresh button with spinning animation.
-- Case creation dialog (`CaseCreateDialog`) and Task creation dialog (`TaskCreateDialog`) integrated.
-- Dashboard data loading via `Promise.allSettled` for resilient parallel fetching: cases (limit 1000), caseTypes (limit 100), statusTypes (limit 500), tasks (filtered by current user, limit 100).
-- KPI computation (`src/utils/dashboardHelpers.js::computeKpis`) calculating open count, overdue count, completed this month count, task count.
-- Status aggregation (`src/utils/dashboardHelpers.js::aggregateByStatus`).
-- My Work items generation (`src/utils/dashboardHelpers.js::getMyWorkItems`).
-- Empty state with welcome message (different for admin vs regular user).
-- Error display with retry button.
-- Auto-refresh every 5 minutes (`setInterval`).
-- Loading state with `globalLoading` flag and `icon-spinning` animation.
-- Grid layout with DEFAULT_LAYOUT: 4 KPI tiles (3 cols each) in row 1, cases-by-status (6 cols) and my-work (6 cols) in row 2.
-- Navigation to case/task detail on work item click.
-- Three Nextcloud Dashboard widgets registered as PHP classes: `CasesOverviewWidget` (`lib/Dashboard/CasesOverviewWidget.php`), `MyTasksWidget` (`lib/Dashboard/MyTasksWidget.php`), `OverdueCasesWidget` (`lib/Dashboard/OverdueCasesWidget.php`) -- these are Nextcloud-native dashboard widgets separate from the in-app dashboard.
-- Widget entry points: `src/casesOverviewWidget.js`, `src/myTasksWidget.js`, `src/overdueCasesWidget.js`.
-- Widget Vue components: `src/views/widgets/CasesOverviewWidget.vue`, `src/views/widgets/MyTasksWidget.vue`, `src/views/widgets/OverdueCasesWidget.vue`.
-- Dashboard helper components: `src/views/dashboard/KpiCards.vue`, `src/views/dashboard/StatusChart.vue`, `src/views/dashboard/OverduePanel.vue`, `src/views/dashboard/MyWorkPreview.vue`, `src/views/dashboard/ActivityFeed.vue`.
-
-**Not yet implemented or partial:**
-- REQ-DASH-003: Cases by Type chart (V1).
-- REQ-DASH-004: Overdue Cases panel as separate panel in the two-column layout (overdue is shown as KPI card count but not as a detailed list panel with case details in the main dashboard -- the `OverduePanel.vue` component exists but may not be wired into the main dashboard layout).
-- REQ-DASH-006: Recent Activity feed (the `ActivityFeed.vue` component exists but is not visually present in the `Dashboard.vue` template -- no `#widget-activity` slot).
-- REQ-DASH-011: Average Processing Time KPI (V1) -- the `kpis` object has `avgDays` field but the KPI card for "Completed This Month" does not display the average.
-- KPI sub-labels (`+3 today`, `action needed`, `avg 18 days`, `2 due today`) are defined in the spec but not all are displayed in the current implementation.
-- Clickable KPI cards navigating to filtered views (e.g., clicking Overdue navigates to overdue-filtered case list).
-- RBAC scoping -- dashboard fetches all cases (limit 1000) without explicit RBAC filtering (relies on OpenRegister's built-in access control).
-- Layout responsiveness (single-column collapse on narrow viewports).
-
-### Standards & References
-
-- **WCAG AA**: KPI cards need ARIA labels, charts need text alternatives.
-- **Nextcloud Dashboard API**: Three IWidget implementations for Nextcloud-native dashboard integration.
-- **Nextcloud Activity API (`OCP\Activity\IManager`)**: Activity feed data source (mentioned in spec, `ActivityFeed.vue` component exists).
-- **GEMMA**: Dashboard follows zaakgericht werken management information patterns.
-
-### Specificity Assessment
-
-This spec is very detailed and mostly implementation-ready. The current implementation closely follows the spec.
-
-**Strengths:** Concrete KPI definitions with sub-labels, chart specifications, layout wireframe, empty state and error scenarios.
-
-**Missing/Ambiguous:**
-- The spec defines a two-column layout (left: charts + My Work; right: Overdue + Activity) but the implementation uses a grid layout with CnDashboardPage -- this architectural difference is not problematic but the Activity Feed and Overdue Panel are not yet wired in.
-- No specification of the configurable grid layout behavior (is the user able to rearrange widgets?).
-- No specification of the Nextcloud-native dashboard widgets (CasesOverviewWidget, MyTasksWidget, OverdueCasesWidget) -- these exist in the code but not in the spec.
-
-**Open questions:**
-1. Should the in-app dashboard and Nextcloud-native dashboard widgets share data/state?
-2. Should the auto-refresh interval (5 minutes) be configurable?
-3. How should the dashboard handle >1000 cases (current fetch limit)?
+# Dashboard Specification
+
+## Purpose
+
+The dashboard is the landing page of the Procest app. It provides an at-a-glance overview of case management activity: KPI cards with headline metrics, status and type distribution charts, an overdue cases panel, a personal workload preview, a recent activity feed, and quick actions. The dashboard aggregates data across all cases visible to the current user (respecting RBAC via OpenRegister).
+
+**Feature tiers**: MVP (KPI cards, status chart, overdue panel, my work preview, activity feed, quick actions, empty state, refresh); V1 (average processing time KPI, case type breakdown chart)
+
+## Data Sources
+
+All dashboard data comes from OpenRegister queries against the `procest` register:
+- **Cases**: schema `case` — filtered by non-final status for "open", by `deadline < today` for "overdue", by `endDate` within current month for "completed this month"
+- **Tasks**: schema `task` — filtered by `assignee == currentUser` and status `available` or `active`
+- **Activity**: Nextcloud Activity API (`OCP\Activity\IManager`) — filtered by app `procest`, last 10 events
+
+## Requirements
+
+### REQ-DASH-001: KPI Cards Row [MVP]
+
+The dashboard MUST display a row of four KPI cards at the top, providing headline metrics for the current user's case management workload.
+
+#### Scenario: Open cases count with today indicator
+- GIVEN there are 24 cases with non-final status visible to the current user
+- AND 3 of those cases were created today (startDate == today)
+- WHEN the user views the dashboard
+- THEN the system MUST display a KPI card titled "Open Cases"
+- AND the card MUST show the count "24"
+- AND the card MUST show a sub-label "+3 today"
+- AND the count MUST only include cases whose current status is not marked `isFinal`
+
+#### Scenario: Overdue cases count with action indicator
+- GIVEN there are 3 cases where `deadline < today` and status is not final
+- WHEN the user views the dashboard
+- THEN the system MUST display a KPI card titled "Overdue"
+- AND the card MUST show the count "3"
+- AND the card MUST show a warning sub-label (e.g., "action needed") to indicate urgency
+- AND clicking the card SHOULD navigate to a filtered view showing only overdue cases
+
+#### Scenario: Completed this month with average processing days
+- GIVEN 12 cases reached a final status during the current calendar month
+- AND those 12 cases had an average duration of 18 days (from `startDate` to `endDate`)
+- WHEN the user views the dashboard
+- THEN the system MUST display a KPI card titled "Completed This Month"
+- AND the card MUST show the count "12"
+- AND the card MUST show a sub-label "avg 18 days"
+
+#### Scenario: My tasks count with due-today indicator
+- GIVEN the current user has 7 tasks assigned with status `available` or `active`
+- AND 2 of those tasks have `dueDate == today`
+- WHEN the user views the dashboard
+- THEN the system MUST display a KPI card titled "My Tasks"
+- AND the card MUST show the count "7"
+- AND the card MUST show a sub-label "2 due today"
+
+#### Scenario: Zero values in KPI cards
+- GIVEN no cases exist in the system
+- WHEN the user views the dashboard
+- THEN each KPI card MUST show "0" as the count
+- AND sub-labels MUST either show "0 today" / "none" or be omitted gracefully
+- AND the cards MUST NOT show errors or broken layouts
+
+### REQ-DASH-002: Cases by Status Chart [MVP]
+
+The dashboard MUST display a horizontal bar chart showing the distribution of open cases across status types.
+
+#### Scenario: Status distribution with multiple statuses
+- GIVEN open cases distributed as: Ontvangen (8), In behandeling (6), Besluitvorming (5), Bezwaar (3), Afgehandeld today (2)
+- WHEN the user views the dashboard
+- THEN the system MUST display a horizontal bar chart titled "Cases by Status"
+- AND each bar MUST show the status name on the left and the count on the right
+- AND bars MUST be ordered by count (descending) or by status order (ascending) -- the implementation SHOULD use status order from case types for consistency
+- AND each bar's length MUST be proportional to its count relative to the maximum
+
+#### Scenario: Statuses with zero cases
+- GIVEN a status type "Bezwaar" exists but no cases currently have that status
+- WHEN the user views the status chart
+- THEN the system MAY omit statuses with zero cases from the chart
+- OR the system MAY show them with an empty bar and count "0"
+
+#### Scenario: Multiple case types with same-named statuses
+- GIVEN case type "Omgevingsvergunning" has status "In behandeling" (3 cases)
+- AND case type "Subsidieaanvraag" also has status "In behandeling" (4 cases)
+- WHEN the user views the status chart
+- THEN the system MUST aggregate cases by status name across case types
+- AND the chart MUST show "In behandeling" with count 7
+
+### REQ-DASH-003: Cases by Type Chart [V1]
+
+The dashboard SHOULD display a bar chart showing the distribution of open cases by case type.
+
+#### Scenario: Case type distribution
+- GIVEN open cases distributed as: Omgevingsvergunning (10), Subsidieaanvraag (7), Klacht (4), Melding (3)
+- WHEN the user views the dashboard
+- THEN the system MUST display a bar chart titled "Cases by Type"
+- AND each bar MUST show the case type title and the count
+- AND bars MUST be ordered by count descending
+
+#### Scenario: Case type with no open cases
+- GIVEN a published case type "Bezwaarschrift" exists but has no open cases
+- WHEN the user views the case type chart
+- THEN the system MAY omit types with zero open cases
+- OR the system MAY show them with a zero-count bar
+
+### REQ-DASH-004: Overdue Cases Panel [MVP]
+
+The dashboard MUST display a panel listing cases that have exceeded their processing deadline.
+
+#### Scenario: Overdue cases list with details
+- GIVEN the following overdue cases:
+ | identifier | title | caseType | daysOverdue | assignee |
+ |------------|--------------------------|----------------------|-------------|----------|
+ | 2024-042 | Bouwvergunning Keizersgr | Omgevingsvergunning | 5 | Jan |
+ | 2024-038 | Subsidie innovatie | Subsidieaanvraag | 2 | Maria |
+- AND case #2024-045 "Klacht behandeling" is due tomorrow (not yet overdue)
+- WHEN the user views the dashboard
+- THEN the system MUST display an "Overdue Cases" panel
+- AND the panel MUST list each overdue case showing: identifier, title, case type, days overdue, and handler name
+- AND cases MUST be sorted by days overdue descending (most overdue first)
+- AND case #2024-045 MUST NOT appear in this panel (it is not yet overdue)
+
+#### Scenario: Overdue case visual severity
+- GIVEN a case that is 5 days overdue
+- AND a case that is due tomorrow (1 day remaining)
+- WHEN the user views the overdue panel
+- THEN overdue cases MUST be displayed with a red indicator
+- AND cases due within 1 day MAY be displayed with a yellow/warning indicator in a separate "at risk" section or alongside overdue cases
+
+#### Scenario: Overdue panel with "view all" link
+- GIVEN there are 8 overdue cases
+- WHEN the user views the dashboard
+- THEN the panel MUST show all overdue cases (or a scrollable list if many)
+- AND the panel MUST include a "View all overdue" link that navigates to the case list filtered by overdue status
+
+#### Scenario: No overdue cases
+- GIVEN all open cases have `deadline >= today`
+- WHEN the user views the dashboard
+- THEN the overdue panel MUST display a positive message (e.g., "No overdue cases") or be hidden
+- AND the KPI card for overdue MUST show "0"
+
+### REQ-DASH-005: My Work Preview [MVP]
+
+The dashboard MUST display a preview of the current user's personal workload, showing the top 5 most urgent items.
+
+#### Scenario: My Work preview shows top 5 items
+- GIVEN the current user is handler on 3 cases and has 4 tasks assigned
+- WHEN the user views the dashboard
+- THEN the system MUST display a "My Work" preview panel showing the top 5 items
+- AND items MUST be sorted by priority (urgent first), then deadline/dueDate (soonest first)
+- AND each item MUST show: entity type badge ([CASE] or [TASK]), title, case type or parent case reference, deadline/dueDate, and overdue status if applicable
+
+#### Scenario: My Work preview link to full view
+- GIVEN the My Work preview is displayed
+- WHEN the user clicks "View all my work"
+- THEN the system MUST navigate to the full My Work view
+
+#### Scenario: My Work preview with no items
+- GIVEN the current user has no assigned cases or tasks
+- WHEN the user views the dashboard
+- THEN the My Work preview MUST display a message such as "No items assigned to you"
+
+### REQ-DASH-006: Recent Activity Feed [MVP]
+
+The dashboard MUST display a feed of the last 10 case management events.
+
+#### Scenario: Activity feed shows recent events
+- GIVEN the following recent events occurred:
+ 1. Case #042 status changed to "In behandeling" by Jan (10 min ago)
+ 2. Decision recorded on Case #036 "Vergunning verleend" by Maria (1 hour ago)
+ 3. Task "Review docs" completed by Pieter (2 hours ago)
+ 4. Document "Situatietekening" uploaded on Case #042 (yesterday)
+- WHEN the user views the dashboard
+- THEN the system MUST display a "Recent Activity" feed
+- AND the feed MUST show the last 10 events ordered by timestamp descending (most recent first)
+- AND each event MUST show: event description, actor name, and relative timestamp
+- AND the event types displayed MUST include: status changes, task completions, decisions, document uploads
+
+#### Scenario: Activity feed "view all" link
+- GIVEN the activity feed is displayed
+- WHEN the user clicks "View all activity"
+- THEN the system MUST navigate to a full activity view or the Nextcloud activity app filtered to Procest events
+
+#### Scenario: Activity feed with no events
+- GIVEN no Procest activity events have been recorded
+- WHEN the user views the dashboard
+- THEN the activity feed MUST display a message such as "No recent activity"
+
+### REQ-DASH-007: Quick Actions [MVP]
+
+The dashboard MUST provide quick action buttons for common case management tasks.
+
+#### Scenario: New Case button
+- GIVEN the user is on the dashboard
+- WHEN they click the "+ New Case" button
+- THEN the system MUST navigate to the case creation form
+- AND the case creation form MUST pre-select the default case type (if one is configured)
+
+#### Scenario: Quick action visibility
+- GIVEN the user is on the dashboard
+- THEN the "+ New Case" button MUST be prominently visible, placed in the top-right area of the dashboard or header bar
+
+### REQ-DASH-008: Dashboard Data Scope [MVP]
+
+The dashboard MUST aggregate data across all cases visible to the current user, respecting RBAC.
+
+#### Scenario: Dashboard respects user permissions
+- GIVEN user "Jan" has access to 20 cases via RBAC
+- AND user "Maria" has access to 15 cases (some overlapping with Jan's)
+- WHEN Jan views the dashboard
+- THEN all counts, charts, and panels MUST reflect only the 20 cases Jan can access
+- AND the system MUST NOT expose data from cases Jan cannot access
+
+#### Scenario: Admin sees all cases
+- GIVEN an admin user has access to all 50 cases in the system
+- WHEN the admin views the dashboard
+- THEN all dashboard metrics MUST reflect all 50 cases
+
+### REQ-DASH-009: Empty State [MVP]
+
+The dashboard MUST display a helpful setup message when no cases exist.
+
+#### Scenario: Fresh installation with no data
+- GIVEN Procest was just installed and no cases or case types exist
+- WHEN the user views the dashboard
+- THEN the system MUST display an empty state with:
+ - A friendly message explaining what Procest does (e.g., "Welcome to Procest - Case Management for Nextcloud")
+ - A call-to-action to create the first case type (for admins) or inform non-admins that the app needs configuration
+ - Helpful guidance or a link to documentation
+- AND all KPI cards MUST show "0" without errors
+- AND charts MUST either be hidden or show an empty state
+
+#### Scenario: Cases exist but user has no access
+- GIVEN cases exist but the current user has no RBAC access to any of them
+- WHEN the user views the dashboard
+- THEN the dashboard MUST show zero values and empty panels
+- AND the system SHOULD display a message such as "You have no cases assigned yet"
+
+### REQ-DASH-010: Dashboard Refresh Behavior [MVP]
+
+The dashboard MUST load data on mount and support manual refresh.
+
+#### Scenario: Dashboard loads data on mount
+- GIVEN the user navigates to the dashboard
+- WHEN the dashboard component mounts
+- THEN the system MUST fetch all dashboard data (KPI metrics, chart data, overdue list, my work items, activity feed) from the API
+- AND the system SHOULD show loading skeletons or spinners while data is being fetched
+- AND the system MUST NOT display stale data from a previous session
+
+#### Scenario: Manual refresh button
+- GIVEN the user is viewing the dashboard
+- WHEN they click the refresh button
+- THEN the system MUST re-fetch all dashboard data from the API
+- AND the system SHOULD show a brief loading indicator during refresh
+- AND the data displayed MUST reflect the current state after refresh completes
+
+#### Scenario: API error during dashboard load
+- GIVEN the OpenRegister API is temporarily unavailable
+- WHEN the user navigates to the dashboard
+- THEN the system MUST display an error message (e.g., "Unable to load dashboard data")
+- AND the system MUST provide a retry option
+- AND the system MUST NOT display partial or misleading data
+
+### REQ-DASH-011: Average Processing Time KPI [V1]
+
+The dashboard SHOULD display the average processing time across completed cases.
+
+#### Scenario: Average processing time calculation
+- GIVEN 12 cases were completed this month with durations: 14, 16, 18, 20, 22, 15, 17, 19, 21, 13, 19, 22 days
+- WHEN the user views the dashboard
+- THEN the "Completed This Month" KPI card MUST show the average duration as "avg 18 days"
+- AND the average MUST be calculated as the arithmetic mean of `endDate - startDate` for all cases completed in the current calendar month
+
+#### Scenario: No completed cases this month
+- GIVEN no cases have reached a final status in the current calendar month
+- WHEN the user views the dashboard
+- THEN the "Completed This Month" KPI card MUST show "0"
+- AND the average sub-label MUST show "no data" or be omitted
+
+### REQ-DASH-012: Error Scenarios [MVP]
+
+The dashboard MUST handle error conditions gracefully.
+
+#### Scenario: Dashboard for user with no permissions
+- GIVEN a user who is authenticated but has no RBAC permissions for any cases
+- WHEN they view the dashboard
+- THEN the system MUST display zero values in all KPI cards
+- AND the system MUST NOT show error messages related to permissions
+- AND the system SHOULD display a helpful message (e.g., "No cases assigned to you yet")
+
+#### Scenario: Partial data load failure
+- GIVEN the cases API returns data but the activity API fails
+- WHEN the user views the dashboard
+- THEN the system MUST display the available data (KPI cards, charts)
+- AND the failed section (activity feed) MUST show a localized error message with a retry option
+- AND the system MUST NOT block the entire dashboard due to a single section failure
+
+#### Scenario: Dashboard with deleted case type
+- GIVEN a case references a case type that has been deleted or is no longer valid
+- WHEN the user views the dashboard
+- THEN the case MUST still be counted in KPI metrics and charts
+- AND the case type name SHOULD fall back to "Unknown type" or the stored identifier
+- AND the system MUST NOT crash or show an unhandled error
+
+### REQ-DASH-013: Dashboard Layout [MVP]
+
+The dashboard MUST follow the layout structure defined in the design reference (DESIGN-REFERENCES.md section 3.1).
+
+#### Scenario: Layout structure
+- GIVEN the user views the dashboard
+- THEN the page MUST display the following sections in order:
+ 1. KPI cards row (4 cards: Open Cases, Overdue, Completed This Month, My Tasks)
+ 2. Two-column layout below the KPI row:
+ - Left column: Cases by Status chart, Cases by Type chart (V1), My Work preview
+ - Right column: Overdue Cases panel, Recent Activity feed
+- AND the layout MUST be responsive, collapsing to a single column on narrow viewports
+
+#### Scenario: Navigation header
+- GIVEN the user is on the dashboard
+- THEN the navigation MUST include tabs or links for: Dashboard, Cases, Tasks, Decisions, My Work, and Settings (admin only)
+- AND the Dashboard tab MUST be visually marked as active
+
+## Non-Functional Requirements
+
+- **Performance**: Dashboard MUST load within 2 seconds for up to 1000 cases. Individual API calls SHOULD complete within 500ms.
+- **Accessibility**: All KPI cards MUST have appropriate ARIA labels. Charts MUST have text alternatives. The dashboard MUST meet WCAG AA standards.
+- **Localization**: All labels, messages, and date formatting MUST support English and Dutch localization.
+- **Caching**: Dashboard data MAY be cached client-side for up to 60 seconds to reduce API load, but MUST be refreshable on demand.
+
+### Current Implementation Status
+
+**Substantially implemented (MVP).** The dashboard is fully functional with KPI cards, status chart, My Work preview, and quick actions.
+
+**Implemented:**
+- Dashboard page (`src/views/Dashboard.vue`) using `CnDashboardPage` from `@conduction/nextcloud-vue` with configurable grid layout.
+- KPI cards row (4 cards): Open Cases (with count), Overdue (with warning styling when > 0), Completed This Month (count), My Tasks (count). Cards use material design icons (FolderOpen, AlertCircle, CheckCircle, ClipboardCheckOutline).
+- Cases by Status horizontal bar chart with proportional bar widths, status labels, counts, and color coding. Empty state: "No open cases".
+- My Work preview panel showing top 5 items (cases and tasks) with entity type badges ([CASE]/[TASK]), title, reference, deadline text, overdue highlighting. "View all my work" link navigates to MyWork route.
+- Quick actions: "+ New Case" button (primary) and "+ New Task" button in header area. Refresh button with spinning animation.
+- Case creation dialog (`CaseCreateDialog`) and Task creation dialog (`TaskCreateDialog`) integrated.
+- Dashboard data loading via `Promise.allSettled` for resilient parallel fetching: cases (limit 1000), caseTypes (limit 100), statusTypes (limit 500), tasks (filtered by current user, limit 100).
+- KPI computation (`src/utils/dashboardHelpers.js::computeKpis`) calculating open count, overdue count, completed this month count, task count.
+- Status aggregation (`src/utils/dashboardHelpers.js::aggregateByStatus`).
+- My Work items generation (`src/utils/dashboardHelpers.js::getMyWorkItems`).
+- Empty state with welcome message (different for admin vs regular user).
+- Error display with retry button.
+- Auto-refresh every 5 minutes (`setInterval`).
+- Loading state with `globalLoading` flag and `icon-spinning` animation.
+- Grid layout with DEFAULT_LAYOUT: 4 KPI tiles (3 cols each) in row 1, cases-by-status (6 cols) and my-work (6 cols) in row 2.
+- Navigation to case/task detail on work item click.
+- Three Nextcloud Dashboard widgets registered as PHP classes: `CasesOverviewWidget` (`lib/Dashboard/CasesOverviewWidget.php`), `MyTasksWidget` (`lib/Dashboard/MyTasksWidget.php`), `OverdueCasesWidget` (`lib/Dashboard/OverdueCasesWidget.php`) -- these are Nextcloud-native dashboard widgets separate from the in-app dashboard.
+- Widget entry points: `src/casesOverviewWidget.js`, `src/myTasksWidget.js`, `src/overdueCasesWidget.js`.
+- Widget Vue components: `src/views/widgets/CasesOverviewWidget.vue`, `src/views/widgets/MyTasksWidget.vue`, `src/views/widgets/OverdueCasesWidget.vue`.
+- Dashboard helper components: `src/views/dashboard/KpiCards.vue`, `src/views/dashboard/StatusChart.vue`, `src/views/dashboard/OverduePanel.vue`, `src/views/dashboard/MyWorkPreview.vue`, `src/views/dashboard/ActivityFeed.vue`.
+
+**Not yet implemented or partial:**
+- REQ-DASH-003: Cases by Type chart (V1).
+- REQ-DASH-004: Overdue Cases panel as separate panel in the two-column layout (overdue is shown as KPI card count but not as a detailed list panel with case details in the main dashboard -- the `OverduePanel.vue` component exists but may not be wired into the main dashboard layout).
+- REQ-DASH-006: Recent Activity feed (the `ActivityFeed.vue` component exists but is not visually present in the `Dashboard.vue` template -- no `#widget-activity` slot).
+- REQ-DASH-011: Average Processing Time KPI (V1) -- the `kpis` object has `avgDays` field but the KPI card for "Completed This Month" does not display the average.
+- KPI sub-labels (`+3 today`, `action needed`, `avg 18 days`, `2 due today`) are defined in the spec but not all are displayed in the current implementation.
+- Clickable KPI cards navigating to filtered views (e.g., clicking Overdue navigates to overdue-filtered case list).
+- RBAC scoping -- dashboard fetches all cases (limit 1000) without explicit RBAC filtering (relies on OpenRegister's built-in access control).
+- Layout responsiveness (single-column collapse on narrow viewports).
+
+### Standards & References
+
+- **WCAG AA**: KPI cards need ARIA labels, charts need text alternatives.
+- **Nextcloud Dashboard API**: Three IWidget implementations for Nextcloud-native dashboard integration.
+- **Nextcloud Activity API (`OCP\Activity\IManager`)**: Activity feed data source (mentioned in spec, `ActivityFeed.vue` component exists).
+- **GEMMA**: Dashboard follows zaakgericht werken management information patterns.
+
+### Specificity Assessment
+
+This spec is very detailed and mostly implementation-ready. The current implementation closely follows the spec.
+
+**Strengths:** Concrete KPI definitions with sub-labels, chart specifications, layout wireframe, empty state and error scenarios.
+
+**Missing/Ambiguous:**
+- The spec defines a two-column layout (left: charts + My Work; right: Overdue + Activity) but the implementation uses a grid layout with CnDashboardPage -- this architectural difference is not problematic but the Activity Feed and Overdue Panel are not yet wired in.
+- No specification of the configurable grid layout behavior (is the user able to rearrange widgets?).
+- No specification of the Nextcloud-native dashboard widgets (CasesOverviewWidget, MyTasksWidget, OverdueCasesWidget) -- these exist in the code but not in the spec.
+
+**Open questions:**
+1. Should the in-app dashboard and Nextcloud-native dashboard widgets share data/state?
+2. Should the auto-refresh interval (5 minutes) be configurable?
+3. How should the dashboard handle >1000 cases (current fetch limit)?
diff --git a/openspec/specs/my-work/spec.md b/openspec/specs/my-work/spec.md
index ec1bed85..153bfbaf 100644
--- a/openspec/specs/my-work/spec.md
+++ b/openspec/specs/my-work/spec.md
@@ -1,357 +1,357 @@
-# My Work (Werkvoorraad) Specification
-
-## Purpose
-
-My Work is the personal productivity hub for case handlers. It aggregates all work items assigned to the current user -- cases where they are the handler and tasks assigned to them -- into a single prioritized view. Items are grouped by urgency (Overdue, Due This Week, Upcoming, No Deadline) and sorted by priority then deadline within each group. This view answers the daily question: "What do I need to work on next?"
-
-**Feature tiers**: MVP (cases + tasks, filter tabs, sorting, grouping, overdue highlighting, item navigation, empty state); V1 (cross-app workload with Pipelinq, show completed toggle)
-
-## Data Sources
-
-My Work queries two OpenRegister schemas in the `procest` register:
-- **Cases**: schema `case` with filter `assignee == currentUser` AND status NOT `isFinal`
-- **Tasks**: schema `task` with filter `assignee == currentUser` AND status IN (`available`, `active`)
-
-For V1 cross-app workload:
-- **Pipelinq leads**: filter `assignedTo == currentUser` with non-closed stage
-- **Pipelinq requests**: filter `assignedTo == currentUser` with non-final status
-
-## Requirements
-
-### REQ-MYWORK-001: Personal Workload View [MVP]
-
-The system MUST provide a "My Work" view showing all cases and tasks assigned to the current user in a unified list.
-
-#### Scenario: View assigned cases and tasks
-- GIVEN user "Jan" is handler on 3 cases:
- | identifier | title | caseType | status | deadline | priority |
- |------------|---------------------------|---------------------|------------------|------------|----------|
- | 2024-042 | Bouwvergunning Keizersgr | Omgevingsvergunning | In behandeling | 2026-02-20 | high |
- | 2024-038 | Subsidie innovatie | Subsidieaanvraag | Besluitvorming | 2026-02-23 | normal |
- | 2024-048 | Subsidie verduurzaming | Subsidieaanvraag | In behandeling | 2026-02-28 | normal |
-- AND Jan has 4 tasks assigned:
- | title | case | dueDate | priority | status |
- |--------------------|-----------:|------------|----------|-----------|
- | Review documents | 2024-042 | 2026-02-26 | high | active |
- | Collect information| 2024-048 | 2026-03-01 | normal | available |
- | Contact applicant | 2024-050 | 2026-03-03 | normal | available |
- | Prepare decision | 2024-042 | 2026-03-05 | normal | available |
-- WHEN Jan navigates to "My Work"
-- THEN the system MUST display all 7 items in a unified list
-- AND the total item count "7 items total" MUST be shown
-
-#### Scenario: Case item display
-- GIVEN a case item in the My Work list
-- THEN the item MUST display:
- - A "[CASE]" badge to identify the entity type
- - The case identifier (e.g., "#2024-042")
- - The case title (e.g., "Bouwvergunning Keizersgracht")
- - The case type name (e.g., "Omgevingsvergunning")
- - The current status name (e.g., "In behandeling")
- - The deadline date
- - Days overdue (red, e.g., "5 days overdue") or days remaining (e.g., "3 days")
- - Priority indicator (if not normal)
-
-#### Scenario: Task item display
-- GIVEN a task item in the My Work list
-- THEN the item MUST display:
- - A "[TASK]" badge to identify the entity type
- - The task title (e.g., "Review documents")
- - The parent case reference as a clickable link (e.g., "Case: #2024-042 Bouwvergunning Keizersgracht")
- - The due date
- - Days overdue or days remaining
- - Priority indicator (if not normal)
-
-### REQ-MYWORK-002: Filter Tabs [MVP]
-
-The system MUST provide filter tabs to narrow the My Work list by entity type.
-
-#### Scenario: Filter tab layout
-- GIVEN the user has 3 cases and 4 tasks
-- WHEN they view My Work
-- THEN the system MUST display three filter tabs: "All", "Cases", "Tasks"
-- AND each tab MUST show the item count in parentheses: "All (7)", "Cases (3)", "Tasks (4)"
-- AND the "All" tab MUST be selected by default
-
-#### Scenario: Filter by Cases only
-- GIVEN the user has 3 cases and 4 tasks
-- WHEN they click the "Cases" tab
-- THEN only the 3 case items MUST be shown
-- AND the task items MUST be hidden
-- AND the grouped sections MUST update to reflect only case items
-
-#### Scenario: Filter by Tasks only
-- GIVEN the user has 3 cases and 4 tasks
-- WHEN they click the "Tasks" tab
-- THEN only the 4 task items MUST be shown
-- AND the case items MUST be hidden
-
-#### Scenario: Filter tab with zero items
-- GIVEN the user has 3 cases but 0 tasks
-- WHEN they view My Work
-- THEN the "Tasks" tab MUST show "Tasks (0)"
-- AND clicking the "Tasks" tab MUST show an empty state message
-
-### REQ-MYWORK-003: Sorting [MVP]
-
-The system MUST sort My Work items by priority first, then by deadline/dueDate.
-
-#### Scenario: Default sort order
-- GIVEN items with mixed priorities and deadlines:
- | item | priority | deadline/dueDate |
- |---------------------------|----------|------------------|
- | Case #042 Bouwvergunning | high | 2026-02-20 |
- | Task: Review documents | high | 2026-02-26 |
- | Case #038 Subsidie innov. | normal | 2026-02-23 |
- | Case #048 Subsidie verduu.| normal | 2026-02-28 |
- | Task: Collect information | normal | 2026-03-01 |
- | Task: Contact applicant | normal | 2026-03-03 |
- | Task: Prepare decision | normal | 2026-03-05 |
-- WHEN the user views My Work without changing sort
-- THEN items MUST be sorted by priority (urgent > high > normal > low), then by deadline ascending (soonest first)
-- AND the resulting order MUST be as listed above (high-priority items first, then normal sorted by date)
-
-#### Scenario: Items without deadline appear last within priority group
-- GIVEN two normal-priority items:
- - Case #048 with deadline 2026-02-28
- - Case #055 with no deadline set
-- WHEN the user views My Work
-- THEN Case #048 MUST appear before Case #055
-- AND Case #055 MUST appear in the "No Deadline" grouped section
-
-### REQ-MYWORK-004: Grouped Sections [MVP]
-
-The system MUST group My Work items into urgency-based sections to provide visual structure.
-
-#### Scenario: Overdue section (red)
-- GIVEN cases/tasks where deadline/dueDate is before today
-- WHEN the user views My Work
-- THEN those items MUST appear in a section titled "OVERDUE"
-- AND the section MUST have a red visual treatment (red background tint, red section header, or red left border)
-- AND each item within MUST show "X days overdue" in red text
-- AND the section MUST appear first (above all other sections)
-
-#### Scenario: Due This Week section
-- GIVEN today is Monday, 2026-02-23
-- AND there are items with deadline/dueDate between today and Sunday 2026-03-01 (inclusive)
-- WHEN the user views My Work
-- THEN those items MUST appear in a section titled "DUE THIS WEEK"
-- AND each item MUST show the number of days remaining (e.g., "1 day", "3 days")
-
-#### Scenario: Upcoming section
-- GIVEN items with deadline/dueDate after the current week
-- WHEN the user views My Work
-- THEN those items MUST appear in a section titled "UPCOMING"
-- AND each item MUST show the due date
-
-#### Scenario: No Deadline section
-- GIVEN items with no deadline or dueDate set
-- WHEN the user views My Work
-- THEN those items MUST appear in a section titled "NO DEADLINE"
-- AND this section MUST appear last (below all dated sections)
-
-#### Scenario: Item count per section
-- GIVEN 2 overdue items, 3 due this week, and 2 upcoming
-- WHEN the user views My Work
-- THEN each section header SHOULD display the count of items in that section (e.g., "OVERDUE (2)")
-
-#### Scenario: Empty sections are hidden
-- GIVEN no items are overdue
-- WHEN the user views My Work
-- THEN the "OVERDUE" section MUST NOT be displayed
-- AND the first visible section MUST be whichever section has items
-
-### REQ-MYWORK-005: Overdue Highlighting [MVP]
-
-The system MUST visually distinguish overdue items from on-time items.
-
-#### Scenario: Overdue case highlighting
-- GIVEN case #2024-042 has deadline 2026-02-20 and today is 2026-02-25
-- AND the case status is "In behandeling" (not final)
-- WHEN the user views My Work
-- THEN the case MUST be displayed with a red visual indicator (red background, red badge, or red left border)
-- AND the text "5 days overdue" MUST be displayed in red
-- AND the deadline date MUST be shown
-
-#### Scenario: Overdue task highlighting
-- GIVEN a task "Review documents" has dueDate 2026-02-24 and today is 2026-02-25
-- AND the task status is "active"
-- WHEN the user views My Work
-- THEN the task MUST be displayed with a red visual indicator
-- AND the text "1 day overdue" MUST be displayed in red
-
-#### Scenario: Non-overdue item (normal display)
-- GIVEN a case with deadline 2026-02-28 and today is 2026-02-25
-- WHEN the user views My Work
-- THEN the case MUST be displayed without red highlighting
-- AND the text "3 days" MUST be displayed in a neutral color
-
-### REQ-MYWORK-006: Default Filter -- Non-Final Items Only [MVP]
-
-By default, My Work MUST only show open (non-completed) items.
-
-#### Scenario: Only non-final cases shown by default
-- GIVEN the user is handler on 5 cases: 3 with non-final status, 2 with final status ("Afgehandeld")
-- WHEN they view My Work
-- THEN only the 3 non-final cases MUST be shown
-- AND the 2 completed cases MUST be hidden
-
-#### Scenario: Only non-completed tasks shown by default
-- GIVEN the user has 6 tasks: 4 with status `available` or `active`, 2 with status `completed`
-- WHEN they view My Work
-- THEN only the 4 open tasks MUST be shown
-
-#### Scenario: Toggle to show completed items
-- GIVEN the user is viewing My Work with 3 open items
-- AND they have 2 completed items hidden
-- WHEN they toggle the "Show completed" control
-- THEN all 5 items MUST be displayed
-- AND completed items MUST be visually distinguished (e.g., strikethrough, muted colors, or a "Completed" badge)
-- AND completed items SHOULD appear at the bottom of the list, below all open items
-
-### REQ-MYWORK-007: Item Navigation [MVP]
-
-Clicking an item in My Work MUST navigate to the appropriate detail view.
-
-#### Scenario: Click case item to navigate
-- GIVEN case #2024-042 appears in My Work
-- WHEN the user clicks on the case item
-- THEN the system MUST navigate to the case detail view for case #2024-042
-
-#### Scenario: Click task item to navigate
-- GIVEN a task "Review documents" for case #2024-042 appears in My Work
-- WHEN the user clicks on the task item
-- THEN the system MUST navigate to the task detail or the parent case detail view with the task highlighted
-
-#### Scenario: Click parent case reference on task
-- GIVEN a task item shows "Case: #2024-042 Bouwvergunning Keizersgracht" as a clickable reference
-- WHEN the user clicks on the parent case reference (not the task itself)
-- THEN the system MUST navigate to the case detail view for case #2024-042
-
-### REQ-MYWORK-008: Cross-App Workload [V1]
-
-The My Work view SHOULD include items from Pipelinq (leads and requests) assigned to the current user.
-
-#### Scenario: Include Pipelinq leads and requests
-- GIVEN the current user has:
- - 2 cases in Procest
- - 3 tasks in Procest
- - 1 lead in Pipelinq (assigned to them)
- - 2 requests in Pipelinq (assigned to them)
-- WHEN they view My Work with cross-app integration enabled
-- THEN all 8 items MUST appear in a unified list
-- AND each item MUST be labeled with its source: [CASE], [TASK], [LEAD], [REQUEST]
-- AND Pipelinq items MUST follow the same sorting and grouping rules as Procest items
-
-#### Scenario: Cross-app filter tabs
-- GIVEN cross-app workload is enabled and the user has items from both Procest and Pipelinq
-- WHEN they view My Work
-- THEN the filter tabs MUST include: "All", "Cases", "Tasks", "Leads", "Requests"
-- AND each tab MUST show its item count
-
-#### Scenario: Pipelinq app not installed
-- GIVEN the Pipelinq app is not installed on this Nextcloud instance
-- WHEN the user views My Work
-- THEN the system MUST show only Procest items (cases and tasks)
-- AND no Pipelinq-related filter tabs MUST be shown
-- AND no error messages MUST appear about Pipelinq being unavailable
-
-### REQ-MYWORK-009: Empty State [MVP]
-
-The system MUST display a helpful message when the user has no assigned items.
-
-#### Scenario: No assigned items
-- GIVEN the current user has no cases where they are handler and no tasks assigned to them
-- WHEN they navigate to "My Work"
-- THEN the system MUST display an empty state with:
- - A friendly message (e.g., "You have no cases or tasks assigned to you")
- - Guidance on how items appear here (e.g., "Cases and tasks assigned to you will appear in this view")
-- AND the filter tabs MUST all show "(0)"
-
-#### Scenario: All items completed (show-completed toggle off)
-- GIVEN the user has 5 items but all have reached final/completed status
-- AND the "Show completed" toggle is off
-- WHEN they view My Work
-- THEN the system MUST display a contextual empty state (e.g., "All caught up! No open items.")
-- AND the system SHOULD indicate that completed items can be shown via the toggle
-
-### REQ-MYWORK-010: Concurrent State Changes [MVP]
-
-The system MUST handle cases where items change status while the user is viewing My Work.
-
-#### Scenario: Case closed while viewing My Work
-- GIVEN the user is viewing My Work with case #2024-042 listed
-- AND another user changes case #2024-042 to a final status
-- WHEN the user refreshes My Work (or the data auto-refreshes)
-- THEN case #2024-042 MUST no longer appear in the list (unless "Show completed" is on)
-- AND the item counts MUST update accordingly
-
-#### Scenario: Case deleted while in My Work list
-- GIVEN the user is viewing My Work with case #2024-042 listed
-- AND case #2024-042 is deleted by an admin
-- WHEN the user clicks on case #2024-042
-- THEN the system MUST display a "Case not found" message or redirect to the case list
-- AND the system MUST NOT show an unhandled error
-- AND on next refresh, the deleted case MUST no longer appear in My Work
-
-#### Scenario: Task reassigned away from user
-- GIVEN the user is viewing My Work with task "Review documents" listed
-- AND the task is reassigned to a different user
-- WHEN the user refreshes My Work
-- THEN the task MUST no longer appear in the list
-- AND the item counts MUST update accordingly
-
-## Non-Functional Requirements
-
-- **Performance**: My Work MUST load within 1 second for users with up to 100 assigned items. The two queries (cases + tasks) SHOULD be executed in parallel.
-- **Accessibility**: Each item MUST be keyboard-navigable. Screen readers MUST announce the entity type, title, urgency status, and deadline. Overdue visual indicators MUST NOT rely solely on color (use text "X days overdue" as well). All content MUST meet WCAG AA standards.
-- **Localization**: All labels, section titles, date formatting, and relative time expressions (e.g., "5 days overdue", "3 days") MUST support English and Dutch localization.
-- **Responsiveness**: The My Work view MUST adapt to narrow viewports, maintaining readability of all item fields on mobile screens.
-
----
-
-### Current Implementation Status
-
-**Substantially implemented (MVP).** The My Work view exists and covers most MVP requirements.
-
-**Implemented (with file paths):**
-- **My Work view**: `src/views/MyWork.vue` -- full implementation with filter tabs (All/Cases/Tasks), grouped sections (Overdue, Due this week, Upcoming, No deadline), overdue highlighting (red left border, red text), item counts per section, empty states, and show-completed toggle.
-- **Navigation entry**: `src/navigation/MainMenu.vue` -- "My Work" menu item with `AccountCheck` icon linked to route `/my-work`.
-- **Router**: `src/router/index.js` -- route `{ path: '/my-work', name: 'MyWork', component: MyWork }`.
-- **Dashboard helpers**: `src/utils/dashboardHelpers.js` -- `getGroupedMyWorkItems()` function that groups items into overdue/dueThisWeek/upcoming/noDeadline sections with sorting by priority then deadline.
-- **Task API service**: `src/services/taskApi.js` -- `fetchTasksForCases()` fetches CalDAV tasks linked to cases.
-- **Object store**: `src/store/modules/object.js` -- uses `createObjectStore('object')` from `@conduction/nextcloud-vue` for CRUD operations against OpenRegister.
-- **Filter tabs**: Three tabs (All, Cases, Tasks) with item counts, active tab highlighting (REQ-MYWORK-002).
-- **Grouped sections**: Four sections with section counts and empty section hiding (REQ-MYWORK-004).
-- **Overdue highlighting**: Red border on overdue rows, red overdue text, priority indicators (REQ-MYWORK-005).
-- **Default non-final filter**: Active cases only by default; show-completed toggle fetches final-status cases (REQ-MYWORK-006).
-- **Item navigation**: Click navigates to CaseDetail; task clicks navigate to the linked case (REQ-MYWORK-007).
-- **Empty state**: NcEmptyContent with "No items assigned to you" and "All caught up!" messages (REQ-MYWORK-009).
-- **Dashboard widgets**: `lib/Dashboard/MyTasksWidget.php`, `lib/Dashboard/OverdueCasesWidget.php`, `lib/Dashboard/CasesOverviewWidget.php` -- Nextcloud dashboard widgets with corresponding Vue components in `src/views/widgets/`.
-- **Dashboard preview**: `src/views/dashboard/MyWorkPreview.vue` and `src/views/dashboard/OverduePanel.vue` -- summary panels on the main dashboard.
-
-**Not yet implemented:**
-- **REQ-MYWORK-008: Cross-App Workload (V1)**: No Pipelinq integration. The view only shows Procest cases and tasks. No [LEAD] or [REQUEST] badges. No conditional filter tabs for Pipelinq items.
-- **Task data source**: Currently uses CalDAV tasks via `fetchTasksForCases()` rather than OpenRegister `task` schema objects as specified. The spec envisions tasks as OpenRegister objects, but the current implementation fetches from Nextcloud's CalDAV task backend.
-- **Case type name resolution**: The case type name is not displayed on case items in the My Work list (spec requires it in REQ-MYWORK-001).
-- **Keyboard navigation**: No explicit keyboard navigation support (tab through items, enter to open).
-- **Screen reader announcements**: No ARIA attributes for entity type, urgency status, or deadline.
-- **Localization**: Translation functions `t()` are used throughout, but Dutch translations may be incomplete in l10n files.
-- **Responsiveness**: No explicit responsive/mobile styling in the component.
-
-### Standards & References
-
-- **CMMN 1.1**: Task statuses (available, active, completed, terminated, disabled) follow the CMMN PlanItem lifecycle, as implemented in `src/utils/taskLifecycle.js`.
-- **Schema.org**: Cases map to `schema:Project`, tasks to `schema:Action` (defined in `procest_register.json`).
-- **ZGW APIs (VNG Realisatie)**: Cases correspond to `Zaak`, tasks to internal work items. The ZRC controller (`lib/Controller/ZrcController.php`) provides ZGW-compliant endpoints.
-- **WCAG 2.1 AA**: Spec requires color-independent overdue indicators (text "X days overdue" alongside color). Currently implemented with both red styling and text labels.
-- **NL Design System**: CSS variables used for colors (e.g., `--color-error`, `--color-primary-element-light`) which support theming.
-
-### Specificity Assessment
-
-- **Mostly implementable as-is.** The MVP requirements are specific enough and are largely already implemented.
-- **Ambiguity in task data source**: The spec assumes tasks are OpenRegister objects in the `task` schema, but the current implementation uses CalDAV tasks. This architectural mismatch needs resolution -- should the spec be updated to reflect CalDAV, or should the implementation migrate to OpenRegister tasks?
-- **Missing detail on cross-app workload**: The V1 cross-app requirement lacks detail on how Pipelinq data is discovered (does Procest query the Pipelinq register directly? Does Pipelinq expose an API?).
-- **Open questions:**
- - Should the My Work view support auto-refresh (polling or websocket) for concurrent state changes (REQ-MYWORK-010)?
- - What is the performance target for users with 100+ items? The current implementation fetches up to 100 cases in a single call.
+# My Work (Werkvoorraad) Specification
+
+## Purpose
+
+My Work is the personal productivity hub for case handlers. It aggregates all work items assigned to the current user -- cases where they are the handler and tasks assigned to them -- into a single prioritized view. Items are grouped by urgency (Overdue, Due This Week, Upcoming, No Deadline) and sorted by priority then deadline within each group. This view answers the daily question: "What do I need to work on next?"
+
+**Feature tiers**: MVP (cases + tasks, filter tabs, sorting, grouping, overdue highlighting, item navigation, empty state); V1 (cross-app workload with Pipelinq, show completed toggle)
+
+## Data Sources
+
+My Work queries two OpenRegister schemas in the `procest` register:
+- **Cases**: schema `case` with filter `assignee == currentUser` AND status NOT `isFinal`
+- **Tasks**: schema `task` with filter `assignee == currentUser` AND status IN (`available`, `active`)
+
+For V1 cross-app workload:
+- **Pipelinq leads**: filter `assignedTo == currentUser` with non-closed stage
+- **Pipelinq requests**: filter `assignedTo == currentUser` with non-final status
+
+## Requirements
+
+### REQ-MYWORK-001: Personal Workload View [MVP]
+
+The system MUST provide a "My Work" view showing all cases and tasks assigned to the current user in a unified list.
+
+#### Scenario: View assigned cases and tasks
+- GIVEN user "Jan" is handler on 3 cases:
+ | identifier | title | caseType | status | deadline | priority |
+ |------------|---------------------------|---------------------|------------------|------------|----------|
+ | 2024-042 | Bouwvergunning Keizersgr | Omgevingsvergunning | In behandeling | 2026-02-20 | high |
+ | 2024-038 | Subsidie innovatie | Subsidieaanvraag | Besluitvorming | 2026-02-23 | normal |
+ | 2024-048 | Subsidie verduurzaming | Subsidieaanvraag | In behandeling | 2026-02-28 | normal |
+- AND Jan has 4 tasks assigned:
+ | title | case | dueDate | priority | status |
+ |--------------------|-----------:|------------|----------|-----------|
+ | Review documents | 2024-042 | 2026-02-26 | high | active |
+ | Collect information| 2024-048 | 2026-03-01 | normal | available |
+ | Contact applicant | 2024-050 | 2026-03-03 | normal | available |
+ | Prepare decision | 2024-042 | 2026-03-05 | normal | available |
+- WHEN Jan navigates to "My Work"
+- THEN the system MUST display all 7 items in a unified list
+- AND the total item count "7 items total" MUST be shown
+
+#### Scenario: Case item display
+- GIVEN a case item in the My Work list
+- THEN the item MUST display:
+ - A "[CASE]" badge to identify the entity type
+ - The case identifier (e.g., "#2024-042")
+ - The case title (e.g., "Bouwvergunning Keizersgracht")
+ - The case type name (e.g., "Omgevingsvergunning")
+ - The current status name (e.g., "In behandeling")
+ - The deadline date
+ - Days overdue (red, e.g., "5 days overdue") or days remaining (e.g., "3 days")
+ - Priority indicator (if not normal)
+
+#### Scenario: Task item display
+- GIVEN a task item in the My Work list
+- THEN the item MUST display:
+ - A "[TASK]" badge to identify the entity type
+ - The task title (e.g., "Review documents")
+ - The parent case reference as a clickable link (e.g., "Case: #2024-042 Bouwvergunning Keizersgracht")
+ - The due date
+ - Days overdue or days remaining
+ - Priority indicator (if not normal)
+
+### REQ-MYWORK-002: Filter Tabs [MVP]
+
+The system MUST provide filter tabs to narrow the My Work list by entity type.
+
+#### Scenario: Filter tab layout
+- GIVEN the user has 3 cases and 4 tasks
+- WHEN they view My Work
+- THEN the system MUST display three filter tabs: "All", "Cases", "Tasks"
+- AND each tab MUST show the item count in parentheses: "All (7)", "Cases (3)", "Tasks (4)"
+- AND the "All" tab MUST be selected by default
+
+#### Scenario: Filter by Cases only
+- GIVEN the user has 3 cases and 4 tasks
+- WHEN they click the "Cases" tab
+- THEN only the 3 case items MUST be shown
+- AND the task items MUST be hidden
+- AND the grouped sections MUST update to reflect only case items
+
+#### Scenario: Filter by Tasks only
+- GIVEN the user has 3 cases and 4 tasks
+- WHEN they click the "Tasks" tab
+- THEN only the 4 task items MUST be shown
+- AND the case items MUST be hidden
+
+#### Scenario: Filter tab with zero items
+- GIVEN the user has 3 cases but 0 tasks
+- WHEN they view My Work
+- THEN the "Tasks" tab MUST show "Tasks (0)"
+- AND clicking the "Tasks" tab MUST show an empty state message
+
+### REQ-MYWORK-003: Sorting [MVP]
+
+The system MUST sort My Work items by priority first, then by deadline/dueDate.
+
+#### Scenario: Default sort order
+- GIVEN items with mixed priorities and deadlines:
+ | item | priority | deadline/dueDate |
+ |---------------------------|----------|------------------|
+ | Case #042 Bouwvergunning | high | 2026-02-20 |
+ | Task: Review documents | high | 2026-02-26 |
+ | Case #038 Subsidie innov. | normal | 2026-02-23 |
+ | Case #048 Subsidie verduu.| normal | 2026-02-28 |
+ | Task: Collect information | normal | 2026-03-01 |
+ | Task: Contact applicant | normal | 2026-03-03 |
+ | Task: Prepare decision | normal | 2026-03-05 |
+- WHEN the user views My Work without changing sort
+- THEN items MUST be sorted by priority (urgent > high > normal > low), then by deadline ascending (soonest first)
+- AND the resulting order MUST be as listed above (high-priority items first, then normal sorted by date)
+
+#### Scenario: Items without deadline appear last within priority group
+- GIVEN two normal-priority items:
+ - Case #048 with deadline 2026-02-28
+ - Case #055 with no deadline set
+- WHEN the user views My Work
+- THEN Case #048 MUST appear before Case #055
+- AND Case #055 MUST appear in the "No Deadline" grouped section
+
+### REQ-MYWORK-004: Grouped Sections [MVP]
+
+The system MUST group My Work items into urgency-based sections to provide visual structure.
+
+#### Scenario: Overdue section (red)
+- GIVEN cases/tasks where deadline/dueDate is before today
+- WHEN the user views My Work
+- THEN those items MUST appear in a section titled "OVERDUE"
+- AND the section MUST have a red visual treatment (red background tint, red section header, or red left border)
+- AND each item within MUST show "X days overdue" in red text
+- AND the section MUST appear first (above all other sections)
+
+#### Scenario: Due This Week section
+- GIVEN today is Monday, 2026-02-23
+- AND there are items with deadline/dueDate between today and Sunday 2026-03-01 (inclusive)
+- WHEN the user views My Work
+- THEN those items MUST appear in a section titled "DUE THIS WEEK"
+- AND each item MUST show the number of days remaining (e.g., "1 day", "3 days")
+
+#### Scenario: Upcoming section
+- GIVEN items with deadline/dueDate after the current week
+- WHEN the user views My Work
+- THEN those items MUST appear in a section titled "UPCOMING"
+- AND each item MUST show the due date
+
+#### Scenario: No Deadline section
+- GIVEN items with no deadline or dueDate set
+- WHEN the user views My Work
+- THEN those items MUST appear in a section titled "NO DEADLINE"
+- AND this section MUST appear last (below all dated sections)
+
+#### Scenario: Item count per section
+- GIVEN 2 overdue items, 3 due this week, and 2 upcoming
+- WHEN the user views My Work
+- THEN each section header SHOULD display the count of items in that section (e.g., "OVERDUE (2)")
+
+#### Scenario: Empty sections are hidden
+- GIVEN no items are overdue
+- WHEN the user views My Work
+- THEN the "OVERDUE" section MUST NOT be displayed
+- AND the first visible section MUST be whichever section has items
+
+### REQ-MYWORK-005: Overdue Highlighting [MVP]
+
+The system MUST visually distinguish overdue items from on-time items.
+
+#### Scenario: Overdue case highlighting
+- GIVEN case #2024-042 has deadline 2026-02-20 and today is 2026-02-25
+- AND the case status is "In behandeling" (not final)
+- WHEN the user views My Work
+- THEN the case MUST be displayed with a red visual indicator (red background, red badge, or red left border)
+- AND the text "5 days overdue" MUST be displayed in red
+- AND the deadline date MUST be shown
+
+#### Scenario: Overdue task highlighting
+- GIVEN a task "Review documents" has dueDate 2026-02-24 and today is 2026-02-25
+- AND the task status is "active"
+- WHEN the user views My Work
+- THEN the task MUST be displayed with a red visual indicator
+- AND the text "1 day overdue" MUST be displayed in red
+
+#### Scenario: Non-overdue item (normal display)
+- GIVEN a case with deadline 2026-02-28 and today is 2026-02-25
+- WHEN the user views My Work
+- THEN the case MUST be displayed without red highlighting
+- AND the text "3 days" MUST be displayed in a neutral color
+
+### REQ-MYWORK-006: Default Filter -- Non-Final Items Only [MVP]
+
+By default, My Work MUST only show open (non-completed) items.
+
+#### Scenario: Only non-final cases shown by default
+- GIVEN the user is handler on 5 cases: 3 with non-final status, 2 with final status ("Afgehandeld")
+- WHEN they view My Work
+- THEN only the 3 non-final cases MUST be shown
+- AND the 2 completed cases MUST be hidden
+
+#### Scenario: Only non-completed tasks shown by default
+- GIVEN the user has 6 tasks: 4 with status `available` or `active`, 2 with status `completed`
+- WHEN they view My Work
+- THEN only the 4 open tasks MUST be shown
+
+#### Scenario: Toggle to show completed items
+- GIVEN the user is viewing My Work with 3 open items
+- AND they have 2 completed items hidden
+- WHEN they toggle the "Show completed" control
+- THEN all 5 items MUST be displayed
+- AND completed items MUST be visually distinguished (e.g., strikethrough, muted colors, or a "Completed" badge)
+- AND completed items SHOULD appear at the bottom of the list, below all open items
+
+### REQ-MYWORK-007: Item Navigation [MVP]
+
+Clicking an item in My Work MUST navigate to the appropriate detail view.
+
+#### Scenario: Click case item to navigate
+- GIVEN case #2024-042 appears in My Work
+- WHEN the user clicks on the case item
+- THEN the system MUST navigate to the case detail view for case #2024-042
+
+#### Scenario: Click task item to navigate
+- GIVEN a task "Review documents" for case #2024-042 appears in My Work
+- WHEN the user clicks on the task item
+- THEN the system MUST navigate to the task detail or the parent case detail view with the task highlighted
+
+#### Scenario: Click parent case reference on task
+- GIVEN a task item shows "Case: #2024-042 Bouwvergunning Keizersgracht" as a clickable reference
+- WHEN the user clicks on the parent case reference (not the task itself)
+- THEN the system MUST navigate to the case detail view for case #2024-042
+
+### REQ-MYWORK-008: Cross-App Workload [V1]
+
+The My Work view SHOULD include items from Pipelinq (leads and requests) assigned to the current user.
+
+#### Scenario: Include Pipelinq leads and requests
+- GIVEN the current user has:
+ - 2 cases in Procest
+ - 3 tasks in Procest
+ - 1 lead in Pipelinq (assigned to them)
+ - 2 requests in Pipelinq (assigned to them)
+- WHEN they view My Work with cross-app integration enabled
+- THEN all 8 items MUST appear in a unified list
+- AND each item MUST be labeled with its source: [CASE], [TASK], [LEAD], [REQUEST]
+- AND Pipelinq items MUST follow the same sorting and grouping rules as Procest items
+
+#### Scenario: Cross-app filter tabs
+- GIVEN cross-app workload is enabled and the user has items from both Procest and Pipelinq
+- WHEN they view My Work
+- THEN the filter tabs MUST include: "All", "Cases", "Tasks", "Leads", "Requests"
+- AND each tab MUST show its item count
+
+#### Scenario: Pipelinq app not installed
+- GIVEN the Pipelinq app is not installed on this Nextcloud instance
+- WHEN the user views My Work
+- THEN the system MUST show only Procest items (cases and tasks)
+- AND no Pipelinq-related filter tabs MUST be shown
+- AND no error messages MUST appear about Pipelinq being unavailable
+
+### REQ-MYWORK-009: Empty State [MVP]
+
+The system MUST display a helpful message when the user has no assigned items.
+
+#### Scenario: No assigned items
+- GIVEN the current user has no cases where they are handler and no tasks assigned to them
+- WHEN they navigate to "My Work"
+- THEN the system MUST display an empty state with:
+ - A friendly message (e.g., "You have no cases or tasks assigned to you")
+ - Guidance on how items appear here (e.g., "Cases and tasks assigned to you will appear in this view")
+- AND the filter tabs MUST all show "(0)"
+
+#### Scenario: All items completed (show-completed toggle off)
+- GIVEN the user has 5 items but all have reached final/completed status
+- AND the "Show completed" toggle is off
+- WHEN they view My Work
+- THEN the system MUST display a contextual empty state (e.g., "All caught up! No open items.")
+- AND the system SHOULD indicate that completed items can be shown via the toggle
+
+### REQ-MYWORK-010: Concurrent State Changes [MVP]
+
+The system MUST handle cases where items change status while the user is viewing My Work.
+
+#### Scenario: Case closed while viewing My Work
+- GIVEN the user is viewing My Work with case #2024-042 listed
+- AND another user changes case #2024-042 to a final status
+- WHEN the user refreshes My Work (or the data auto-refreshes)
+- THEN case #2024-042 MUST no longer appear in the list (unless "Show completed" is on)
+- AND the item counts MUST update accordingly
+
+#### Scenario: Case deleted while in My Work list
+- GIVEN the user is viewing My Work with case #2024-042 listed
+- AND case #2024-042 is deleted by an admin
+- WHEN the user clicks on case #2024-042
+- THEN the system MUST display a "Case not found" message or redirect to the case list
+- AND the system MUST NOT show an unhandled error
+- AND on next refresh, the deleted case MUST no longer appear in My Work
+
+#### Scenario: Task reassigned away from user
+- GIVEN the user is viewing My Work with task "Review documents" listed
+- AND the task is reassigned to a different user
+- WHEN the user refreshes My Work
+- THEN the task MUST no longer appear in the list
+- AND the item counts MUST update accordingly
+
+## Non-Functional Requirements
+
+- **Performance**: My Work MUST load within 1 second for users with up to 100 assigned items. The two queries (cases + tasks) SHOULD be executed in parallel.
+- **Accessibility**: Each item MUST be keyboard-navigable. Screen readers MUST announce the entity type, title, urgency status, and deadline. Overdue visual indicators MUST NOT rely solely on color (use text "X days overdue" as well). All content MUST meet WCAG AA standards.
+- **Localization**: All labels, section titles, date formatting, and relative time expressions (e.g., "5 days overdue", "3 days") MUST support English and Dutch localization.
+- **Responsiveness**: The My Work view MUST adapt to narrow viewports, maintaining readability of all item fields on mobile screens.
+
+---
+
+### Current Implementation Status
+
+**Substantially implemented (MVP).** The My Work view exists and covers most MVP requirements.
+
+**Implemented (with file paths):**
+- **My Work view**: `src/views/MyWork.vue` -- full implementation with filter tabs (All/Cases/Tasks), grouped sections (Overdue, Due this week, Upcoming, No deadline), overdue highlighting (red left border, red text), item counts per section, empty states, and show-completed toggle.
+- **Navigation entry**: `src/navigation/MainMenu.vue` -- "My Work" menu item with `AccountCheck` icon linked to route `/my-work`.
+- **Router**: `src/router/index.js` -- route `{ path: '/my-work', name: 'MyWork', component: MyWork }`.
+- **Dashboard helpers**: `src/utils/dashboardHelpers.js` -- `getGroupedMyWorkItems()` function that groups items into overdue/dueThisWeek/upcoming/noDeadline sections with sorting by priority then deadline.
+- **Task API service**: `src/services/taskApi.js` -- `fetchTasksForCases()` fetches CalDAV tasks linked to cases.
+- **Object store**: `src/store/modules/object.js` -- uses `createObjectStore('object')` from `@conduction/nextcloud-vue` for CRUD operations against OpenRegister.
+- **Filter tabs**: Three tabs (All, Cases, Tasks) with item counts, active tab highlighting (REQ-MYWORK-002).
+- **Grouped sections**: Four sections with section counts and empty section hiding (REQ-MYWORK-004).
+- **Overdue highlighting**: Red border on overdue rows, red overdue text, priority indicators (REQ-MYWORK-005).
+- **Default non-final filter**: Active cases only by default; show-completed toggle fetches final-status cases (REQ-MYWORK-006).
+- **Item navigation**: Click navigates to CaseDetail; task clicks navigate to the linked case (REQ-MYWORK-007).
+- **Empty state**: NcEmptyContent with "No items assigned to you" and "All caught up!" messages (REQ-MYWORK-009).
+- **Dashboard widgets**: `lib/Dashboard/MyTasksWidget.php`, `lib/Dashboard/OverdueCasesWidget.php`, `lib/Dashboard/CasesOverviewWidget.php` -- Nextcloud dashboard widgets with corresponding Vue components in `src/views/widgets/`.
+- **Dashboard preview**: `src/views/dashboard/MyWorkPreview.vue` and `src/views/dashboard/OverduePanel.vue` -- summary panels on the main dashboard.
+
+**Not yet implemented:**
+- **REQ-MYWORK-008: Cross-App Workload (V1)**: No Pipelinq integration. The view only shows Procest cases and tasks. No [LEAD] or [REQUEST] badges. No conditional filter tabs for Pipelinq items.
+- **Task data source**: Currently uses CalDAV tasks via `fetchTasksForCases()` rather than OpenRegister `task` schema objects as specified. The spec envisions tasks as OpenRegister objects, but the current implementation fetches from Nextcloud's CalDAV task backend.
+- **Case type name resolution**: The case type name is not displayed on case items in the My Work list (spec requires it in REQ-MYWORK-001).
+- **Keyboard navigation**: No explicit keyboard navigation support (tab through items, enter to open).
+- **Screen reader announcements**: No ARIA attributes for entity type, urgency status, or deadline.
+- **Localization**: Translation functions `t()` are used throughout, but Dutch translations may be incomplete in l10n files.
+- **Responsiveness**: No explicit responsive/mobile styling in the component.
+
+### Standards & References
+
+- **CMMN 1.1**: Task statuses (available, active, completed, terminated, disabled) follow the CMMN PlanItem lifecycle, as implemented in `src/utils/taskLifecycle.js`.
+- **Schema.org**: Cases map to `schema:Project`, tasks to `schema:Action` (defined in `procest_register.json`).
+- **ZGW APIs (VNG Realisatie)**: Cases correspond to `Zaak`, tasks to internal work items. The ZRC controller (`lib/Controller/ZrcController.php`) provides ZGW-compliant endpoints.
+- **WCAG 2.1 AA**: Spec requires color-independent overdue indicators (text "X days overdue" alongside color). Currently implemented with both red styling and text labels.
+- **NL Design System**: CSS variables used for colors (e.g., `--color-error`, `--color-primary-element-light`) which support theming.
+
+### Specificity Assessment
+
+- **Mostly implementable as-is.** The MVP requirements are specific enough and are largely already implemented.
+- **Ambiguity in task data source**: The spec assumes tasks are OpenRegister objects in the `task` schema, but the current implementation uses CalDAV tasks. This architectural mismatch needs resolution -- should the spec be updated to reflect CalDAV, or should the implementation migrate to OpenRegister tasks?
+- **Missing detail on cross-app workload**: The V1 cross-app requirement lacks detail on how Pipelinq data is discovered (does Procest query the Pipelinq register directly? Does Pipelinq expose an API?).
+- **Open questions:**
+ - Should the My Work view support auto-refresh (polling or websocket) for concurrent state changes (REQ-MYWORK-010)?
+ - What is the performance target for users with 100+ items? The current implementation fetches up to 100 cases in a single call.
diff --git a/openspec/specs/openregister-integration/spec.md b/openspec/specs/openregister-integration/spec.md
index 9ea2429a..b0588b81 100644
--- a/openspec/specs/openregister-integration/spec.md
+++ b/openspec/specs/openregister-integration/spec.md
@@ -1,951 +1,951 @@
-# OpenRegister Integration Specification
-
-## Purpose
-
-Procest owns **no database tables**. All data is stored as OpenRegister objects in a dedicated `procest` register containing 12 schemas. This spec defines how the register and schemas are configured, how the repair step initializes the data model, how the frontend interacts with the OpenRegister API, the Pinia store patterns, cross-entity reference semantics, error handling, pagination, RBAC, cascade behaviors, and performance considerations.
-
-OpenRegister integration is the foundational layer upon which all other Procest features are built.
-
-**Standards**: OpenAPI 3.0.0 (schema format), OpenRegister API conventions
-**Feature tier**: MVP (foundation for all features)
-
----
-
-## Architecture Overview
-
-```
-┌─────────────────────────────────────────────────┐
-│ Procest Frontend (Vue 2 + Pinia) │
-│ - Pinia stores per entity type │
-│ - API service layer with error handling │
-└──────────────┬──────────────────────────────────┘
- │ REST API calls
-┌──────────────▼──────────────────────────────────┐
-│ OpenRegister API │
-│ /index.php/apps/openregister/api/objects/ │
-│ {register}/{schema}/{id} │
-│ - CRUD operations │
-│ - Search, pagination, filtering │
-│ - Schema validation │
-│ - RBAC enforcement │
-└──────────────┬──────────────────────────────────┘
- │
-┌──────────────▼──────────────────────────────────┐
-│ OpenRegister Storage (PostgreSQL) │
-│ - JSON object storage │
-│ - Schema validation │
-│ - Audit trail │
-└─────────────────────────────────────────────────┘
-```
-
----
-
-## Register and Schema Definitions
-
-### Register
-
-| Field | Value |
-|-------|-------|
-| Name | `procest` |
-| Slug | `procest` |
-| Description | Case management register for Procest |
-
-### Schema Inventory (12 schemas)
-
-The register MUST define exactly 12 schemas, organized into two groups:
-
-**Configuration schemas** (managed by admins, define case type behavior):
-
-| # | Schema | Purpose | CMMN/Schema.org | ZGW Equivalent |
-|---|--------|---------|-----------------|----------------|
-| 1 | `caseType` | Case type definition | CaseDefinition / CasePlanModel | ZaakType |
-| 2 | `statusType` | Status lifecycle phase per case type | Milestone | StatusType |
-| 3 | `resultType` | Case outcome type with archival rules | Case outcome | ResultaatType |
-| 4 | `roleType` | Participant role type per case type | schema:Role | RolType |
-| 5 | `propertyDefinition` | Custom field definition per case type | schema:PropertyValueSpecification | Eigenschap |
-| 6 | `documentType` | Document type requirement per case type | schema:DigitalDocument | InformatieObjectType |
-| 7 | `decisionType` | Decision type definition | schema:ChooseAction definition | BesluitType |
-
-**Instance schemas** (created by users during case operations):
-
-| # | Schema | Purpose | CMMN/Schema.org | ZGW Equivalent |
-|---|--------|---------|-----------------|----------------|
-| 8 | `case` | Case instance | CasePlanModel / schema:Project | Zaak |
-| 9 | `task` | Task within a case | HumanTask / schema:Action | (Taak) |
-| 10 | `role` | Role assignment on a case | schema:Role instance | Rol |
-| 11 | `result` | Case outcome record | Case result | Resultaat |
-| 12 | `decision` | Formal decision on a case | schema:ChooseAction instance | Besluit |
-
----
-
-## Requirements
-
-### REQ-OREG-001: Configuration File
-
-**Tier**: MVP
-
-The system MUST define its register and all schemas in a JSON configuration file that follows the OpenAPI 3.0.0 format, consistent with the pattern used by `opencatalogi` and `softwarecatalog`.
-
-#### Scenario: Configuration file exists and is valid
-
-- GIVEN the Procest app source code
-- THEN the file `lib/Settings/procest_register.json` MUST exist
-- AND it MUST be valid JSON
-- AND it MUST conform to OpenAPI 3.0.0 format
-- AND it MUST define a register with slug `procest`
-- AND it MUST define exactly 12 schemas as listed in the schema inventory
-
-#### Scenario: Schema defines required properties for case
-
-- GIVEN the `case` schema definition in `procest_register.json`
-- THEN it MUST define the following required properties:
- - `title` (string, max 255)
- - `caseType` (string, format: uuid, reference to caseType)
- - `status` (string, format: uuid, reference to statusType)
- - `startDate` (string, format: date)
-- AND it MUST define the following optional properties:
- - `description` (string)
- - `identifier` (string, auto-generated)
- - `result` (string, format: uuid, reference to result)
- - `endDate` (string, format: date)
- - `plannedEndDate` (string, format: date)
- - `deadline` (string, format: date)
- - `confidentiality` (string, enum)
- - `assignee` (string)
- - `priority` (string, enum: low, normal, high, urgent)
- - `parentCase` (string, format: uuid)
- - `relatedCases` (array of strings)
- - `geometry` (object, GeoJSON)
-
-#### Scenario: Schema defines required properties for task
-
-- GIVEN the `task` schema definition in `procest_register.json`
-- THEN it MUST define:
- - `title` (string, required)
- - `status` (string, enum: available, active, completed, terminated, disabled, required, default: "available")
- - `case` (string, format: uuid, required)
- - `description` (string, optional)
- - `assignee` (string, optional)
- - `dueDate` (string, format: date-time, optional)
- - `priority` (string, enum: low, normal, high, urgent, optional, default: "normal")
- - `completedDate` (string, format: date-time, optional)
-
-#### Scenario: Schema defines required properties for role
-
-- GIVEN the `role` schema definition
-- THEN it MUST define:
- - `name` (string, required)
- - `roleType` (string, format: uuid, required)
- - `case` (string, format: uuid, required)
- - `participant` (string, required)
- - `description` (string, optional)
-
-#### Scenario: Schema defines required properties for result
-
-- GIVEN the `result` schema definition
-- THEN it MUST define:
- - `name` (string, required)
- - `case` (string, format: uuid, required)
- - `resultType` (string, format: uuid, required)
- - `description` (string, optional)
-
-#### Scenario: Schema defines required properties for decision
-
-- GIVEN the `decision` schema definition
-- THEN it MUST define:
- - `title` (string, required)
- - `case` (string, format: uuid, required)
- - `description` (string, optional)
- - `decisionType` (string, format: uuid, optional)
- - `decidedBy` (string, optional)
- - `decidedAt` (string, format: date-time, optional)
- - `effectiveDate` (string, format: date, optional)
- - `expiryDate` (string, format: date, optional)
-
-#### Scenario: Schema defines caseType with all behavioral fields
-
-- GIVEN the `caseType` schema definition
-- THEN it MUST define at minimum:
- - `title` (string, required)
- - `description` (string, optional)
- - `identifier` (string, auto)
- - `purpose` (string, required)
- - `trigger` (string, required)
- - `subject` (string, required)
- - `processingDeadline` (string, ISO 8601 duration, required)
- - `confidentiality` (string, enum, required)
- - `isDraft` (boolean, default: true)
- - `validFrom` (string, format: date, required)
- - `validUntil` (string, format: date, optional)
- - `origin` (string, enum: internal, external, required)
- - `suspensionAllowed` (boolean, required)
- - `extensionAllowed` (boolean, required)
- - `publicationRequired` (boolean, required)
-
-#### Scenario: All schemas include type annotations
-
-- GIVEN each schema definition
-- THEN each MUST include a `@type` property or annotation referencing the appropriate standard:
- - `case`: `schema:Project`
- - `task`: `schema:Action`
- - `role`: `schema:Role`
- - `result`: (no standard type, app-specific)
- - `decision`: `schema:ChooseAction`
- - `caseType`: `schema:Project` definition
- - `statusType`: `schema:ActionStatusType`
- - `roleType`: `schema:Role` definition
- - `propertyDefinition`: `schema:PropertyValueSpecification`
- - `documentType`: `schema:DigitalDocument`
- - `decisionType`: `schema:ChooseAction` definition
- - `resultType`: (no standard type)
-
----
-
-### REQ-OREG-002: Auto-Configuration on Install (Repair Step)
-
-**Tier**: MVP
-
-The system MUST import the register configuration during app installation and upgrades via the Nextcloud repair step mechanism.
-
-#### Scenario: First install creates register and all schemas
-
-- GIVEN Procest is being installed for the first time on a Nextcloud instance with OpenRegister
-- WHEN the repair step `lib/Migration/ImportConfiguration.php` runs
-- THEN it MUST call `ConfigurationService::importFromApp('procest')`
-- AND the `procest` register MUST be created in OpenRegister
-- AND all 12 schemas MUST be created with their property definitions
-- AND the repair step MUST log success or failure
-
-#### Scenario: Upgrade adds new schemas without data loss
-
-- GIVEN Procest was previously installed with 10 schemas (before decisionType and propertyDefinition were added)
-- AND existing cases, tasks, and roles exist in the register
-- WHEN the repair step runs during upgrade
-- THEN the 2 new schemas (`decisionType`, `propertyDefinition`) MUST be created
-- AND existing schemas MUST be updated if their definitions changed (new properties added)
-- AND existing objects in unchanged schemas MUST NOT be modified or deleted
-- AND no data loss MUST occur
-
-#### Scenario: Repair step is idempotent
-
-- GIVEN the repair step has already run successfully
-- WHEN the repair step runs again (e.g., during `occ maintenance:repair`)
-- THEN it MUST NOT create duplicate registers or schemas
-- AND existing data MUST remain intact
-- AND the operation MUST complete without errors
-
-#### Scenario: Repair step handles missing OpenRegister gracefully
-
-- GIVEN Procest is installed but OpenRegister is NOT installed
-- WHEN the repair step runs
-- THEN it MUST log a clear error message indicating that OpenRegister is required
-- AND the repair step MUST NOT crash or throw an unhandled exception
-- AND Procest MUST indicate to the admin that OpenRegister needs to be installed
-
-#### Scenario: Schema property additions are non-destructive
-
-- GIVEN the `task` schema previously had 6 properties
-- AND the upgrade adds 2 new optional properties (e.g., `checklist`, `blockedBy`)
-- WHEN the repair step updates the schema
-- THEN the 2 new properties MUST be added to the schema
-- AND existing task objects MUST remain valid (new properties are optional)
-- AND existing task objects MUST NOT have the new properties set to any default
-
----
-
-### REQ-OREG-003: Frontend API Interaction Patterns
-
-**Tier**: MVP
-
-The frontend MUST interact with OpenRegister's REST API for all CRUD operations. All API calls MUST follow consistent URL patterns and error handling.
-
-#### Scenario: Base URL pattern
-
-- GIVEN the Procest frontend needs to access OpenRegister
-- THEN all API calls MUST use the base URL pattern: `/index.php/apps/openregister/api/objects/procest/{schema}`
-- AND for single objects: `/index.php/apps/openregister/api/objects/procest/{schema}/{uuid}`
-
-#### Scenario: List all cases (GET collection)
-
-- GIVEN the `case` schema exists in the `procest` register with 24 case objects
-- WHEN the frontend requests the case list
-- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/case`
-- AND the response MUST include:
- - An array of case objects
- - Pagination metadata (`total`, `page`, `limit`, `pages`)
-- AND the default page size MUST be configurable (e.g., 20)
-
-#### Scenario: Get a single case (GET object)
-
-- GIVEN a case with UUID "abc-123-def" exists
-- WHEN the frontend requests the case detail
-- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/case/abc-123-def`
-- AND the response MUST include all case properties
-
-#### Scenario: Create a new case (POST)
-
-- GIVEN the user fills in the new case form with:
- - title: "Bouwvergunning Prinsengracht 200"
- - caseType: "casetype-uuid-omgevings"
- - startDate: "2026-03-01"
-- WHEN the user submits the form
-- THEN the frontend MUST call `POST /index.php/apps/openregister/api/objects/procest/case`
-- AND the request body MUST contain the case properties as JSON
-- AND the response MUST include the created object with its generated UUID
-
-#### Scenario: Update an existing case (PUT)
-
-- GIVEN an existing case with UUID "abc-123-def"
-- WHEN the user updates the description
-- THEN the frontend MUST call `PUT /index.php/apps/openregister/api/objects/procest/case/abc-123-def`
-- AND the request body MUST contain the full updated object
-- AND the response MUST include the updated object
-
-#### Scenario: Delete a case (DELETE)
-
-- GIVEN an existing case with UUID "abc-123-def"
-- WHEN the user deletes the case
-- THEN the frontend MUST call `DELETE /index.php/apps/openregister/api/objects/procest/case/abc-123-def`
-- AND the response MUST confirm deletion (HTTP 200 or 204)
-
-#### Scenario: API call with authentication
-
-- GIVEN a logged-in Nextcloud user
-- THEN all OpenRegister API calls MUST include the Nextcloud session cookie or authorization header
-- AND unauthenticated requests MUST be rejected with HTTP 401
-
----
-
-### REQ-OREG-004: Pagination and Filtering
-
-**Tier**: MVP
-
-The frontend MUST support paginated access to object lists and use OpenRegister query parameters for filtering, searching, and sorting.
-
-#### Scenario: Paginate case list
-
-- GIVEN 24 cases exist in the register
-- WHEN the frontend requests page 2 with limit 10
-- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/case?_page=2&_limit=10`
-- AND the response MUST contain cases 11-20
-- AND the pagination metadata MUST show: `total: 24`, `page: 2`, `limit: 10`, `pages: 3`
-
-#### Scenario: Filter cases by status
-
-- GIVEN cases with various status references
-- WHEN the frontend filters by a specific status type UUID
-- THEN it MUST include the filter as a query parameter: `?status=statustype-uuid-inbehandeling`
-- AND only cases matching that status MUST be returned
-
-#### Scenario: Filter tasks by case
-
-- GIVEN 23 tasks across 8 cases
-- WHEN the frontend requests tasks for case #2024-042 (UUID: "case-uuid-042")
-- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/task?case=case-uuid-042`
-- AND only tasks linked to that case MUST be returned
-
-#### Scenario: Filter tasks by assignee
-
-- GIVEN tasks assigned to various users
-- WHEN the frontend filters by assignee "jan.devries"
-- THEN it MUST include `?assignee=jan.devries` in the query
-- AND only tasks assigned to Jan MUST be returned
-
-#### Scenario: Search by text field
-
-- GIVEN cases with various titles
-- WHEN the user searches for "bouwvergunning"
-- THEN the frontend MUST pass the search term via the appropriate OpenRegister search parameter
-- AND results MUST include cases whose title contains "bouwvergunning" (case-insensitive)
-
-#### Scenario: Sort by field
-
-- GIVEN the task list is displayed
-- WHEN the user sorts by due date ascending
-- THEN the frontend MUST include `?_sort=dueDate&_order=asc` in the query
-- AND the API response MUST return tasks ordered by due date ascending
-
-#### Scenario: Combined filters
-
-- GIVEN the user applies multiple filters: assignee "jan.devries", status "active", sorted by priority
-- THEN the frontend MUST combine all filters: `?assignee=jan.devries&status=active&_sort=priority&_order=desc`
-- AND the API MUST apply all filters conjunctively (AND logic)
-
----
-
-### REQ-OREG-005: Pinia Store Patterns
-
-**Tier**: MVP
-
-The frontend MUST use Pinia stores for state management, with one store per entity type. Stores MUST follow a consistent pattern for CRUD actions, loading states, error handling, and pagination.
-
-#### Scenario: Case store provides standard CRUD actions
-
-- GIVEN the `useCaseStore()` Pinia store
-- THEN it MUST provide the following actions:
- - `fetchCases(params?)` -- list with optional filter/pagination params
- - `fetchCase(id)` -- get single case by UUID
- - `createCase(data)` -- create new case
- - `updateCase(id, data)` -- update existing case
- - `deleteCase(id)` -- delete case
-- AND each action MUST construct the correct OpenRegister API URL
-- AND each action MUST handle loading states and errors
-
-#### Scenario: Store tracks loading state
-
-- GIVEN the case store
-- WHEN `fetchCases()` is called
-- THEN `store.loading` MUST be set to `true` before the API call
-- AND `store.loading` MUST be set to `false` after the API call completes (success or failure)
-- AND the UI MUST show a loading indicator while `store.loading` is `true`
-
-#### Scenario: Store tracks error state
-
-- GIVEN the case store
-- WHEN an API call fails with HTTP 500
-- THEN `store.error` MUST be set to an error object containing the status code and message
-- AND the UI MUST display a user-friendly error message
-- AND `store.loading` MUST be set to `false`
-
-#### Scenario: Store handles pagination state
-
-- GIVEN the case store fetches a paginated list
-- THEN the store state MUST include:
- - `items` -- array of case objects for the current page
- - `total` -- total number of matching cases
- - `page` -- current page number
- - `limit` -- items per page
- - `pages` -- total number of pages
-- AND the store MUST provide a `fetchPage(page)` action that fetches a specific page
-
-#### Scenario: Task store follows the same pattern
-
-- GIVEN the `useTaskStore()` Pinia store
-- THEN it MUST provide: `fetchTasks(params?)`, `fetchTask(id)`, `createTask(data)`, `updateTask(id, data)`, `deleteTask(id)`
-- AND it MUST follow the same loading/error/pagination pattern as the case store
-
-#### Scenario: All entity types have stores
-
-- GIVEN the Procest frontend
-- THEN Pinia stores MUST exist for all 12 entity types:
- - `useCaseStore()`, `useTaskStore()`, `useRoleStore()`, `useResultStore()`, `useDecisionStore()`
- - `useCaseTypeStore()`, `useStatusTypeStore()`, `useResultTypeStore()`, `useRoleTypeStore()`
- - `usePropertyDefinitionStore()`, `useDocumentTypeStore()`, `useDecisionTypeStore()`
-- AND each store MUST follow the same CRUD + loading + error + pagination pattern
-
-#### Scenario: Store caches fetched data
-
-- GIVEN the case store has already fetched case "abc-123-def"
-- WHEN `fetchCase("abc-123-def")` is called again within the same session
-- THEN the store SHOULD return the cached version immediately
-- AND the store MAY optionally refetch in the background (stale-while-revalidate)
-
----
-
-### REQ-OREG-006: Cross-Entity References
-
-**Tier**: MVP
-
-Entities in Procest reference each other via UUID. The frontend MUST resolve these references to display meaningful data (titles, names) rather than raw UUIDs.
-
-#### Scenario: Task references a case
-
-- GIVEN a task object with `case: "case-uuid-042"`
-- WHEN the task is displayed in a list or card
-- THEN the frontend MUST resolve "case-uuid-042" to display the case identifier and title (e.g., "Case #2024-042 Bouwvergunning Keizersgracht")
-- AND the resolved case reference MUST be clickable, navigating to the case detail
-
-#### Scenario: Case references a case type
-
-- GIVEN a case object with `caseType: "casetype-uuid-omgevings"`
-- WHEN the case is displayed in the case list
-- THEN the frontend MUST resolve the case type to display its title (e.g., "Omgevingsvergunning")
-
-#### Scenario: Role references both case and role type
-
-- GIVEN a role object with:
- - `case: "case-uuid-042"`
- - `roleType: "roletype-uuid-handler"`
- - `participant: "jan.devries"`
-- WHEN the role is displayed on the case detail page
-- THEN the frontend MUST resolve:
- - The role type to its name (e.g., "Behandelaar")
- - The participant to the Nextcloud user display name (e.g., "Jan de Vries")
- - The case reference to the case title (if displayed outside case context)
-
-#### Scenario: Result references a result type
-
-- GIVEN a result object with `resultType: "resulttype-uuid-granted"`
-- WHEN the result is displayed
-- THEN the frontend MUST resolve the result type to its name (e.g., "Vergunning verleend")
-- AND the archival information from the result type SHOULD be accessible
-
-#### Scenario: Case type hierarchy resolution
-
-- GIVEN a case detail view that needs to display:
- - The case type name
- - The current status name (from status type)
- - The handler name (from role)
- - Task list (from tasks referencing this case)
-- WHEN the case detail page loads
-- THEN the frontend MUST fetch and resolve all related entities
-- AND cross-references MUST be resolved in parallel where possible
-
-#### Scenario: Dangling reference (referenced object deleted)
-
-- GIVEN a task with `case: "case-uuid-deleted"` where the referenced case has been deleted
-- WHEN the task is displayed
-- THEN the frontend MUST handle the missing reference gracefully
-- AND it SHOULD display a "Case not found" or "[Deleted]" placeholder
-- AND the task MUST still be viewable and manageable
-
----
-
-### REQ-OREG-007: Schema Validation Rules
-
-**Tier**: MVP
-
-OpenRegister MUST validate objects against their schema definitions before storage. Procest schemas MUST define appropriate validation constraints.
-
-#### Scenario: Required field validation
-
-- GIVEN the `task` schema requires `title` and `case`
-- WHEN the frontend submits a task without a title
-- THEN the OpenRegister API MUST return HTTP 400/422 with a validation error
-- AND the error response MUST identify the missing field (`title`)
-- AND the frontend MUST display the validation error to the user
-
-#### Scenario: Enum validation for task status
-
-- GIVEN the `task` schema defines `status` as enum: `available`, `active`, `completed`, `terminated`, `disabled`
-- WHEN the frontend submits a task with `status: "pending"`
-- THEN the OpenRegister API MUST reject the request
-- AND the error MUST indicate that "pending" is not a valid value for `status`
-
-#### Scenario: Enum validation for priority
-
-- GIVEN the `task` schema defines `priority` as enum: `low`, `normal`, `high`, `urgent`
-- WHEN the frontend submits a task with `priority: "critical"`
-- THEN the API MUST reject with a validation error
-
-#### Scenario: Date format validation
-
-- GIVEN the `case` schema defines `startDate` as format: date
-- WHEN the frontend submits a case with `startDate: "not-a-date"`
-- THEN the API MUST reject with a format validation error
-
-#### Scenario: UUID reference format validation
-
-- GIVEN the `task` schema defines `case` as format: uuid
-- WHEN the frontend submits a task with `case: "not-a-uuid"`
-- THEN the API MUST reject with a format validation error
-
-#### Scenario: String length validation
-
-- GIVEN the `case` schema defines `title` with maxLength: 255
-- WHEN the frontend submits a case with a title of 300 characters
-- THEN the API MUST reject with a length validation error
-
----
-
-### REQ-OREG-008: Error Handling
-
-**Tier**: MVP
-
-The frontend MUST handle all categories of API errors gracefully and present user-friendly messages.
-
-#### Scenario: Network error (offline/timeout)
-
-- GIVEN the user is creating a case
-- WHEN the API call fails due to a network timeout
-- THEN the frontend MUST display a message like "Unable to reach the server. Please check your connection and try again."
-- AND the form data MUST be preserved (not cleared)
-- AND a retry option SHOULD be available
-
-#### Scenario: Validation error (HTTP 400/422)
-
-- GIVEN the user submits a case with missing required fields
-- WHEN the API returns HTTP 422 with field-level errors
-- THEN the frontend MUST map errors to specific form fields
-- AND invalid fields MUST be highlighted with their error messages
-- AND the form MUST remain editable for correction
-
-#### Scenario: Authorization error (HTTP 403)
-
-- GIVEN a user without admin privileges
-- WHEN they attempt to create a case type via the API
-- THEN the API MUST return HTTP 403
-- AND the frontend MUST display "You do not have permission to perform this action"
-
-#### Scenario: Not found error (HTTP 404)
-
-- GIVEN a case with UUID "abc-123-def" has been deleted
-- WHEN the frontend attempts to fetch it
-- THEN the API MUST return HTTP 404
-- AND the frontend MUST display "The requested case could not be found"
-- AND the frontend SHOULD redirect to the case list
-
-#### Scenario: Server error (HTTP 500)
-
-- GIVEN an unexpected error occurs on the server
-- WHEN the API returns HTTP 500
-- THEN the frontend MUST display a generic error message: "An unexpected error occurred. Please try again later."
-- AND the error SHOULD be logged to the browser console with details for debugging
-
-#### Scenario: Concurrent modification conflict (HTTP 409)
-
-- GIVEN two users are editing the same case simultaneously
-- WHEN user A saves after user B has already saved
-- THEN the API SHOULD return HTTP 409 (conflict)
-- AND the frontend MUST inform user A that the case was modified by another user
-- AND the frontend SHOULD offer to reload the latest version
-
----
-
-### REQ-OREG-009: Cascade Behaviors
-
-**Tier**: V1
-
-The system MUST define what happens to dependent entities when a parent entity is deleted or modified.
-
-#### Scenario: Delete a case with linked tasks, roles, results, and decisions
-
-- GIVEN case #2024-042 has:
- - 5 tasks
- - 3 roles
- - 1 result
- - 2 decisions
-- WHEN the user deletes case #2024-042
-- THEN the system MUST either:
- - (a) Cascade delete all linked tasks, roles, results, and decisions, OR
- - (b) Prevent deletion and warn the user that dependent entities exist
-- AND the system MUST NOT leave orphaned task/role/result/decision objects
-- AND the chosen behavior MUST be consistent
-
-#### Scenario: Delete a case type that is in use
-
-- GIVEN case type "Omgevingsvergunning" is referenced by 10 active cases
-- WHEN an admin attempts to delete the case type
-- THEN the system MUST prevent the deletion
-- AND the error message MUST indicate that the case type is in use by 10 cases
-- AND the admin SHOULD be advised to set the case type as draft or set a `validUntil` date instead
-
-#### Scenario: Delete a case type that is not in use
-
-- GIVEN case type "Bezwaarschrift" (draft, no cases reference it)
-- WHEN an admin deletes the case type
-- THEN the case type MUST be deleted
-- AND all linked status types, result types, role types, property definitions, document types, and decision types MUST also be deleted (cascade)
-
-#### Scenario: Remove a status type from a case type
-
-- GIVEN case type "Omgevingsvergunning" has 4 status types
-- AND status type "Besluitvorming" (order: 3) is being removed
-- AND 3 cases currently have status "Besluitvorming"
-- THEN the system MUST prevent removal
-- AND the error message MUST indicate that 3 cases are currently in this status
-
-#### Scenario: Remove an unused status type
-
-- GIVEN status type "Verouderde status" is linked to case type "Omgevingsvergunning"
-- AND no cases currently reference this status type
-- WHEN the admin removes it
-- THEN the status type MUST be deleted
-- AND the remaining status types MUST maintain their order (reorder if needed)
-
----
-
-### REQ-OREG-010: Audit Trail Integration
-
-**Tier**: MVP
-
-All create, update, and delete operations on Procest objects MUST be captured in the audit trail.
-
-#### Scenario: Case creation is logged
-
-- GIVEN user "jan.devries" creates case #2024-053
-- THEN the audit trail MUST record:
- - Action: "created"
- - Entity type: "case"
- - Entity UUID
- - User: "jan.devries"
- - Timestamp
- - Key field values (title, caseType)
-
-#### Scenario: Task status change is logged
-
-- GIVEN user "jan.devries" changes task "Review documenten" from `active` to `completed`
-- THEN the audit trail MUST record:
- - Action: "status_changed"
- - Entity type: "task"
- - Entity UUID
- - User: "jan.devries"
- - Old value: "active"
- - New value: "completed"
- - Timestamp
-
-#### Scenario: Role assignment is logged
-
-- GIVEN a coordinator assigns "maria.bakker" as advisor on case #2024-042
-- THEN the audit trail MUST record:
- - Action: "role_assigned"
- - Entity type: "role"
- - Case reference
- - Participant: "maria.bakker"
- - Role type: "Advisor"
- - Timestamp
-
-#### Scenario: Decision creation is logged
-
-- GIVEN "dr.k.bakker" records a decision on case #2024-042
-- THEN the audit trail MUST record the decision creation with all key fields
-
-#### Scenario: Audit trail is displayed on case detail
-
-- GIVEN case #2024-042 has 15 audit events
-- WHEN the user views the Activity Timeline section on the case detail
-- THEN the events MUST be displayed in reverse chronological order
-- AND each event MUST show: description, user, timestamp
-- AND the timeline MUST be paginated or have a "Load more" option
-
----
-
-### REQ-OREG-011: RBAC (Role-Based Access Control)
-
-**Tier**: MVP
-
-The system MUST enforce access control via OpenRegister's RBAC system. Configuration entities (case types, status types, etc.) MUST be admin-only. Instance entities (cases, tasks, roles, results, decisions) MUST be accessible to authorized users.
-
-#### Scenario: Admin-only access to case type management
-
-- GIVEN a non-admin user "jan.devries"
-- WHEN Jan attempts to create, update, or delete a case type via the API
-- THEN the system MUST return HTTP 403
-- AND the operation MUST NOT be performed
-
-#### Scenario: Admin can manage all configuration entities
-
-- GIVEN an admin user "admin"
-- THEN the admin MUST be able to CRUD all 7 configuration schemas:
- - caseType, statusType, resultType, roleType, propertyDefinition, documentType, decisionType
-- AND the admin settings page in Nextcloud MUST provide the management UI
-
-#### Scenario: Regular user can create cases and tasks
-
-- GIVEN a regular Nextcloud user "jan.devries"
-- THEN Jan MUST be able to:
- - Create cases (POST to case schema)
- - Create tasks on cases he has access to
- - Create roles on cases he has access to
- - Record results on cases he is handler for
- - Create decisions on cases he has access to
-
-#### Scenario: User can only see cases they have access to
-
-- GIVEN OpenRegister RBAC is configured
-- WHEN "jan.devries" requests the case list
-- THEN the API MUST return only cases that Jan has permission to view
-- AND cases assigned to other users/organizations that Jan has no role in MUST NOT be returned
-
-#### Scenario: Nextcloud admin settings page requires admin
-
-- GIVEN a non-admin user navigates to the Procest admin settings URL
-- THEN the Nextcloud admin settings system MUST prevent access
-- AND the user MUST be redirected or shown an "access denied" page
-
----
-
-### REQ-OREG-012: Performance and Eager Loading
-
-**Tier**: MVP
-
-The frontend MUST minimize API round-trips by fetching related entities efficiently.
-
-#### Scenario: Case detail page loads all related data in parallel
-
-- GIVEN the user opens case detail for case #2024-042
-- THEN the frontend MUST fetch the following in parallel (not sequentially):
- - Case object (with case type, status references)
- - Tasks for the case (`?case=case-uuid-042`)
- - Roles for the case (`?case=case-uuid-042`)
- - Decisions for the case (`?case=case-uuid-042`)
- - Result for the case (if exists)
-- AND the total load time MUST be under 3 seconds for a case with 10 tasks, 5 roles, 3 decisions
-
-#### Scenario: Case list resolves case type names efficiently
-
-- GIVEN the case list shows 20 cases referencing 4 different case types
-- THEN the frontend MUST NOT make 20 individual API calls to resolve case type names
-- AND instead MUST fetch all relevant case types in a single call (or use the cached case type store)
-- AND the case type store SHOULD pre-fetch all case types on app initialization (small dataset, typically less than 20)
-
-#### Scenario: Status type resolution is cached
-
-- GIVEN case types have between 3-6 status types each
-- WHEN the case list or detail page needs to display status names
-- THEN status types MUST be fetched once per case type and cached in the Pinia store
-- AND subsequent accesses MUST use the cached data
-
-#### Scenario: My Work aggregation performance
-
-- GIVEN the My Work view needs to display cases and tasks for the current user
-- THEN the frontend MUST make exactly 2 API calls:
- - Cases with `?assignee=currentUser&status_ne=final` (non-final cases assigned to user)
- - Tasks with `?assignee=currentUser&status=available,active` (active/available tasks)
-- AND the results MUST be merged and sorted client-side
-- AND the total load time MUST be under 2 seconds
-
-#### Scenario: Pagination prevents loading too many objects
-
-- GIVEN the case list could contain hundreds of cases
-- THEN the default page size MUST NOT exceed 50
-- AND the frontend MUST use pagination (not load all objects at once)
-- AND lazy loading or virtual scrolling SHOULD be used for long lists
-
----
-
-### REQ-OREG-013: Cross-Entity Reference Map
-
-**Tier**: MVP
-
-For implementation clarity, this is the complete reference map showing how entities relate to each other.
-
-```
-CaseType ─────────────────────────────────────────────────────────────┐
-│ │
-├── StatusType[] (statusType.caseType → caseType UUID) │
-├── ResultType[] (resultType.caseType → caseType UUID) │
-├── RoleType[] (roleType.caseType → caseType UUID) │
-├── PropertyDefinition[] (propertyDefinition.caseType → caseType UUID)│
-├── DocumentType[] (documentType.caseType → caseType UUID) │
-└── DecisionType[] (decisionType.caseType → caseType UUID) │
- │
-Case ─────────────────────────────────────────────────────────────────┤
-│ case.caseType → caseType UUID │
-│ case.status → statusType UUID │
-│ case.result → result UUID (optional) │
-│ case.assignee → Nextcloud user UID (optional) │
-│ case.parentCase → case UUID (optional, for sub-cases) │
-│ │
-├── Task[] (task.case → case UUID) │
-│ task.assignee → Nextcloud user UID (optional) │
-│ │
-├── Role[] (role.case → case UUID) │
-│ role.roleType → roleType UUID │
-│ role.participant → Nextcloud user UID or contact ref │
-│ │
-├── Result (result.case → case UUID, at most 1) │
-│ result.resultType → resultType UUID │
-│ │
-└── Decision[] (decision.case → case UUID) │
- decision.decisionType → decisionType UUID (optional) │
- decision.decidedBy → Nextcloud user UID (optional) │
-```
-
-#### Scenario: Verify reference integrity on task creation
-
-- GIVEN a user creates a task with `case: "case-uuid-042"`
-- THEN the system SHOULD verify that case "case-uuid-042" exists in the register
-- AND if the referenced case does not exist, the creation SHOULD be rejected
-
-#### Scenario: Verify role type belongs to the correct case type
-
-- GIVEN a user creates a role on case #2024-042 (caseType: "Omgevingsvergunning")
-- AND the user specifies roleType UUID for "Klager" which belongs to case type "Klacht"
-- THEN the system SHOULD reject the role creation
-- AND the error MUST indicate that the role type does not belong to the case's case type
-
-#### Scenario: Case type deletion cascades to child types
-
-- GIVEN case type "Bezwaarschrift" has 3 status types, 2 result types, and 2 role types
-- AND no cases reference this case type
-- WHEN the admin deletes the case type
-- THEN all 3 status types, 2 result types, and 2 role types MUST also be deleted
-
----
-
-## Summary: API Endpoint Patterns
-
-| Entity | List | Get | Create | Update | Delete |
-|--------|------|-----|--------|--------|--------|
-| Case | `GET .../procest/case` | `GET .../procest/case/{id}` | `POST .../procest/case` | `PUT .../procest/case/{id}` | `DELETE .../procest/case/{id}` |
-| Task | `GET .../procest/task` | `GET .../procest/task/{id}` | `POST .../procest/task` | `PUT .../procest/task/{id}` | `DELETE .../procest/task/{id}` |
-| Role | `GET .../procest/role` | `GET .../procest/role/{id}` | `POST .../procest/role` | `PUT .../procest/role/{id}` | `DELETE .../procest/role/{id}` |
-| Result | `GET .../procest/result` | `GET .../procest/result/{id}` | `POST .../procest/result` | `PUT .../procest/result/{id}` | `DELETE .../procest/result/{id}` |
-| Decision | `GET .../procest/decision` | `GET .../procest/decision/{id}` | `POST .../procest/decision` | `PUT .../procest/decision/{id}` | `DELETE .../procest/decision/{id}` |
-| CaseType | `GET .../procest/caseType` | `GET .../procest/caseType/{id}` | `POST .../procest/caseType` | `PUT .../procest/caseType/{id}` | `DELETE .../procest/caseType/{id}` |
-| StatusType | `GET .../procest/statusType` | `GET .../procest/statusType/{id}` | `POST .../procest/statusType` | `PUT .../procest/statusType/{id}` | `DELETE .../procest/statusType/{id}` |
-| ResultType | `GET .../procest/resultType` | `GET .../procest/resultType/{id}` | `POST .../procest/resultType` | `PUT .../procest/resultType/{id}` | `DELETE .../procest/resultType/{id}` |
-| RoleType | `GET .../procest/roleType` | `GET .../procest/roleType/{id}` | `POST .../procest/roleType` | `PUT .../procest/roleType/{id}` | `DELETE .../procest/roleType/{id}` |
-| PropDef | `GET .../procest/propertyDefinition` | `GET .../procest/propertyDefinition/{id}` | `POST ...` | `PUT ...` | `DELETE ...` |
-| DocType | `GET .../procest/documentType` | `GET .../procest/documentType/{id}` | `POST ...` | `PUT ...` | `DELETE ...` |
-| DecisionType | `GET .../procest/decisionType` | `GET .../procest/decisionType/{id}` | `POST ...` | `PUT ...` | `DELETE ...` |
-
-Base URL: `/index.php/apps/openregister/api/objects`
-
----
-
-## Pinia Store Inventory
-
-| Store | Entity | Key Extra Features |
-|-------|--------|-------------------|
-| `useCaseStore()` | case | Resolves caseType and status names; My Work filtering |
-| `useTaskStore()` | task | Kanban grouping by status; overdue calculation |
-| `useRoleStore()` | role | Resolves participant display names from Nextcloud |
-| `useResultStore()` | result | Links to resultType for archival info |
-| `useDecisionStore()` | decision | Validity period calculations |
-| `useCaseTypeStore()` | caseType | Cached on app init; used by all case views |
-| `useStatusTypeStore()` | statusType | Ordered by `order`; cached per case type |
-| `useResultTypeStore()` | resultType | Filtered by caseType |
-| `useRoleTypeStore()` | roleType | Filtered by caseType |
-| `usePropertyDefinitionStore()` | propertyDefinition | Filtered by caseType |
-| `useDocumentTypeStore()` | documentType | Filtered by caseType |
-| `useDecisionTypeStore()` | decisionType | Filtered by caseType (V1) |
-
----
-
-### Current Implementation Status
-
-**Core architecture implemented; individual entity stores differ from spec.**
-
-**Implemented (with file paths):**
-- **Configuration file**: `lib/Settings/procest_register.json` exists, is valid JSON, conforms to OpenAPI 3.0.0, defines a register with app `procest`. Defines 12 schemas: `caseType`, `statusType`, `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType`, `case`, `task`, `role`, `result`, `decision`. Each schema includes `x-schema-org-type` and `x-zgw-equivalent` annotations (REQ-OREG-001).
-- **Repair step**: `lib/Repair/InitializeSettings.php` calls `SettingsService::loadConfiguration()` which uses `ConfigurationService::importFromApp('procest')` from OpenRegister. Handles missing OpenRegister gracefully with warning. Is idempotent (REQ-OREG-002).
-- **Settings controller**: `lib/Controller/SettingsController.php` with routes `GET /api/settings` and `POST /api/settings` (REQ-OREG-003).
-- **Settings store**: `src/store/modules/settings.js` -- Pinia store that fetches and saves settings with loading/error state tracking.
-- **Object store**: `src/store/modules/object.js` -- uses `createObjectStore('object')` from `@conduction/nextcloud-vue` shared library. This is a **single unified store** rather than 12 individual stores as specified. The shared library provides CRUD, pagination, caching, `resolveReferences`, and `fetchSchema` functionality via plugins: `filesPlugin()`, `auditTrailsPlugin()`, `relationsPlugin()`.
-- **Frontend API patterns**: The object store queries OpenRegister via `/index.php/apps/openregister/api/objects/{register}/{schema}` endpoints (REQ-OREG-003).
-- **ZGW API layer**: Full ZGW-compliant API controllers exist: `ZrcController.php` (Zaken), `ZtcController.php` (Catalogi), `DrcController.php` (Documenten), `BrcController.php` (Besluiten), `NrcController.php` (Notificaties), `AcController.php` (Autorisaties) with ZGW-to-English mapping via `ZgwMappingService` (REQ-OREG-011 partial).
-- **ZGW business rules**: `lib/Service/ZgwBusinessRulesService.php`, `ZgwZrcRulesService.php`, `ZgwZtcRulesService.php`, `ZgwDrcRulesService.php`, `ZgwBrcRulesService.php` implement validation and cross-entity rules.
-- **ZGW auth middleware**: `lib/Middleware/ZgwAuthMiddleware.php` for JWT-based ZGW authentication.
-- **Audit trail**: The `auditTrailsPlugin()` in the object store integrates with OpenRegister's audit trail. ZGW controllers expose `/audittrail` sub-routes (REQ-OREG-010).
-- **Cross-entity references**: The `relationsPlugin()` in the object store supports resolving references. Case detail views resolve case types, status types, participants, and tasks (REQ-OREG-006).
-- **Case detail parallel loading**: `src/views/cases/CaseDetail.vue` fetches case, tasks, roles, and related data (REQ-OREG-012).
-- **Participants section**: `src/views/cases/components/ParticipantsSection.vue` resolves role types and participant display names via Nextcloud OCS API.
-- **Result section**: `src/views/cases/components/ResultSection.vue` resolves result types.
-
-**Not yet implemented or differs from spec:**
-- **12 individual Pinia stores**: The spec envisions `useCaseStore()`, `useTaskStore()`, `useRoleStore()`, etc. The actual implementation uses a **single `useObjectStore()`** with dynamic type registration via `@conduction/nextcloud-vue`. This is architecturally different but functionally equivalent.
-- **REQ-OREG-009: Cascade behaviors (V1)**: No cascade delete logic exists in the frontend or backend. Deleting a case does not automatically delete linked tasks/roles/results/decisions. Deleting a case type does not cascade to child type entities.
-- **REQ-OREG-007: Schema validation**: Validation is delegated to OpenRegister's schema validation. The frontend does client-side validation in `src/utils/caseValidation.js` and `src/utils/caseTypeValidation.js`, but server-side validation happens in OpenRegister, not in Procest.
-- **REQ-OREG-008: Concurrent modification (HTTP 409)**: Not implemented. No optimistic locking or conflict detection.
-- **Store caching (stale-while-revalidate)**: The shared library handles caching, but the specific behavior is not visible from the Procest codebase.
-
-### Standards & References
-
-- **OpenAPI 3.0.0**: The register configuration file follows this format.
-- **ZGW APIs (VNG Realisatie)**: Full ZGW-compliant API layer with Zaken (ZRC), Catalogi (ZTC), Documenten (DRC), Besluiten (BRC), Notificaties (NRC), and Autorisaties (AC) endpoints.
-- **CMMN 1.1**: Task lifecycle states follow the CasePlanModel/HumanTask pattern.
-- **Schema.org**: Entity type annotations in `procest_register.json`.
-- **Common Ground**: Layered architecture with data in OpenRegister (information layer) and Procest as process layer.
-
-### Specificity Assessment
-
-- **Mostly implementable as-is**, but the 12-store pattern conflicts with the actual architecture (single unified object store from `@conduction/nextcloud-vue`). The spec should be updated to reflect the shared library pattern or the implementation should diverge.
-- **Missing details:**
- - The spec does not mention the ZGW API layer, which is a major feature of the actual implementation.
- - Cascade behavior rules need concrete definition (which approach: cascade delete or prevent delete?).
- - RBAC enforcement details depend on OpenRegister's RBAC implementation, which is not specified here.
-- **Open questions:**
- - Should the spec be updated to match the single-store pattern, or should 12 individual stores be created?
- - How does ZGW field mapping (English to Dutch) interact with the OpenRegister schema definitions?
+# OpenRegister Integration Specification
+
+## Purpose
+
+Procest owns **no database tables**. All data is stored as OpenRegister objects in a dedicated `procest` register containing 12 schemas. This spec defines how the register and schemas are configured, how the repair step initializes the data model, how the frontend interacts with the OpenRegister API, the Pinia store patterns, cross-entity reference semantics, error handling, pagination, RBAC, cascade behaviors, and performance considerations.
+
+OpenRegister integration is the foundational layer upon which all other Procest features are built.
+
+**Standards**: OpenAPI 3.0.0 (schema format), OpenRegister API conventions
+**Feature tier**: MVP (foundation for all features)
+
+---
+
+## Architecture Overview
+
+```
+┌─────────────────────────────────────────────────┐
+│ Procest Frontend (Vue 2 + Pinia) │
+│ - Pinia stores per entity type │
+│ - API service layer with error handling │
+└──────────────┬──────────────────────────────────┘
+ │ REST API calls
+┌──────────────▼──────────────────────────────────┐
+│ OpenRegister API │
+│ /index.php/apps/openregister/api/objects/ │
+│ {register}/{schema}/{id} │
+│ - CRUD operations │
+│ - Search, pagination, filtering │
+│ - Schema validation │
+│ - RBAC enforcement │
+└──────────────┬──────────────────────────────────┘
+ │
+┌──────────────▼──────────────────────────────────┐
+│ OpenRegister Storage (PostgreSQL) │
+│ - JSON object storage │
+│ - Schema validation │
+│ - Audit trail │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+## Register and Schema Definitions
+
+### Register
+
+| Field | Value |
+|-------|-------|
+| Name | `procest` |
+| Slug | `procest` |
+| Description | Case management register for Procest |
+
+### Schema Inventory (12 schemas)
+
+The register MUST define exactly 12 schemas, organized into two groups:
+
+**Configuration schemas** (managed by admins, define case type behavior):
+
+| # | Schema | Purpose | CMMN/Schema.org | ZGW Equivalent |
+|---|--------|---------|-----------------|----------------|
+| 1 | `caseType` | Case type definition | CaseDefinition / CasePlanModel | ZaakType |
+| 2 | `statusType` | Status lifecycle phase per case type | Milestone | StatusType |
+| 3 | `resultType` | Case outcome type with archival rules | Case outcome | ResultaatType |
+| 4 | `roleType` | Participant role type per case type | schema:Role | RolType |
+| 5 | `propertyDefinition` | Custom field definition per case type | schema:PropertyValueSpecification | Eigenschap |
+| 6 | `documentType` | Document type requirement per case type | schema:DigitalDocument | InformatieObjectType |
+| 7 | `decisionType` | Decision type definition | schema:ChooseAction definition | BesluitType |
+
+**Instance schemas** (created by users during case operations):
+
+| # | Schema | Purpose | CMMN/Schema.org | ZGW Equivalent |
+|---|--------|---------|-----------------|----------------|
+| 8 | `case` | Case instance | CasePlanModel / schema:Project | Zaak |
+| 9 | `task` | Task within a case | HumanTask / schema:Action | (Taak) |
+| 10 | `role` | Role assignment on a case | schema:Role instance | Rol |
+| 11 | `result` | Case outcome record | Case result | Resultaat |
+| 12 | `decision` | Formal decision on a case | schema:ChooseAction instance | Besluit |
+
+---
+
+## Requirements
+
+### REQ-OREG-001: Configuration File
+
+**Tier**: MVP
+
+The system MUST define its register and all schemas in a JSON configuration file that follows the OpenAPI 3.0.0 format, consistent with the pattern used by `opencatalogi` and `softwarecatalog`.
+
+#### Scenario: Configuration file exists and is valid
+
+- GIVEN the Procest app source code
+- THEN the file `lib/Settings/procest_register.json` MUST exist
+- AND it MUST be valid JSON
+- AND it MUST conform to OpenAPI 3.0.0 format
+- AND it MUST define a register with slug `procest`
+- AND it MUST define exactly 12 schemas as listed in the schema inventory
+
+#### Scenario: Schema defines required properties for case
+
+- GIVEN the `case` schema definition in `procest_register.json`
+- THEN it MUST define the following required properties:
+ - `title` (string, max 255)
+ - `caseType` (string, format: uuid, reference to caseType)
+ - `status` (string, format: uuid, reference to statusType)
+ - `startDate` (string, format: date)
+- AND it MUST define the following optional properties:
+ - `description` (string)
+ - `identifier` (string, auto-generated)
+ - `result` (string, format: uuid, reference to result)
+ - `endDate` (string, format: date)
+ - `plannedEndDate` (string, format: date)
+ - `deadline` (string, format: date)
+ - `confidentiality` (string, enum)
+ - `assignee` (string)
+ - `priority` (string, enum: low, normal, high, urgent)
+ - `parentCase` (string, format: uuid)
+ - `relatedCases` (array of strings)
+ - `geometry` (object, GeoJSON)
+
+#### Scenario: Schema defines required properties for task
+
+- GIVEN the `task` schema definition in `procest_register.json`
+- THEN it MUST define:
+ - `title` (string, required)
+ - `status` (string, enum: available, active, completed, terminated, disabled, required, default: "available")
+ - `case` (string, format: uuid, required)
+ - `description` (string, optional)
+ - `assignee` (string, optional)
+ - `dueDate` (string, format: date-time, optional)
+ - `priority` (string, enum: low, normal, high, urgent, optional, default: "normal")
+ - `completedDate` (string, format: date-time, optional)
+
+#### Scenario: Schema defines required properties for role
+
+- GIVEN the `role` schema definition
+- THEN it MUST define:
+ - `name` (string, required)
+ - `roleType` (string, format: uuid, required)
+ - `case` (string, format: uuid, required)
+ - `participant` (string, required)
+ - `description` (string, optional)
+
+#### Scenario: Schema defines required properties for result
+
+- GIVEN the `result` schema definition
+- THEN it MUST define:
+ - `name` (string, required)
+ - `case` (string, format: uuid, required)
+ - `resultType` (string, format: uuid, required)
+ - `description` (string, optional)
+
+#### Scenario: Schema defines required properties for decision
+
+- GIVEN the `decision` schema definition
+- THEN it MUST define:
+ - `title` (string, required)
+ - `case` (string, format: uuid, required)
+ - `description` (string, optional)
+ - `decisionType` (string, format: uuid, optional)
+ - `decidedBy` (string, optional)
+ - `decidedAt` (string, format: date-time, optional)
+ - `effectiveDate` (string, format: date, optional)
+ - `expiryDate` (string, format: date, optional)
+
+#### Scenario: Schema defines caseType with all behavioral fields
+
+- GIVEN the `caseType` schema definition
+- THEN it MUST define at minimum:
+ - `title` (string, required)
+ - `description` (string, optional)
+ - `identifier` (string, auto)
+ - `purpose` (string, required)
+ - `trigger` (string, required)
+ - `subject` (string, required)
+ - `processingDeadline` (string, ISO 8601 duration, required)
+ - `confidentiality` (string, enum, required)
+ - `isDraft` (boolean, default: true)
+ - `validFrom` (string, format: date, required)
+ - `validUntil` (string, format: date, optional)
+ - `origin` (string, enum: internal, external, required)
+ - `suspensionAllowed` (boolean, required)
+ - `extensionAllowed` (boolean, required)
+ - `publicationRequired` (boolean, required)
+
+#### Scenario: All schemas include type annotations
+
+- GIVEN each schema definition
+- THEN each MUST include a `@type` property or annotation referencing the appropriate standard:
+ - `case`: `schema:Project`
+ - `task`: `schema:Action`
+ - `role`: `schema:Role`
+ - `result`: (no standard type, app-specific)
+ - `decision`: `schema:ChooseAction`
+ - `caseType`: `schema:Project` definition
+ - `statusType`: `schema:ActionStatusType`
+ - `roleType`: `schema:Role` definition
+ - `propertyDefinition`: `schema:PropertyValueSpecification`
+ - `documentType`: `schema:DigitalDocument`
+ - `decisionType`: `schema:ChooseAction` definition
+ - `resultType`: (no standard type)
+
+---
+
+### REQ-OREG-002: Auto-Configuration on Install (Repair Step)
+
+**Tier**: MVP
+
+The system MUST import the register configuration during app installation and upgrades via the Nextcloud repair step mechanism.
+
+#### Scenario: First install creates register and all schemas
+
+- GIVEN Procest is being installed for the first time on a Nextcloud instance with OpenRegister
+- WHEN the repair step `lib/Migration/ImportConfiguration.php` runs
+- THEN it MUST call `ConfigurationService::importFromApp('procest')`
+- AND the `procest` register MUST be created in OpenRegister
+- AND all 12 schemas MUST be created with their property definitions
+- AND the repair step MUST log success or failure
+
+#### Scenario: Upgrade adds new schemas without data loss
+
+- GIVEN Procest was previously installed with 10 schemas (before decisionType and propertyDefinition were added)
+- AND existing cases, tasks, and roles exist in the register
+- WHEN the repair step runs during upgrade
+- THEN the 2 new schemas (`decisionType`, `propertyDefinition`) MUST be created
+- AND existing schemas MUST be updated if their definitions changed (new properties added)
+- AND existing objects in unchanged schemas MUST NOT be modified or deleted
+- AND no data loss MUST occur
+
+#### Scenario: Repair step is idempotent
+
+- GIVEN the repair step has already run successfully
+- WHEN the repair step runs again (e.g., during `occ maintenance:repair`)
+- THEN it MUST NOT create duplicate registers or schemas
+- AND existing data MUST remain intact
+- AND the operation MUST complete without errors
+
+#### Scenario: Repair step handles missing OpenRegister gracefully
+
+- GIVEN Procest is installed but OpenRegister is NOT installed
+- WHEN the repair step runs
+- THEN it MUST log a clear error message indicating that OpenRegister is required
+- AND the repair step MUST NOT crash or throw an unhandled exception
+- AND Procest MUST indicate to the admin that OpenRegister needs to be installed
+
+#### Scenario: Schema property additions are non-destructive
+
+- GIVEN the `task` schema previously had 6 properties
+- AND the upgrade adds 2 new optional properties (e.g., `checklist`, `blockedBy`)
+- WHEN the repair step updates the schema
+- THEN the 2 new properties MUST be added to the schema
+- AND existing task objects MUST remain valid (new properties are optional)
+- AND existing task objects MUST NOT have the new properties set to any default
+
+---
+
+### REQ-OREG-003: Frontend API Interaction Patterns
+
+**Tier**: MVP
+
+The frontend MUST interact with OpenRegister's REST API for all CRUD operations. All API calls MUST follow consistent URL patterns and error handling.
+
+#### Scenario: Base URL pattern
+
+- GIVEN the Procest frontend needs to access OpenRegister
+- THEN all API calls MUST use the base URL pattern: `/index.php/apps/openregister/api/objects/procest/{schema}`
+- AND for single objects: `/index.php/apps/openregister/api/objects/procest/{schema}/{uuid}`
+
+#### Scenario: List all cases (GET collection)
+
+- GIVEN the `case` schema exists in the `procest` register with 24 case objects
+- WHEN the frontend requests the case list
+- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/case`
+- AND the response MUST include:
+ - An array of case objects
+ - Pagination metadata (`total`, `page`, `limit`, `pages`)
+- AND the default page size MUST be configurable (e.g., 20)
+
+#### Scenario: Get a single case (GET object)
+
+- GIVEN a case with UUID "abc-123-def" exists
+- WHEN the frontend requests the case detail
+- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/case/abc-123-def`
+- AND the response MUST include all case properties
+
+#### Scenario: Create a new case (POST)
+
+- GIVEN the user fills in the new case form with:
+ - title: "Bouwvergunning Prinsengracht 200"
+ - caseType: "casetype-uuid-omgevings"
+ - startDate: "2026-03-01"
+- WHEN the user submits the form
+- THEN the frontend MUST call `POST /index.php/apps/openregister/api/objects/procest/case`
+- AND the request body MUST contain the case properties as JSON
+- AND the response MUST include the created object with its generated UUID
+
+#### Scenario: Update an existing case (PUT)
+
+- GIVEN an existing case with UUID "abc-123-def"
+- WHEN the user updates the description
+- THEN the frontend MUST call `PUT /index.php/apps/openregister/api/objects/procest/case/abc-123-def`
+- AND the request body MUST contain the full updated object
+- AND the response MUST include the updated object
+
+#### Scenario: Delete a case (DELETE)
+
+- GIVEN an existing case with UUID "abc-123-def"
+- WHEN the user deletes the case
+- THEN the frontend MUST call `DELETE /index.php/apps/openregister/api/objects/procest/case/abc-123-def`
+- AND the response MUST confirm deletion (HTTP 200 or 204)
+
+#### Scenario: API call with authentication
+
+- GIVEN a logged-in Nextcloud user
+- THEN all OpenRegister API calls MUST include the Nextcloud session cookie or authorization header
+- AND unauthenticated requests MUST be rejected with HTTP 401
+
+---
+
+### REQ-OREG-004: Pagination and Filtering
+
+**Tier**: MVP
+
+The frontend MUST support paginated access to object lists and use OpenRegister query parameters for filtering, searching, and sorting.
+
+#### Scenario: Paginate case list
+
+- GIVEN 24 cases exist in the register
+- WHEN the frontend requests page 2 with limit 10
+- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/case?_page=2&_limit=10`
+- AND the response MUST contain cases 11-20
+- AND the pagination metadata MUST show: `total: 24`, `page: 2`, `limit: 10`, `pages: 3`
+
+#### Scenario: Filter cases by status
+
+- GIVEN cases with various status references
+- WHEN the frontend filters by a specific status type UUID
+- THEN it MUST include the filter as a query parameter: `?status=statustype-uuid-inbehandeling`
+- AND only cases matching that status MUST be returned
+
+#### Scenario: Filter tasks by case
+
+- GIVEN 23 tasks across 8 cases
+- WHEN the frontend requests tasks for case #2024-042 (UUID: "case-uuid-042")
+- THEN it MUST call `GET /index.php/apps/openregister/api/objects/procest/task?case=case-uuid-042`
+- AND only tasks linked to that case MUST be returned
+
+#### Scenario: Filter tasks by assignee
+
+- GIVEN tasks assigned to various users
+- WHEN the frontend filters by assignee "jan.devries"
+- THEN it MUST include `?assignee=jan.devries` in the query
+- AND only tasks assigned to Jan MUST be returned
+
+#### Scenario: Search by text field
+
+- GIVEN cases with various titles
+- WHEN the user searches for "bouwvergunning"
+- THEN the frontend MUST pass the search term via the appropriate OpenRegister search parameter
+- AND results MUST include cases whose title contains "bouwvergunning" (case-insensitive)
+
+#### Scenario: Sort by field
+
+- GIVEN the task list is displayed
+- WHEN the user sorts by due date ascending
+- THEN the frontend MUST include `?_sort=dueDate&_order=asc` in the query
+- AND the API response MUST return tasks ordered by due date ascending
+
+#### Scenario: Combined filters
+
+- GIVEN the user applies multiple filters: assignee "jan.devries", status "active", sorted by priority
+- THEN the frontend MUST combine all filters: `?assignee=jan.devries&status=active&_sort=priority&_order=desc`
+- AND the API MUST apply all filters conjunctively (AND logic)
+
+---
+
+### REQ-OREG-005: Pinia Store Patterns
+
+**Tier**: MVP
+
+The frontend MUST use Pinia stores for state management, with one store per entity type. Stores MUST follow a consistent pattern for CRUD actions, loading states, error handling, and pagination.
+
+#### Scenario: Case store provides standard CRUD actions
+
+- GIVEN the `useCaseStore()` Pinia store
+- THEN it MUST provide the following actions:
+ - `fetchCases(params?)` -- list with optional filter/pagination params
+ - `fetchCase(id)` -- get single case by UUID
+ - `createCase(data)` -- create new case
+ - `updateCase(id, data)` -- update existing case
+ - `deleteCase(id)` -- delete case
+- AND each action MUST construct the correct OpenRegister API URL
+- AND each action MUST handle loading states and errors
+
+#### Scenario: Store tracks loading state
+
+- GIVEN the case store
+- WHEN `fetchCases()` is called
+- THEN `store.loading` MUST be set to `true` before the API call
+- AND `store.loading` MUST be set to `false` after the API call completes (success or failure)
+- AND the UI MUST show a loading indicator while `store.loading` is `true`
+
+#### Scenario: Store tracks error state
+
+- GIVEN the case store
+- WHEN an API call fails with HTTP 500
+- THEN `store.error` MUST be set to an error object containing the status code and message
+- AND the UI MUST display a user-friendly error message
+- AND `store.loading` MUST be set to `false`
+
+#### Scenario: Store handles pagination state
+
+- GIVEN the case store fetches a paginated list
+- THEN the store state MUST include:
+ - `items` -- array of case objects for the current page
+ - `total` -- total number of matching cases
+ - `page` -- current page number
+ - `limit` -- items per page
+ - `pages` -- total number of pages
+- AND the store MUST provide a `fetchPage(page)` action that fetches a specific page
+
+#### Scenario: Task store follows the same pattern
+
+- GIVEN the `useTaskStore()` Pinia store
+- THEN it MUST provide: `fetchTasks(params?)`, `fetchTask(id)`, `createTask(data)`, `updateTask(id, data)`, `deleteTask(id)`
+- AND it MUST follow the same loading/error/pagination pattern as the case store
+
+#### Scenario: All entity types have stores
+
+- GIVEN the Procest frontend
+- THEN Pinia stores MUST exist for all 12 entity types:
+ - `useCaseStore()`, `useTaskStore()`, `useRoleStore()`, `useResultStore()`, `useDecisionStore()`
+ - `useCaseTypeStore()`, `useStatusTypeStore()`, `useResultTypeStore()`, `useRoleTypeStore()`
+ - `usePropertyDefinitionStore()`, `useDocumentTypeStore()`, `useDecisionTypeStore()`
+- AND each store MUST follow the same CRUD + loading + error + pagination pattern
+
+#### Scenario: Store caches fetched data
+
+- GIVEN the case store has already fetched case "abc-123-def"
+- WHEN `fetchCase("abc-123-def")` is called again within the same session
+- THEN the store SHOULD return the cached version immediately
+- AND the store MAY optionally refetch in the background (stale-while-revalidate)
+
+---
+
+### REQ-OREG-006: Cross-Entity References
+
+**Tier**: MVP
+
+Entities in Procest reference each other via UUID. The frontend MUST resolve these references to display meaningful data (titles, names) rather than raw UUIDs.
+
+#### Scenario: Task references a case
+
+- GIVEN a task object with `case: "case-uuid-042"`
+- WHEN the task is displayed in a list or card
+- THEN the frontend MUST resolve "case-uuid-042" to display the case identifier and title (e.g., "Case #2024-042 Bouwvergunning Keizersgracht")
+- AND the resolved case reference MUST be clickable, navigating to the case detail
+
+#### Scenario: Case references a case type
+
+- GIVEN a case object with `caseType: "casetype-uuid-omgevings"`
+- WHEN the case is displayed in the case list
+- THEN the frontend MUST resolve the case type to display its title (e.g., "Omgevingsvergunning")
+
+#### Scenario: Role references both case and role type
+
+- GIVEN a role object with:
+ - `case: "case-uuid-042"`
+ - `roleType: "roletype-uuid-handler"`
+ - `participant: "jan.devries"`
+- WHEN the role is displayed on the case detail page
+- THEN the frontend MUST resolve:
+ - The role type to its name (e.g., "Behandelaar")
+ - The participant to the Nextcloud user display name (e.g., "Jan de Vries")
+ - The case reference to the case title (if displayed outside case context)
+
+#### Scenario: Result references a result type
+
+- GIVEN a result object with `resultType: "resulttype-uuid-granted"`
+- WHEN the result is displayed
+- THEN the frontend MUST resolve the result type to its name (e.g., "Vergunning verleend")
+- AND the archival information from the result type SHOULD be accessible
+
+#### Scenario: Case type hierarchy resolution
+
+- GIVEN a case detail view that needs to display:
+ - The case type name
+ - The current status name (from status type)
+ - The handler name (from role)
+ - Task list (from tasks referencing this case)
+- WHEN the case detail page loads
+- THEN the frontend MUST fetch and resolve all related entities
+- AND cross-references MUST be resolved in parallel where possible
+
+#### Scenario: Dangling reference (referenced object deleted)
+
+- GIVEN a task with `case: "case-uuid-deleted"` where the referenced case has been deleted
+- WHEN the task is displayed
+- THEN the frontend MUST handle the missing reference gracefully
+- AND it SHOULD display a "Case not found" or "[Deleted]" placeholder
+- AND the task MUST still be viewable and manageable
+
+---
+
+### REQ-OREG-007: Schema Validation Rules
+
+**Tier**: MVP
+
+OpenRegister MUST validate objects against their schema definitions before storage. Procest schemas MUST define appropriate validation constraints.
+
+#### Scenario: Required field validation
+
+- GIVEN the `task` schema requires `title` and `case`
+- WHEN the frontend submits a task without a title
+- THEN the OpenRegister API MUST return HTTP 400/422 with a validation error
+- AND the error response MUST identify the missing field (`title`)
+- AND the frontend MUST display the validation error to the user
+
+#### Scenario: Enum validation for task status
+
+- GIVEN the `task` schema defines `status` as enum: `available`, `active`, `completed`, `terminated`, `disabled`
+- WHEN the frontend submits a task with `status: "pending"`
+- THEN the OpenRegister API MUST reject the request
+- AND the error MUST indicate that "pending" is not a valid value for `status`
+
+#### Scenario: Enum validation for priority
+
+- GIVEN the `task` schema defines `priority` as enum: `low`, `normal`, `high`, `urgent`
+- WHEN the frontend submits a task with `priority: "critical"`
+- THEN the API MUST reject with a validation error
+
+#### Scenario: Date format validation
+
+- GIVEN the `case` schema defines `startDate` as format: date
+- WHEN the frontend submits a case with `startDate: "not-a-date"`
+- THEN the API MUST reject with a format validation error
+
+#### Scenario: UUID reference format validation
+
+- GIVEN the `task` schema defines `case` as format: uuid
+- WHEN the frontend submits a task with `case: "not-a-uuid"`
+- THEN the API MUST reject with a format validation error
+
+#### Scenario: String length validation
+
+- GIVEN the `case` schema defines `title` with maxLength: 255
+- WHEN the frontend submits a case with a title of 300 characters
+- THEN the API MUST reject with a length validation error
+
+---
+
+### REQ-OREG-008: Error Handling
+
+**Tier**: MVP
+
+The frontend MUST handle all categories of API errors gracefully and present user-friendly messages.
+
+#### Scenario: Network error (offline/timeout)
+
+- GIVEN the user is creating a case
+- WHEN the API call fails due to a network timeout
+- THEN the frontend MUST display a message like "Unable to reach the server. Please check your connection and try again."
+- AND the form data MUST be preserved (not cleared)
+- AND a retry option SHOULD be available
+
+#### Scenario: Validation error (HTTP 400/422)
+
+- GIVEN the user submits a case with missing required fields
+- WHEN the API returns HTTP 422 with field-level errors
+- THEN the frontend MUST map errors to specific form fields
+- AND invalid fields MUST be highlighted with their error messages
+- AND the form MUST remain editable for correction
+
+#### Scenario: Authorization error (HTTP 403)
+
+- GIVEN a user without admin privileges
+- WHEN they attempt to create a case type via the API
+- THEN the API MUST return HTTP 403
+- AND the frontend MUST display "You do not have permission to perform this action"
+
+#### Scenario: Not found error (HTTP 404)
+
+- GIVEN a case with UUID "abc-123-def" has been deleted
+- WHEN the frontend attempts to fetch it
+- THEN the API MUST return HTTP 404
+- AND the frontend MUST display "The requested case could not be found"
+- AND the frontend SHOULD redirect to the case list
+
+#### Scenario: Server error (HTTP 500)
+
+- GIVEN an unexpected error occurs on the server
+- WHEN the API returns HTTP 500
+- THEN the frontend MUST display a generic error message: "An unexpected error occurred. Please try again later."
+- AND the error SHOULD be logged to the browser console with details for debugging
+
+#### Scenario: Concurrent modification conflict (HTTP 409)
+
+- GIVEN two users are editing the same case simultaneously
+- WHEN user A saves after user B has already saved
+- THEN the API SHOULD return HTTP 409 (conflict)
+- AND the frontend MUST inform user A that the case was modified by another user
+- AND the frontend SHOULD offer to reload the latest version
+
+---
+
+### REQ-OREG-009: Cascade Behaviors
+
+**Tier**: V1
+
+The system MUST define what happens to dependent entities when a parent entity is deleted or modified.
+
+#### Scenario: Delete a case with linked tasks, roles, results, and decisions
+
+- GIVEN case #2024-042 has:
+ - 5 tasks
+ - 3 roles
+ - 1 result
+ - 2 decisions
+- WHEN the user deletes case #2024-042
+- THEN the system MUST either:
+ - (a) Cascade delete all linked tasks, roles, results, and decisions, OR
+ - (b) Prevent deletion and warn the user that dependent entities exist
+- AND the system MUST NOT leave orphaned task/role/result/decision objects
+- AND the chosen behavior MUST be consistent
+
+#### Scenario: Delete a case type that is in use
+
+- GIVEN case type "Omgevingsvergunning" is referenced by 10 active cases
+- WHEN an admin attempts to delete the case type
+- THEN the system MUST prevent the deletion
+- AND the error message MUST indicate that the case type is in use by 10 cases
+- AND the admin SHOULD be advised to set the case type as draft or set a `validUntil` date instead
+
+#### Scenario: Delete a case type that is not in use
+
+- GIVEN case type "Bezwaarschrift" (draft, no cases reference it)
+- WHEN an admin deletes the case type
+- THEN the case type MUST be deleted
+- AND all linked status types, result types, role types, property definitions, document types, and decision types MUST also be deleted (cascade)
+
+#### Scenario: Remove a status type from a case type
+
+- GIVEN case type "Omgevingsvergunning" has 4 status types
+- AND status type "Besluitvorming" (order: 3) is being removed
+- AND 3 cases currently have status "Besluitvorming"
+- THEN the system MUST prevent removal
+- AND the error message MUST indicate that 3 cases are currently in this status
+
+#### Scenario: Remove an unused status type
+
+- GIVEN status type "Verouderde status" is linked to case type "Omgevingsvergunning"
+- AND no cases currently reference this status type
+- WHEN the admin removes it
+- THEN the status type MUST be deleted
+- AND the remaining status types MUST maintain their order (reorder if needed)
+
+---
+
+### REQ-OREG-010: Audit Trail Integration
+
+**Tier**: MVP
+
+All create, update, and delete operations on Procest objects MUST be captured in the audit trail.
+
+#### Scenario: Case creation is logged
+
+- GIVEN user "jan.devries" creates case #2024-053
+- THEN the audit trail MUST record:
+ - Action: "created"
+ - Entity type: "case"
+ - Entity UUID
+ - User: "jan.devries"
+ - Timestamp
+ - Key field values (title, caseType)
+
+#### Scenario: Task status change is logged
+
+- GIVEN user "jan.devries" changes task "Review documenten" from `active` to `completed`
+- THEN the audit trail MUST record:
+ - Action: "status_changed"
+ - Entity type: "task"
+ - Entity UUID
+ - User: "jan.devries"
+ - Old value: "active"
+ - New value: "completed"
+ - Timestamp
+
+#### Scenario: Role assignment is logged
+
+- GIVEN a coordinator assigns "maria.bakker" as advisor on case #2024-042
+- THEN the audit trail MUST record:
+ - Action: "role_assigned"
+ - Entity type: "role"
+ - Case reference
+ - Participant: "maria.bakker"
+ - Role type: "Advisor"
+ - Timestamp
+
+#### Scenario: Decision creation is logged
+
+- GIVEN "dr.k.bakker" records a decision on case #2024-042
+- THEN the audit trail MUST record the decision creation with all key fields
+
+#### Scenario: Audit trail is displayed on case detail
+
+- GIVEN case #2024-042 has 15 audit events
+- WHEN the user views the Activity Timeline section on the case detail
+- THEN the events MUST be displayed in reverse chronological order
+- AND each event MUST show: description, user, timestamp
+- AND the timeline MUST be paginated or have a "Load more" option
+
+---
+
+### REQ-OREG-011: RBAC (Role-Based Access Control)
+
+**Tier**: MVP
+
+The system MUST enforce access control via OpenRegister's RBAC system. Configuration entities (case types, status types, etc.) MUST be admin-only. Instance entities (cases, tasks, roles, results, decisions) MUST be accessible to authorized users.
+
+#### Scenario: Admin-only access to case type management
+
+- GIVEN a non-admin user "jan.devries"
+- WHEN Jan attempts to create, update, or delete a case type via the API
+- THEN the system MUST return HTTP 403
+- AND the operation MUST NOT be performed
+
+#### Scenario: Admin can manage all configuration entities
+
+- GIVEN an admin user "admin"
+- THEN the admin MUST be able to CRUD all 7 configuration schemas:
+ - caseType, statusType, resultType, roleType, propertyDefinition, documentType, decisionType
+- AND the admin settings page in Nextcloud MUST provide the management UI
+
+#### Scenario: Regular user can create cases and tasks
+
+- GIVEN a regular Nextcloud user "jan.devries"
+- THEN Jan MUST be able to:
+ - Create cases (POST to case schema)
+ - Create tasks on cases he has access to
+ - Create roles on cases he has access to
+ - Record results on cases he is handler for
+ - Create decisions on cases he has access to
+
+#### Scenario: User can only see cases they have access to
+
+- GIVEN OpenRegister RBAC is configured
+- WHEN "jan.devries" requests the case list
+- THEN the API MUST return only cases that Jan has permission to view
+- AND cases assigned to other users/organizations that Jan has no role in MUST NOT be returned
+
+#### Scenario: Nextcloud admin settings page requires admin
+
+- GIVEN a non-admin user navigates to the Procest admin settings URL
+- THEN the Nextcloud admin settings system MUST prevent access
+- AND the user MUST be redirected or shown an "access denied" page
+
+---
+
+### REQ-OREG-012: Performance and Eager Loading
+
+**Tier**: MVP
+
+The frontend MUST minimize API round-trips by fetching related entities efficiently.
+
+#### Scenario: Case detail page loads all related data in parallel
+
+- GIVEN the user opens case detail for case #2024-042
+- THEN the frontend MUST fetch the following in parallel (not sequentially):
+ - Case object (with case type, status references)
+ - Tasks for the case (`?case=case-uuid-042`)
+ - Roles for the case (`?case=case-uuid-042`)
+ - Decisions for the case (`?case=case-uuid-042`)
+ - Result for the case (if exists)
+- AND the total load time MUST be under 3 seconds for a case with 10 tasks, 5 roles, 3 decisions
+
+#### Scenario: Case list resolves case type names efficiently
+
+- GIVEN the case list shows 20 cases referencing 4 different case types
+- THEN the frontend MUST NOT make 20 individual API calls to resolve case type names
+- AND instead MUST fetch all relevant case types in a single call (or use the cached case type store)
+- AND the case type store SHOULD pre-fetch all case types on app initialization (small dataset, typically less than 20)
+
+#### Scenario: Status type resolution is cached
+
+- GIVEN case types have between 3-6 status types each
+- WHEN the case list or detail page needs to display status names
+- THEN status types MUST be fetched once per case type and cached in the Pinia store
+- AND subsequent accesses MUST use the cached data
+
+#### Scenario: My Work aggregation performance
+
+- GIVEN the My Work view needs to display cases and tasks for the current user
+- THEN the frontend MUST make exactly 2 API calls:
+ - Cases with `?assignee=currentUser&status_ne=final` (non-final cases assigned to user)
+ - Tasks with `?assignee=currentUser&status=available,active` (active/available tasks)
+- AND the results MUST be merged and sorted client-side
+- AND the total load time MUST be under 2 seconds
+
+#### Scenario: Pagination prevents loading too many objects
+
+- GIVEN the case list could contain hundreds of cases
+- THEN the default page size MUST NOT exceed 50
+- AND the frontend MUST use pagination (not load all objects at once)
+- AND lazy loading or virtual scrolling SHOULD be used for long lists
+
+---
+
+### REQ-OREG-013: Cross-Entity Reference Map
+
+**Tier**: MVP
+
+For implementation clarity, this is the complete reference map showing how entities relate to each other.
+
+```
+CaseType ─────────────────────────────────────────────────────────────┐
+│ │
+├── StatusType[] (statusType.caseType → caseType UUID) │
+├── ResultType[] (resultType.caseType → caseType UUID) │
+├── RoleType[] (roleType.caseType → caseType UUID) │
+├── PropertyDefinition[] (propertyDefinition.caseType → caseType UUID)│
+├── DocumentType[] (documentType.caseType → caseType UUID) │
+└── DecisionType[] (decisionType.caseType → caseType UUID) │
+ │
+Case ─────────────────────────────────────────────────────────────────┤
+│ case.caseType → caseType UUID │
+│ case.status → statusType UUID │
+│ case.result → result UUID (optional) │
+│ case.assignee → Nextcloud user UID (optional) │
+│ case.parentCase → case UUID (optional, for sub-cases) │
+│ │
+├── Task[] (task.case → case UUID) │
+│ task.assignee → Nextcloud user UID (optional) │
+│ │
+├── Role[] (role.case → case UUID) │
+│ role.roleType → roleType UUID │
+│ role.participant → Nextcloud user UID or contact ref │
+│ │
+├── Result (result.case → case UUID, at most 1) │
+│ result.resultType → resultType UUID │
+│ │
+└── Decision[] (decision.case → case UUID) │
+ decision.decisionType → decisionType UUID (optional) │
+ decision.decidedBy → Nextcloud user UID (optional) │
+```
+
+#### Scenario: Verify reference integrity on task creation
+
+- GIVEN a user creates a task with `case: "case-uuid-042"`
+- THEN the system SHOULD verify that case "case-uuid-042" exists in the register
+- AND if the referenced case does not exist, the creation SHOULD be rejected
+
+#### Scenario: Verify role type belongs to the correct case type
+
+- GIVEN a user creates a role on case #2024-042 (caseType: "Omgevingsvergunning")
+- AND the user specifies roleType UUID for "Klager" which belongs to case type "Klacht"
+- THEN the system SHOULD reject the role creation
+- AND the error MUST indicate that the role type does not belong to the case's case type
+
+#### Scenario: Case type deletion cascades to child types
+
+- GIVEN case type "Bezwaarschrift" has 3 status types, 2 result types, and 2 role types
+- AND no cases reference this case type
+- WHEN the admin deletes the case type
+- THEN all 3 status types, 2 result types, and 2 role types MUST also be deleted
+
+---
+
+## Summary: API Endpoint Patterns
+
+| Entity | List | Get | Create | Update | Delete |
+|--------|------|-----|--------|--------|--------|
+| Case | `GET .../procest/case` | `GET .../procest/case/{id}` | `POST .../procest/case` | `PUT .../procest/case/{id}` | `DELETE .../procest/case/{id}` |
+| Task | `GET .../procest/task` | `GET .../procest/task/{id}` | `POST .../procest/task` | `PUT .../procest/task/{id}` | `DELETE .../procest/task/{id}` |
+| Role | `GET .../procest/role` | `GET .../procest/role/{id}` | `POST .../procest/role` | `PUT .../procest/role/{id}` | `DELETE .../procest/role/{id}` |
+| Result | `GET .../procest/result` | `GET .../procest/result/{id}` | `POST .../procest/result` | `PUT .../procest/result/{id}` | `DELETE .../procest/result/{id}` |
+| Decision | `GET .../procest/decision` | `GET .../procest/decision/{id}` | `POST .../procest/decision` | `PUT .../procest/decision/{id}` | `DELETE .../procest/decision/{id}` |
+| CaseType | `GET .../procest/caseType` | `GET .../procest/caseType/{id}` | `POST .../procest/caseType` | `PUT .../procest/caseType/{id}` | `DELETE .../procest/caseType/{id}` |
+| StatusType | `GET .../procest/statusType` | `GET .../procest/statusType/{id}` | `POST .../procest/statusType` | `PUT .../procest/statusType/{id}` | `DELETE .../procest/statusType/{id}` |
+| ResultType | `GET .../procest/resultType` | `GET .../procest/resultType/{id}` | `POST .../procest/resultType` | `PUT .../procest/resultType/{id}` | `DELETE .../procest/resultType/{id}` |
+| RoleType | `GET .../procest/roleType` | `GET .../procest/roleType/{id}` | `POST .../procest/roleType` | `PUT .../procest/roleType/{id}` | `DELETE .../procest/roleType/{id}` |
+| PropDef | `GET .../procest/propertyDefinition` | `GET .../procest/propertyDefinition/{id}` | `POST ...` | `PUT ...` | `DELETE ...` |
+| DocType | `GET .../procest/documentType` | `GET .../procest/documentType/{id}` | `POST ...` | `PUT ...` | `DELETE ...` |
+| DecisionType | `GET .../procest/decisionType` | `GET .../procest/decisionType/{id}` | `POST ...` | `PUT ...` | `DELETE ...` |
+
+Base URL: `/index.php/apps/openregister/api/objects`
+
+---
+
+## Pinia Store Inventory
+
+| Store | Entity | Key Extra Features |
+|-------|--------|-------------------|
+| `useCaseStore()` | case | Resolves caseType and status names; My Work filtering |
+| `useTaskStore()` | task | Kanban grouping by status; overdue calculation |
+| `useRoleStore()` | role | Resolves participant display names from Nextcloud |
+| `useResultStore()` | result | Links to resultType for archival info |
+| `useDecisionStore()` | decision | Validity period calculations |
+| `useCaseTypeStore()` | caseType | Cached on app init; used by all case views |
+| `useStatusTypeStore()` | statusType | Ordered by `order`; cached per case type |
+| `useResultTypeStore()` | resultType | Filtered by caseType |
+| `useRoleTypeStore()` | roleType | Filtered by caseType |
+| `usePropertyDefinitionStore()` | propertyDefinition | Filtered by caseType |
+| `useDocumentTypeStore()` | documentType | Filtered by caseType |
+| `useDecisionTypeStore()` | decisionType | Filtered by caseType (V1) |
+
+---
+
+### Current Implementation Status
+
+**Core architecture implemented; individual entity stores differ from spec.**
+
+**Implemented (with file paths):**
+- **Configuration file**: `lib/Settings/procest_register.json` exists, is valid JSON, conforms to OpenAPI 3.0.0, defines a register with app `procest`. Defines 12 schemas: `caseType`, `statusType`, `resultType`, `roleType`, `propertyDefinition`, `documentType`, `decisionType`, `case`, `task`, `role`, `result`, `decision`. Each schema includes `x-schema-org-type` and `x-zgw-equivalent` annotations (REQ-OREG-001).
+- **Repair step**: `lib/Repair/InitializeSettings.php` calls `SettingsService::loadConfiguration()` which uses `ConfigurationService::importFromApp('procest')` from OpenRegister. Handles missing OpenRegister gracefully with warning. Is idempotent (REQ-OREG-002).
+- **Settings controller**: `lib/Controller/SettingsController.php` with routes `GET /api/settings` and `POST /api/settings` (REQ-OREG-003).
+- **Settings store**: `src/store/modules/settings.js` -- Pinia store that fetches and saves settings with loading/error state tracking.
+- **Object store**: `src/store/modules/object.js` -- uses `createObjectStore('object')` from `@conduction/nextcloud-vue` shared library. This is a **single unified store** rather than 12 individual stores as specified. The shared library provides CRUD, pagination, caching, `resolveReferences`, and `fetchSchema` functionality via plugins: `filesPlugin()`, `auditTrailsPlugin()`, `relationsPlugin()`.
+- **Frontend API patterns**: The object store queries OpenRegister via `/index.php/apps/openregister/api/objects/{register}/{schema}` endpoints (REQ-OREG-003).
+- **ZGW API layer**: Full ZGW-compliant API controllers exist: `ZrcController.php` (Zaken), `ZtcController.php` (Catalogi), `DrcController.php` (Documenten), `BrcController.php` (Besluiten), `NrcController.php` (Notificaties), `AcController.php` (Autorisaties) with ZGW-to-English mapping via `ZgwMappingService` (REQ-OREG-011 partial).
+- **ZGW business rules**: `lib/Service/ZgwBusinessRulesService.php`, `ZgwZrcRulesService.php`, `ZgwZtcRulesService.php`, `ZgwDrcRulesService.php`, `ZgwBrcRulesService.php` implement validation and cross-entity rules.
+- **ZGW auth middleware**: `lib/Middleware/ZgwAuthMiddleware.php` for JWT-based ZGW authentication.
+- **Audit trail**: The `auditTrailsPlugin()` in the object store integrates with OpenRegister's audit trail. ZGW controllers expose `/audittrail` sub-routes (REQ-OREG-010).
+- **Cross-entity references**: The `relationsPlugin()` in the object store supports resolving references. Case detail views resolve case types, status types, participants, and tasks (REQ-OREG-006).
+- **Case detail parallel loading**: `src/views/cases/CaseDetail.vue` fetches case, tasks, roles, and related data (REQ-OREG-012).
+- **Participants section**: `src/views/cases/components/ParticipantsSection.vue` resolves role types and participant display names via Nextcloud OCS API.
+- **Result section**: `src/views/cases/components/ResultSection.vue` resolves result types.
+
+**Not yet implemented or differs from spec:**
+- **12 individual Pinia stores**: The spec envisions `useCaseStore()`, `useTaskStore()`, `useRoleStore()`, etc. The actual implementation uses a **single `useObjectStore()`** with dynamic type registration via `@conduction/nextcloud-vue`. This is architecturally different but functionally equivalent.
+- **REQ-OREG-009: Cascade behaviors (V1)**: No cascade delete logic exists in the frontend or backend. Deleting a case does not automatically delete linked tasks/roles/results/decisions. Deleting a case type does not cascade to child type entities.
+- **REQ-OREG-007: Schema validation**: Validation is delegated to OpenRegister's schema validation. The frontend does client-side validation in `src/utils/caseValidation.js` and `src/utils/caseTypeValidation.js`, but server-side validation happens in OpenRegister, not in Procest.
+- **REQ-OREG-008: Concurrent modification (HTTP 409)**: Not implemented. No optimistic locking or conflict detection.
+- **Store caching (stale-while-revalidate)**: The shared library handles caching, but the specific behavior is not visible from the Procest codebase.
+
+### Standards & References
+
+- **OpenAPI 3.0.0**: The register configuration file follows this format.
+- **ZGW APIs (VNG Realisatie)**: Full ZGW-compliant API layer with Zaken (ZRC), Catalogi (ZTC), Documenten (DRC), Besluiten (BRC), Notificaties (NRC), and Autorisaties (AC) endpoints.
+- **CMMN 1.1**: Task lifecycle states follow the CasePlanModel/HumanTask pattern.
+- **Schema.org**: Entity type annotations in `procest_register.json`.
+- **Common Ground**: Layered architecture with data in OpenRegister (information layer) and Procest as process layer.
+
+### Specificity Assessment
+
+- **Mostly implementable as-is**, but the 12-store pattern conflicts with the actual architecture (single unified object store from `@conduction/nextcloud-vue`). The spec should be updated to reflect the shared library pattern or the implementation should diverge.
+- **Missing details:**
+ - The spec does not mention the ZGW API layer, which is a major feature of the actual implementation.
+ - Cascade behavior rules need concrete definition (which approach: cascade delete or prevent delete?).
+ - RBAC enforcement details depend on OpenRegister's RBAC implementation, which is not specified here.
+- **Open questions:**
+ - Should the spec be updated to match the single-store pattern, or should 12 individual stores be created?
+ - How does ZGW field mapping (English to Dutch) interact with the OpenRegister schema definitions?
diff --git a/openspec/specs/prometheus-metrics/spec.md b/openspec/specs/prometheus-metrics/spec.md
index c4b56104..1251f893 100644
--- a/openspec/specs/prometheus-metrics/spec.md
+++ b/openspec/specs/prometheus-metrics/spec.md
@@ -1,43 +1,43 @@
-# Prometheus Metrics Endpoint
-
-## Purpose
-Expose application metrics in Prometheus text exposition format at `GET /api/metrics` for monitoring, alerting, and operational dashboards.
-
-## Requirements
-
-### REQ-PROM-001: Metrics Endpoint
-- MUST expose `GET /index.php/apps/procest/api/metrics` returning `text/plain; version=0.0.4; charset=utf-8`
-- MUST require admin authentication (Nextcloud admin or API token)
-- MUST return metrics in Prometheus text exposition format
-
-### REQ-PROM-002: Standard Metrics
-Every app MUST expose these standard metrics:
-- `procest_info` (gauge, labels: version, php_version, nextcloud_version) — always 1
-- `procest_up` (gauge) — 1 if app is healthy, 0 if degraded
-- `procest_requests_total` (counter, labels: method, endpoint, status) — HTTP request count
-- `procest_request_duration_seconds` (histogram, labels: method, endpoint) — request latency
-- `procest_errors_total` (counter, labels: type) — error count by type
-
-### REQ-PROM-003: App-Specific Metrics
-- `procest_cases_total` (gauge, labels: status, case_type) — total cases
-- `procest_cases_overdue_total` (gauge) — cases past deadline
-- `procest_tasks_total` (gauge, labels: status) — total tasks
-- `procest_tasks_overdue_total` (gauge) — tasks past deadline
-- `procest_zgw_requests_total` (counter, labels: api, method, status) — ZGW API calls
-- `procest_zgw_request_duration_seconds` (histogram, labels: api) — ZGW API latency
-
-### REQ-PROM-004: Health Check
-- MUST expose `GET /index.php/apps/procest/api/health` returning JSON `{"status": "ok"|"degraded"|"error", "checks": {...}}`
-- Checks: database connectivity, required dependencies available, ZGW API reachability
-
-## Current Implementation Status
-- **Not implemented**: No MetricsController, HealthController, or metrics/monitoring code exists in the app.
-
-## Standards & References
-- Prometheus text exposition format: https://prometheus.io/docs/instrumenting/exposition_formats/
-- OpenMetrics specification: https://openmetrics.io/
-- Nextcloud server monitoring patterns
-- OpenRegister MetricsService and HeartbeatController as reference implementation
-
-## Specificity Assessment
-Highly specific — metric names, types, and labels are fully defined. Implementation follows a standard pattern that can be shared via a base MetricsService trait/class from OpenRegister.
+# Prometheus Metrics Endpoint
+
+## Purpose
+Expose application metrics in Prometheus text exposition format at `GET /api/metrics` for monitoring, alerting, and operational dashboards.
+
+## Requirements
+
+### REQ-PROM-001: Metrics Endpoint
+- MUST expose `GET /index.php/apps/procest/api/metrics` returning `text/plain; version=0.0.4; charset=utf-8`
+- MUST require admin authentication (Nextcloud admin or API token)
+- MUST return metrics in Prometheus text exposition format
+
+### REQ-PROM-002: Standard Metrics
+Every app MUST expose these standard metrics:
+- `procest_info` (gauge, labels: version, php_version, nextcloud_version) — always 1
+- `procest_up` (gauge) — 1 if app is healthy, 0 if degraded
+- `procest_requests_total` (counter, labels: method, endpoint, status) — HTTP request count
+- `procest_request_duration_seconds` (histogram, labels: method, endpoint) — request latency
+- `procest_errors_total` (counter, labels: type) — error count by type
+
+### REQ-PROM-003: App-Specific Metrics
+- `procest_cases_total` (gauge, labels: status, case_type) — total cases
+- `procest_cases_overdue_total` (gauge) — cases past deadline
+- `procest_tasks_total` (gauge, labels: status) — total tasks
+- `procest_tasks_overdue_total` (gauge) — tasks past deadline
+- `procest_zgw_requests_total` (counter, labels: api, method, status) — ZGW API calls
+- `procest_zgw_request_duration_seconds` (histogram, labels: api) — ZGW API latency
+
+### REQ-PROM-004: Health Check
+- MUST expose `GET /index.php/apps/procest/api/health` returning JSON `{"status": "ok"|"degraded"|"error", "checks": {...}}`
+- Checks: database connectivity, required dependencies available, ZGW API reachability
+
+## Current Implementation Status
+- **Not implemented**: No MetricsController, HealthController, or metrics/monitoring code exists in the app.
+
+## Standards & References
+- Prometheus text exposition format: https://prometheus.io/docs/instrumenting/exposition_formats/
+- OpenMetrics specification: https://openmetrics.io/
+- Nextcloud server monitoring patterns
+- OpenRegister MetricsService and HeartbeatController as reference implementation
+
+## Specificity Assessment
+Highly specific — metric names, types, and labels are fully defined. Implementation follows a standard pattern that can be shared via a base MetricsService trait/class from OpenRegister.
diff --git a/openspec/specs/register-i18n/spec.md b/openspec/specs/register-i18n/spec.md
index 2ed8f020..57d7cae8 100644
--- a/openspec/specs/register-i18n/spec.md
+++ b/openspec/specs/register-i18n/spec.md
@@ -1,63 +1,63 @@
-# Register Content Internationalization
-
-## Purpose
-Enable multi-language support for Procest's register objects, allowing users to view and manage case management content in their preferred language. Built on OpenRegister's register-i18n foundation (see `openregister/openspec/specs/register-i18n/spec.md`).
-
-## Requirements
-
-### REQ-I18N-001: Language-Tagged Fields
-The following Procest-specific fields MUST support multi-language content via OpenRegister's `translatable` flag:
-
-**Case types:**
-- `title` — display name of the case type (e.g., "Omgevingsvergunning" / "Environmental permit")
-- `description` — explanation of the case type's purpose and scope
-- `purpose` — formal purpose description for the case type
-- `trigger` — description of what initiates this type of case
-- `subject` — subject area description
-
-**Status types:**
-- `title` — display name of the status (e.g., "In behandeling" / "In progress")
-- `description` — explanation of what this status means
-
-**Result types:**
-- `title` — display name of the result (e.g., "Toegekend" / "Granted")
-- `description` — explanation of the result type
-
-**Role types:**
-- `title` — display name of the role (e.g., "Behandelaar" / "Case handler")
-- `description` — explanation of role responsibilities
-
-**Document types:**
-- `title` — display name of the document type
-- `description` — explanation of what this document type contains
-
-**NOT translatable:** Case data itself (case content, notes, decisions) is specific to one municipality/language context and MUST NOT be marked as translatable. Case data is created in the language of the municipality handling the case.
-
-### REQ-I18N-002: Language Fallback Chain
-- MUST follow the Nextcloud user's language preference
-- MUST fall back: user language -> app default language -> nl -> en -> first available
-- MUST display fallback indicator when showing non-preferred language
-
-### REQ-I18N-003: Frontend Language Switching
-- MUST show language selector on detail pages when translated content exists
-- MUST preserve current language selection across navigation within the app
-- Language switching MUST NOT require page reload
-
-### REQ-I18N-004: API Language Support
-- API responses MUST accept `Accept-Language` header
-- API responses MUST include `Content-Language` header
-- `?lang=nl` query parameter MUST override Accept-Language
-- Listing endpoints MUST return content in requested language with fallback
-
-## Current Implementation Status
-Not implemented. No multi-language content support exists in Procest. All content is stored in a single language (typically Dutch). Case type definitions, status types, result types, role types, and document types are all single-language.
-
-## Standards & References
-- OpenRegister register-i18n spec (foundation)
-- BCP 47 language tags (nl, en, de, fr, etc.)
-- W3C Internationalization best practices
-- Nextcloud l10n framework (for UI strings -- separate from register content i18n)
-- WCAG 2.1 SC 3.1.1 (Language of Page) and SC 3.1.2 (Language of Parts)
-
-## Specificity Assessment
-Depends on OpenRegister's register-i18n being implemented first. App-level work is primarily frontend (language selector, fallback display) and API layer (Accept-Language routing). The distinction between translatable type definitions and non-translatable case data is critical -- only the "configuration" objects (types, statuses, roles) need translation, not the case instances themselves.
+# Register Content Internationalization
+
+## Purpose
+Enable multi-language support for Procest's register objects, allowing users to view and manage case management content in their preferred language. Built on OpenRegister's register-i18n foundation (see `openregister/openspec/specs/register-i18n/spec.md`).
+
+## Requirements
+
+### REQ-I18N-001: Language-Tagged Fields
+The following Procest-specific fields MUST support multi-language content via OpenRegister's `translatable` flag:
+
+**Case types:**
+- `title` — display name of the case type (e.g., "Omgevingsvergunning" / "Environmental permit")
+- `description` — explanation of the case type's purpose and scope
+- `purpose` — formal purpose description for the case type
+- `trigger` — description of what initiates this type of case
+- `subject` — subject area description
+
+**Status types:**
+- `title` — display name of the status (e.g., "In behandeling" / "In progress")
+- `description` — explanation of what this status means
+
+**Result types:**
+- `title` — display name of the result (e.g., "Toegekend" / "Granted")
+- `description` — explanation of the result type
+
+**Role types:**
+- `title` — display name of the role (e.g., "Behandelaar" / "Case handler")
+- `description` — explanation of role responsibilities
+
+**Document types:**
+- `title` — display name of the document type
+- `description` — explanation of what this document type contains
+
+**NOT translatable:** Case data itself (case content, notes, decisions) is specific to one municipality/language context and MUST NOT be marked as translatable. Case data is created in the language of the municipality handling the case.
+
+### REQ-I18N-002: Language Fallback Chain
+- MUST follow the Nextcloud user's language preference
+- MUST fall back: user language -> app default language -> nl -> en -> first available
+- MUST display fallback indicator when showing non-preferred language
+
+### REQ-I18N-003: Frontend Language Switching
+- MUST show language selector on detail pages when translated content exists
+- MUST preserve current language selection across navigation within the app
+- Language switching MUST NOT require page reload
+
+### REQ-I18N-004: API Language Support
+- API responses MUST accept `Accept-Language` header
+- API responses MUST include `Content-Language` header
+- `?lang=nl` query parameter MUST override Accept-Language
+- Listing endpoints MUST return content in requested language with fallback
+
+## Current Implementation Status
+Not implemented. No multi-language content support exists in Procest. All content is stored in a single language (typically Dutch). Case type definitions, status types, result types, role types, and document types are all single-language.
+
+## Standards & References
+- OpenRegister register-i18n spec (foundation)
+- BCP 47 language tags (nl, en, de, fr, etc.)
+- W3C Internationalization best practices
+- Nextcloud l10n framework (for UI strings -- separate from register content i18n)
+- WCAG 2.1 SC 3.1.1 (Language of Page) and SC 3.1.2 (Language of Parts)
+
+## Specificity Assessment
+Depends on OpenRegister's register-i18n being implemented first. App-level work is primarily frontend (language selector, fallback display) and API layer (Accept-Language routing). The distinction between translatable type definitions and non-translatable case data is critical -- only the "configuration" objects (types, statuses, roles) need translation, not the case instances themselves.
diff --git a/openspec/specs/roles-decisions/spec.md b/openspec/specs/roles-decisions/spec.md
index 074584b3..5b1a2a09 100644
--- a/openspec/specs/roles-decisions/spec.md
+++ b/openspec/specs/roles-decisions/spec.md
@@ -1,750 +1,750 @@
-# Roles & Decisions Specification
-
-## Purpose
-
-Roles define the relationship between participants (Nextcloud users or external contacts) and cases -- who is involved and in what capacity. Results record the formal outcome of a completed case, linking to a predefined result type that controls archival rules. Decisions are formal administrative choices made on cases, with legal validity periods and publication requirements.
-
-Together, these three entities govern participation, outcomes, and formal decision-making within the case lifecycle.
-
-**Standards**: Schema.org (`Role`, `ChooseAction`), CMMN (case outcomes, case participants), ZGW (`Rol`, `Resultaat`, `Besluit`, `RolType`, `ResultaatType`, `BesluitType`)
-**Primary feature tier**: MVP (roles, results), V1 (decisions, role types, result types, decision types)
-
----
-
-## Data Model
-
-### Role Entity
-
-Stored as an OpenRegister object in the `procest` register under the `role` schema.
-
-| Property | Type | Schema.org/ZGW | Required | Default |
-|----------|------|----------------|----------|---------|
-| `name` | string (max 255) | `schema:roleName` / `omschrijving` | Yes | — |
-| `description` | string | `schema:description` / `roltoelichting` | No | — |
-| `roleType` | reference (UUID to RoleType) | — / `omschrijvingGeneriek` (via RoleType) | Yes | — |
-| `case` | reference (UUID to Case) | — / `zaak` | Yes | — |
-| `participant` | string (Nextcloud user UID or contact reference) | `schema:agent` / `betrokkene` | Yes | — |
-
-### Role Type Entity
-
-Stored as an OpenRegister object in the `procest` register under the `roleType` schema.
-
-| Property | Type | ZGW Mapping | Required |
-|----------|------|-------------|----------|
-| `name` | string (max 255) | `omschrijving` | Yes |
-| `caseType` | reference (UUID to CaseType) | `zaaktype` | Yes |
-| `genericRole` | enum | `omschrijvingGeneriek` | Yes |
-
-### Standard Generic Roles
-
-These are the fixed set of generic role categories, derived from ZGW but internationally applicable.
-
-| Generic Role | ZGW Dutch | Description | Typical Use |
-|-------------|-----------|-------------|-------------|
-| `initiator` | Initiator | Started the case | Citizen/applicant who submitted the request |
-| `handler` | Behandelaar | Processes the case | Civil servant assigned to handle the case |
-| `advisor` | Adviseur | Provides advice | Technical or legal advisor consulted |
-| `decision_maker` | Beslisser | Makes decisions | Authority who signs off on decisions |
-| `stakeholder` | Belanghebbende | Has interest in outcome | Neighbor, affected party |
-| `coordinator` | Zaakcoordinator | Coordinates the case | Team lead overseeing case progress |
-| `contact` | Klantcontacter | Contact person | Front-desk agent, customer contact |
-| `co_initiator` | Mede-initiator | Co-initiator | Joint applicant or co-requester |
-
-### Result Entity
-
-Stored as an OpenRegister object in the `procest` register under the `result` schema.
-
-| Property | Type | Source | Required |
-|----------|------|--------|----------|
-| `name` | string (max 255) | `schema:name` | Yes |
-| `description` | string | `schema:description` | No |
-| `case` | reference (UUID to Case) | Parent case | Yes |
-| `resultType` | reference (UUID to ResultType) | ResultType definition | Yes |
-
-### Result Type Entity
-
-Stored as an OpenRegister object in the `procest` register under the `resultType` schema.
-
-| Property | Type | ZGW Mapping | Required |
-|----------|------|-------------|----------|
-| `name` | string (max 255) | `omschrijving` | Yes |
-| `description` | string | `toelichting` | No |
-| `caseType` | reference (UUID to CaseType) | `zaaktype` | Yes |
-| `archiveAction` | enum: `retain`, `destroy` | `archiefnominatie` | No |
-| `retentionPeriod` | duration (ISO 8601, e.g., "P20Y") | `archiefactietermijn` | No |
-| `retentionDateSource` | enum | `afleidingswijze` | No |
-
-### Decision Entity
-
-Stored as an OpenRegister object in the `procest` register under the `decision` schema.
-
-| Property | Type | Schema.org/ZGW | Required | Default |
-|----------|------|----------------|----------|---------|
-| `title` | string (max 255) | `schema:name` | Yes | — |
-| `description` | string | `schema:description` / `toelichting` | No | — |
-| `case` | reference (UUID to Case) | — / `zaak` | Yes | — |
-| `decisionType` | reference (UUID to DecisionType) | — / `besluittype` | No | — |
-| `decidedBy` | string (Nextcloud user UID) | `schema:agent` | No | — |
-| `decidedAt` | datetime (ISO 8601) | `schema:endTime` / `datum` | No | current timestamp |
-| `effectiveDate` | date (ISO 8601) | `schema:startTime` / `ingangsdatum` | No | — |
-| `expiryDate` | date (ISO 8601) | `schema:endTime` / `vervaldatum` | No | — |
-
-### Decision Type Entity
-
-Stored as an OpenRegister object in the `procest` register under the `decisionType` schema.
-
-| Property | Type | ZGW Mapping | Required |
-|----------|------|-------------|----------|
-| `name` | string (max 255) | `omschrijving` | Yes |
-| `description` | string | `toelichting` | No |
-| `category` | string | `besluitcategorie` | No |
-| `objectionPeriod` | duration (ISO 8601) | `reactietermijn` | No |
-| `publicationRequired` | boolean | `publicatie_indicatie` | Yes |
-| `publicationPeriod` | duration (ISO 8601) | `publicatietermijn` | No |
-
----
-
-## Requirements
-
-### REQ-ROLE-001: Role Assignment on Cases
-
-**Tier**: MVP
-
-The system MUST support assigning roles to participants on cases. A role links a participant (Nextcloud user or contact reference) to a case with a specific role type.
-
-#### Scenario: Assign a handler to a case
-
-- GIVEN a case #2024-042 "Bouwvergunning Keizersgracht" exists
-- AND a role type "Behandelaar" (genericRole: `handler`) exists for the case's type "Omgevingsvergunning"
-- WHEN the coordinator assigns Nextcloud user "jan.devries" as handler
-- THEN the system MUST create a role object in the `role` schema with:
- - `name`: "Behandelaar"
- - `roleType`: UUID of the "Behandelaar" role type
- - `case`: UUID of case #2024-042
- - `participant`: "jan.devries"
-- AND the handler MUST appear in the Participants section of the case detail view
-- AND the case's `assignee` field SHOULD also be set to "jan.devries" (handler shortcut)
-- AND the audit trail MUST record the role assignment
-
-#### Scenario: Assign initiator from Pipelinq request-to-case conversion
-
-- GIVEN a Pipelinq request #REQ-2024-089 is being converted to a case
-- AND the requesting contact is "Petra Jansen" (contact ref: "contact-uuid-petra")
-- AND the case type has a role type "Aanvrager" (genericRole: `initiator`)
-- WHEN the case is created from the request
-- THEN the system SHOULD automatically create a role with:
- - `roleType`: UUID of the "Aanvrager" role type
- - `participant`: "contact-uuid-petra"
- - `case`: UUID of the new case
-- AND the initiator MUST appear in the Participants section under "Initiator"
-
-#### Scenario: Assign multiple participants with different roles
-
-- GIVEN case #2024-042 already has:
- - Handler: "jan.devries" (Jan de Vries)
- - Initiator: "contact-uuid-petra" (Petra Jansen)
-- WHEN the coordinator adds an advisor with participant "dr.k.bakker"
-- THEN the system MUST create a new role object for the advisor
-- AND all three participants MUST be visible in the case detail:
- ```
- Handler: Jan de Vries [Reassign]
- Initiator: Petra Jansen (Acme Corp)
- Advisor: Dr. K. Bakker
- ```
-- AND each role MUST show the participant display name and role type label
-
-#### Scenario: Assign the same participant with multiple roles
-
-- GIVEN "jan.devries" is already the handler on case #2024-042
-- WHEN the coordinator also assigns "jan.devries" as the coordinator role
-- THEN the system MUST create a second role object for the coordinator assignment
-- AND the Participants section MUST show Jan de Vries listed under both roles
-
-#### Scenario: Reassign a handler
-
-- GIVEN case #2024-042 has handler "jan.devries" (Jan de Vries)
-- WHEN the coordinator clicks "Reassign" and selects "maria.bakker" (Maria Bakker)
-- THEN the existing handler role MUST be updated with `participant`: "maria.bakker"
-- AND the case `assignee` field SHOULD be updated to "maria.bakker"
-- AND "maria.bakker" SHOULD receive a notification about the assignment
-- AND the audit trail MUST record the reassignment from "jan.devries" to "maria.bakker"
-
-#### Scenario: Remove a role from a case
-
-- GIVEN case #2024-042 has an advisor role for "dr.k.bakker"
-- WHEN the coordinator removes the advisor role
-- THEN the role object MUST be deleted from OpenRegister
-- AND "Dr. K. Bakker" MUST no longer appear in the Participants section
-- AND the audit trail MUST record the removal
-
----
-
-### REQ-ROLE-002: Role Type Enforcement from Case Type
-
-**Tier**: V1
-
-The system SHOULD enforce that only role types linked to the case's case type can be assigned. This prevents assigning roles that are not applicable to the case type.
-
-#### Scenario: Only allowed role types are available for assignment
-
-- GIVEN case type "Omgevingsvergunning" has role types:
- - "Aanvrager" (genericRole: `initiator`)
- - "Behandelaar" (genericRole: `handler`)
- - "Technisch adviseur" (genericRole: `advisor`)
- - "Beslisser" (genericRole: `decision_maker`)
-- WHEN the user opens the "Add Participant" dialog on a case of this type
-- THEN only these four role types MUST be available for selection
-- AND role types from other case types MUST NOT appear
-
-#### Scenario: Reject assignment of a role type not linked to the case type
-
-- GIVEN case type "Klacht behandeling" has only role types: "Klager" (initiator), "Behandelaar" (handler)
-- WHEN the user attempts to assign a role with genericRole `advisor` to a case of this type
-- THEN the system MUST reject the assignment
-- AND the error message MUST indicate that the role type is not allowed for this case type
-
-#### Scenario: Case type with no role types defined
-
-- GIVEN case type "Melding" has no role types configured (V1 feature not yet configured)
-- WHEN the user attempts to add a participant to a case of this type
-- THEN the system SHOULD allow assignment with any generic role as fallback
-- OR the system SHOULD display a message that role types need to be configured by an admin
-
----
-
-### REQ-ROLE-003: Handler Assignment Shortcut
-
-**Tier**: MVP
-
-The system MUST provide a convenient handler assignment mechanism that creates the handler role and updates the case's `assignee` field in a single action.
-
-#### Scenario: Quick handler assignment from case list
-
-- GIVEN the case list shows case #2024-050 "Bouwvergunning Prinsengracht" with handler "---"
-- WHEN the user clicks the handler cell and selects "Jan de Vries"
-- THEN the system MUST create a handler role for "jan.devries" on the case
-- AND the case `assignee` MUST be set to "jan.devries"
-- AND the case list MUST immediately reflect the new handler
-
-#### Scenario: Handler assignment from case detail
-
-- GIVEN case #2024-050 has no handler assigned
-- WHEN the user clicks "Assign Handler" in the Participants section
-- THEN a user picker MUST appear showing Nextcloud users
-- AND selecting "jan.devries" MUST create both the role and update the case assignee
-
----
-
-### REQ-ROLE-004: Role-Based Case Access
-
-**Tier**: V1
-
-The system SHOULD support controlling who can see and edit a case based on their assigned role.
-
-#### Scenario: Handler has full edit access
-
-- GIVEN "jan.devries" is assigned as handler on case #2024-042
-- WHEN Jan views the case
-- THEN Jan MUST have full edit access: update case fields, change status, manage tasks, manage roles
-
-#### Scenario: Advisor has read access plus task assignment
-
-- GIVEN "dr.k.bakker" is assigned as advisor on case #2024-042
-- WHEN Dr. Bakker views the case
-- THEN Dr. Bakker MUST have read access to all case details
-- AND Dr. Bakker SHOULD be able to complete tasks assigned to them
-- AND Dr. Bakker MUST NOT be able to change the case status or manage other roles
-
-#### Scenario: Unassigned user cannot access a restricted case
-
-- GIVEN case #2024-042 has confidentiality `case_sensitive`
-- AND "pieter.smit" has no role on the case
-- WHEN "pieter.smit" attempts to view the case
-- THEN the system SHOULD deny access based on RBAC rules
-- AND the case MUST NOT appear in Pieter's case list
-
----
-
-### REQ-RESULT-001: Case Result Recording
-
-**Tier**: MVP
-
-The system MUST support recording a result when a case is being completed. Each case MUST have at most one result. The result links to a predefined result type from the case type.
-
-#### Scenario: Record a result on case completion
-
-- GIVEN case #2024-042 "Bouwvergunning Keizersgracht" has status "Besluitvorming" (the status before final)
-- AND the case type "Omgevingsvergunning" has result types: "Vergunning verleend", "Vergunning geweigerd", "Ingetrokken"
-- WHEN the handler Jan de Vries records the result "Vergunning verleend"
-- THEN the system MUST create a result object with:
- - `name`: "Vergunning verleend"
- - `case`: UUID of case #2024-042
- - `resultType`: UUID of the "Vergunning verleend" result type
-- AND the case `result` reference MUST point to this result object
-- AND the case `endDate` MUST be set to the current date
-- AND the case status MUST transition to "Afgehandeld" (the final status)
-- AND the audit trail MUST record the result and case closure
-
-#### Scenario: Result type determines archival rules
-
-- GIVEN the result type "Vergunning verleend" has:
- - archiveAction: `retain`
- - retentionPeriod: "P20Y" (20 years)
- - retentionDateSource: `case_completed`
-- WHEN this result is recorded on case #2024-042
-- THEN the system MUST store the archival metadata linked to the case
-- AND the retention end date MUST be calculated as endDate + 20 years
-
-#### Scenario: Result with "Denied" outcome
-
-- GIVEN case #2024-038 "Subsidie innovatie" is being closed
-- WHEN the handler Maria Bakker records result "Subsidie afgewezen" (archiveAction: `destroy`, retentionPeriod: "P10Y")
-- THEN the result MUST be created and linked to the case
-- AND the case MUST be marked as completed with endDate set
-
-#### Scenario: Choose from predefined result types
-
-- GIVEN case type "Omgevingsvergunning" has 3 result types configured
-- WHEN the user initiates case closure on case #2024-042
-- THEN the system MUST present the 3 result types as a selectable list
-- AND the user MUST select one before completing the case
-- AND free-text result entry MUST NOT be allowed (the result must match a defined result type)
-
-#### Scenario: Attempt to record a result with an invalid result type
-
-- GIVEN case type "Omgevingsvergunning" has result types: "Vergunning verleend", "Vergunning geweigerd", "Ingetrokken"
-- WHEN the user attempts to record a result with a result type UUID belonging to case type "Klacht behandeling"
-- THEN the system MUST reject the result
-- AND the error message MUST indicate that the result type does not belong to this case type
-
-#### Scenario: Attempt to record a second result on a case
-
-- GIVEN case #2024-042 already has a result "Vergunning verleend"
-- WHEN the user attempts to record another result
-- THEN the system MUST reject the operation
-- AND the error message MUST indicate that a case can have at most one result
-
-#### Scenario: Case without result types configured
-
-- GIVEN case type "Melding" has no result types defined (MVP without V1 type configuration)
-- WHEN the handler closes the case
-- THEN the system MUST allow case closure without selecting a result type
-- AND a generic result with the case closure information MUST be recorded
-
----
-
-### REQ-RESULT-002: Result Type Configuration
-
-**Tier**: V1
-
-Admin users MUST be able to configure result types per case type, including archival rules.
-
-#### Scenario: Create a result type with archival rules
-
-- GIVEN the admin is editing case type "Omgevingsvergunning"
-- WHEN the admin creates a result type:
- - name: "Vergunning verleend"
- - archiveAction: `retain`
- - retentionPeriod: "P20Y"
- - retentionDateSource: `case_completed`
-- THEN the result type MUST be created and linked to the case type
-- AND the result type MUST appear in the Result Types section of the case type admin page
-
-#### Scenario: Edit a result type's archival rules
-
-- GIVEN result type "Vergunning geweigerd" for case type "Omgevingsvergunning" has retentionPeriod "P10Y"
-- WHEN the admin changes retentionPeriod to "P7Y"
-- THEN the result type MUST be updated
-- AND existing cases that used this result type MUST NOT be retroactively affected
-
-#### Scenario: Delete a result type that is not in use
-
-- GIVEN result type "Ingetrokken" for case type "Omgevingsvergunning" is not referenced by any case result
-- WHEN the admin deletes the result type
-- THEN the result type MUST be removed from the case type
-- AND it MUST no longer appear as an option during case closure
-
-#### Scenario: Attempt to delete a result type that is in use
-
-- GIVEN result type "Vergunning verleend" is referenced by 5 existing case results
-- WHEN the admin attempts to delete it
-- THEN the system SHOULD warn the admin that 5 cases reference this result type
-- AND the system SHOULD either prevent deletion or mark the result type as inactive (not available for new results but still resolves for existing ones)
-
----
-
-### REQ-DECISION-001: Decision CRUD
-
-**Tier**: V1
-
-The system SHOULD support creating, reading, updating, and deleting formal decisions linked to cases. Decisions represent administrative determinations with potential legal effect.
-
-#### Scenario: Create a decision on a case
-
-- GIVEN case #2024-042 "Bouwvergunning Keizersgracht" is in status "Besluitvorming"
-- AND the case type has a decision type "Omgevingsvergunning besluit"
-- WHEN the decision maker "dr.k.bakker" records a decision:
- - title: "Omgevingsvergunning verleend Keizersgracht 100"
- - description: "Vergunning verleend voor de verbouwing van het pand op Keizersgracht 100 conform ingediende bouwtekeningen."
- - decisionType: UUID of "Omgevingsvergunning besluit"
- - effectiveDate: "2026-03-01"
- - expiryDate: "2031-03-01"
-- THEN the system MUST create a decision object in the `decision` schema with:
- - `case`: UUID of case #2024-042
- - `decidedBy`: "dr.k.bakker"
- - `decidedAt`: current timestamp
- - All provided fields stored correctly
-- AND the decision MUST appear in the Decisions section of the case detail view
-- AND the audit trail MUST record the decision creation
-
-#### Scenario: Create a decision with default decidedAt
-
-- GIVEN the user records a decision without explicitly setting `decidedAt`
-- THEN `decidedAt` MUST default to the current timestamp
-- AND the decision date MUST be displayed in the case detail
-
-#### Scenario: View decisions on case detail
-
-- GIVEN case #2024-042 has 2 decisions:
- - "Omgevingsvergunning verleend" (decidedAt: 2026-02-25, effectiveDate: 2026-03-01, expiryDate: 2031-03-01)
- - "Voorwaardelijk gebruik terrein" (decidedAt: 2026-02-20, effectiveDate: 2026-02-20, expiryDate: 2027-02-20)
-- WHEN the user views the case detail
-- THEN both decisions MUST be displayed in the Decisions section
-- AND each decision MUST show: title, decided date, decided by, validity period (effective to expiry)
-- AND decisions MUST be sorted by decidedAt descending (most recent first)
-
-#### Scenario: Update a decision's description
-
-- GIVEN decision "Omgevingsvergunning verleend" exists on case #2024-042
-- WHEN the decision maker updates the description to add additional conditions
-- THEN the decision object MUST be updated via the OpenRegister API
-- AND the audit trail MUST record the modification
-
-#### Scenario: Delete a decision
-
-- GIVEN decision "Voorwaardelijk gebruik terrein" exists on case #2024-042
-- WHEN the user deletes the decision
-- THEN the decision object MUST be removed from OpenRegister
-- AND it MUST no longer appear in the case detail
-- AND the audit trail MUST record the deletion
-
----
-
-### REQ-DECISION-002: Decision Validity Periods
-
-**Tier**: V1
-
-The system SHOULD support tracking the validity period of decisions (effectiveDate to expiryDate) and provide indicators when decisions are nearing expiry or have expired.
-
-#### Scenario: Decision with validity period display
-
-- GIVEN a decision "Omgevingsvergunning verleend" with effectiveDate "2026-03-01" and expiryDate "2031-03-01"
-- AND today is 2026-02-25
-- WHEN the user views the decision
-- THEN the validity period MUST be displayed as "Mar 1, 2026 -- Mar 1, 2031"
-- AND the status MUST show "Not yet effective" (effective date is in the future)
-
-#### Scenario: Active decision
-
-- GIVEN a decision with effectiveDate "2026-01-01" and expiryDate "2031-01-01"
-- AND today is 2026-06-15
-- THEN the decision MUST be displayed as "Active"
-- AND the remaining validity SHOULD be displayed (e.g., "4 years, 6 months remaining")
-
-#### Scenario: Decision nearing expiry
-
-- GIVEN a decision with expiryDate "2026-03-15"
-- AND today is 2026-02-25 (18 days before expiry)
-- THEN the decision SHOULD show an amber warning indicator
-- AND the warning SHOULD indicate "Expires in 18 days"
-
-#### Scenario: Expired decision
-
-- GIVEN a decision with expiryDate "2025-12-31"
-- AND today is 2026-02-25
-- THEN the decision MUST be displayed as "Expired"
-- AND an expired indicator MUST be shown in red
-
-#### Scenario: Decision without expiry date
-
-- GIVEN a decision with effectiveDate "2026-03-01" and no expiryDate
-- THEN the validity MUST be displayed as "From Mar 1, 2026" (no end date)
-- AND the decision MUST be treated as indefinitely valid once effective
-
-#### Scenario: Decision without any dates
-
-- GIVEN a decision with no effectiveDate and no expiryDate
-- THEN no validity period MUST be displayed
-- AND only the decidedAt date MUST be shown
-
----
-
-### REQ-DECISION-003: Decision Types from Case Type
-
-**Tier**: V1
-
-The system SHOULD support linking decision types to case types. When creating a decision on a case, only decision types allowed by the case's case type SHOULD be offered.
-
-#### Scenario: Only allowed decision types are available
-
-- GIVEN case type "Omgevingsvergunning" has decision types:
- - "Omgevingsvergunning besluit" (publicationRequired: true, objectionPeriod: "P6W")
- - "Voorlopige voorziening" (publicationRequired: false)
-- WHEN the user creates a decision on a case of this type
-- THEN only these two decision types MUST be available for selection
-- AND the user MAY also create a decision without a decision type (free-form decision)
-
-#### Scenario: Decision type provides default publication rules
-
-- GIVEN decision type "Omgevingsvergunning besluit" has publicationRequired: true and publicationPeriod: "P6W"
-- WHEN a decision of this type is created
-- THEN the system SHOULD indicate that the decision requires publication
-- AND the publication deadline SHOULD be calculated from the decidedAt date
-
-#### Scenario: Create a decision without a decision type
-
-- GIVEN a case where the case type has decision types configured
-- WHEN the user creates a decision and selects "No type" or leaves decision type empty
-- THEN the system MUST allow the decision to be created without a decision type
-- AND all other required fields (title) MUST still be validated
-
----
-
-### REQ-DECISION-004: Decision Validation
-
-**Tier**: V1
-
-The system MUST validate decision data to ensure consistency and completeness.
-
-#### Scenario: Decision title is required
-
-- GIVEN the user is creating a new decision
-- WHEN the user submits without a title
-- THEN the system MUST reject the request with a validation error
-- AND the error message MUST indicate that `title` is required
-
-#### Scenario: Decision case reference is required
-
-- GIVEN the user is creating a new decision
-- WHEN the user submits without a case reference
-- THEN the system MUST reject the request with a validation error
-- AND the error message MUST indicate that `case` is required
-
-#### Scenario: Expiry date must be after effective date
-
-- GIVEN the user sets effectiveDate "2026-03-01" and expiryDate "2026-02-01"
-- WHEN the user submits the decision
-- THEN the system MUST reject the request
-- AND the error message MUST indicate that expiryDate must be after effectiveDate
-
-#### Scenario: DecidedBy must be a valid Nextcloud user
-
-- GIVEN the user sets decidedBy to "nonexistent.user"
-- WHEN the user submits the decision
-- THEN the system SHOULD warn or reject that the user does not exist
-- AND the system MAY allow the value if it is a free-text reference (external decision maker)
-
----
-
-### REQ-ROLE-005: Participant Display on Case Detail
-
-**Tier**: MVP
-
-The case detail view MUST display all assigned participants grouped by role type, as shown in the design wireframes.
-
-#### Scenario: Full participant section display
-
-- GIVEN case #2024-042 has the following roles:
- - Handler: Jan de Vries ("jan.devries")
- - Initiator: Petra Jansen ("contact-uuid-petra", company "Acme Corp")
- - Advisor: Dr. K. Bakker ("dr.k.bakker")
-- WHEN the user views the case detail page
-- THEN the Participants section MUST display:
- ```
- PARTICIPANTS
-
- Handler:
- [avatar] Jan de Vries
- [Reassign]
-
- Initiator:
- [avatar] Petra Jansen (Acme Corp)
-
- Advisor:
- [avatar] Dr. K. Bakker
-
- [+ Add Participant]
- ```
-- AND each participant MUST show their display name resolved from Nextcloud user or contact reference
-- AND the handler role MUST have a "Reassign" action
-- AND the "Add Participant" button MUST open a dialog to select role type and participant
-
-#### Scenario: No participants assigned
-
-- GIVEN a newly created case #2024-051 with no role assignments
-- WHEN the user views the case detail
-- THEN the Participants section MUST show an empty state
-- AND a prominent "Assign Handler" action MUST be visible
-- AND an "Add Participant" button MUST be available
-
-#### Scenario: External contact as participant
-
-- GIVEN "Petra Jansen" is a contact in Nextcloud Contacts (not a Nextcloud user)
-- WHEN her role is displayed on the case
-- THEN the system MUST resolve the contact reference to show her display name
-- AND the system SHOULD show the organization ("Acme Corp") if available from the contact record
-- AND the participant MUST be distinguished from Nextcloud users (e.g., different icon or label)
-
----
-
-### REQ-ROLE-006: Role Validation
-
-**Tier**: MVP
-
-The system MUST validate role assignments to ensure data integrity.
-
-#### Scenario: Participant is required
-
-- GIVEN the user is creating a new role on a case
-- WHEN the user submits without selecting a participant
-- THEN the system MUST reject the request
-- AND the error message MUST indicate that `participant` is required
-
-#### Scenario: Role type is required
-
-- GIVEN the user is creating a new role on a case
-- WHEN the user submits without selecting a role type
-- THEN the system MUST reject the request
-- AND the error message MUST indicate that `roleType` is required
-
-#### Scenario: Case reference is required
-
-- GIVEN the user is creating a new role
-- WHEN the user submits without a case reference
-- THEN the system MUST reject the request
-- AND the error message MUST indicate that `case` is required
-
-#### Scenario: Validate that the referenced case exists
-
-- GIVEN the user submits a role with `case` set to a non-existent UUID
-- THEN the system MUST reject the request
-- AND the error message MUST indicate that the referenced case does not exist
-
----
-
-### REQ-DECISION-005: Decisions Section on Case Detail
-
-**Tier**: V1
-
-The case detail view MUST display all decisions linked to the case.
-
-#### Scenario: Decisions section with no decisions
-
-- GIVEN case #2024-042 has no decisions recorded
-- WHEN the user views the case detail
-- THEN the Decisions section MUST display "(no decisions yet)"
-- AND an "Add Decision" button MUST be visible
-
-#### Scenario: Decisions section with multiple decisions
-
-- GIVEN case #2024-042 has 2 decisions
-- WHEN the user views the case detail
-- THEN both decisions MUST be listed with:
- - Title
- - Decided by (user display name)
- - Decided at (date)
- - Validity period (if set)
- - Decision type (if set)
-- AND each decision MUST be clickable to view/edit details
-
----
-
-## Error Scenarios Summary
-
-| Error | Expected Behavior | Tier |
-|-------|-------------------|------|
-| Assign role type not linked to case type | Reject with "Role type not allowed for this case type" | V1 |
-| Record result with invalid result type | Reject with "Result type does not belong to this case type" | V1 |
-| Record second result on a case | Reject with "Case already has a result" | MVP |
-| Create decision without title | Reject with validation error "title is required" | V1 |
-| Create decision with expiryDate before effectiveDate | Reject with "expiryDate must be after effectiveDate" | V1 |
-| Create role without participant | Reject with "participant is required" | MVP |
-| Create role referencing non-existent case | Reject with "Referenced case does not exist" | MVP |
-| Assign handler to non-existent user | Reject with "User does not exist" | MVP |
-
----
-
-## Accessibility
-
-All roles and decisions interfaces MUST comply with WCAG AA:
-
-- Participant display names MUST have sufficient contrast
-- Role type selection MUST be keyboard-accessible
-- Decision validity indicators MUST NOT rely solely on color (use text labels alongside color)
-- The "Add Participant" dialog MUST be focusable and navigable by keyboard
-- Screen readers MUST announce role type and participant name for each entry
-
----
-
-## Performance
-
-- The Participants section MUST resolve user/contact display names within 1 second
-- Decision validity calculations MUST be performed client-side (no extra API call)
-- Role and result operations MUST complete within 2 seconds
-- The case detail page MUST load participants, results, and decisions in parallel with other sections
-
----
-
-### Current Implementation Status
-
-**Roles: Substantially implemented (MVP). Results: Partially implemented. Decisions: Not implemented.**
-
-**Roles -- Implemented (with file paths):**
-- **ParticipantsSection**: `src/views/cases/components/ParticipantsSection.vue` -- displays all roles on a case, grouped by role type name. Resolves participant display names via Nextcloud OCS API (`/ocs/v2.php/cloud/users/{uid}`). Shows initials avatar, role type label, and participant name. Supports "Add Participant" button and "Reassign" action on handler roles. Supports "Remove" action on non-handler roles (REQ-ROLE-001, REQ-ROLE-005).
-- **AddParticipantDialog**: `src/views/cases/components/AddParticipantDialog.vue` -- dialog for adding participants with role type selection and user picker. Supports pre-selecting handler role type (REQ-ROLE-003).
-- **Handler reassignment**: `ParticipantsSection.vue` includes inline reassign UI with NcSelect user picker. Updates both the role object's `participant` and the case's `assignee` field (REQ-ROLE-003).
-- **Role removal**: Supported via delete button with confirmation dialog.
-- **Role schema**: Defined in `lib/Settings/procest_register.json` with properties: `name`, `description`, `roleType`, `case`, `participant` (REQ matching the data model).
-- **RoleType schema**: Defined in `procest_register.json` with `name`, `caseType`, `genericRole` properties. The `genericRole` enum includes: `initiator`, `handler`, `advisor`, `decision_maker`, `stakeholder`, `coordinator`, `contact`, `co_initiator`.
-- **Data fetching**: Roles fetched via `objectStore.fetchCollection('role', { '_filters[case]': caseId })`. Role types fetched in parallel.
-- **Display name resolution**: `resolveDisplayNames()` method fetches Nextcloud user info per participant UID.
-- **User picker**: `fetchUsers()` fetches available users from `/ocs/v2.php/cloud/users/details`.
-
-**Roles -- Not yet implemented:**
-- **REQ-ROLE-002: Role type enforcement (V1)**: No validation that assigned role types belong to the case's case type. All role types are shown in the picker regardless of case type.
-- **REQ-ROLE-004: Role-based case access (V1)**: No RBAC enforcement based on role assignments. All users with app access can see all cases.
-- **REQ-ROLE-006: Role validation**: Client-side validation exists in the dialog, but server-side validation of participant existence and case reference validity is delegated to OpenRegister schema validation.
-- **Notifications**: No Nextcloud notification sent when a handler is assigned or reassigned.
-- **External contacts**: Only Nextcloud users are supported as participants. No integration with Nextcloud Contacts for external party references.
-
-**Results -- Partially implemented:**
-- **ResultSection**: `src/views/cases/components/ResultSection.vue` -- displays a single result with name, description, and result type. Resolves result type name from the `resultTypes` array.
-- **Result schema**: Defined in `procest_register.json` with `name`, `description`, `case`, `resultType` properties.
-- **ResultType schema**: Defined in `procest_register.json` with `name`, `description`, `caseType`, `archiveAction`, `retentionPeriod`, `retentionDateSource` properties.
-- **Not implemented**: Result creation UI (selecting from predefined result types during case closure), archival metadata display, result type management in admin settings (REQ-RESULT-002), enforcement of one-result-per-case.
-
-**Decisions -- Not implemented:**
-- **Decision schema**: Defined in `procest_register.json` with `title`, `description`, `case`, `decisionType`, `decidedBy`, `decidedAt`, `effectiveDate`, `expiryDate` properties.
-- **DecisionType schema**: Defined in `procest_register.json` with `name`, `description`, `category`, `objectionPeriod`, `publicationRequired`, `publicationPeriod` properties.
-- **No UI exists** for creating, viewing, editing, or deleting decisions on cases. No Decisions section on the case detail page. No validity period tracking or expiry indicators.
-- The ZGW BRC (Besluiten) controller (`lib/Controller/BrcController.php`) provides ZGW-compliant decision API endpoints, but no frontend consumes them.
-
-### Standards & References
-
-- **ZGW APIs (VNG Realisatie)**: Roles map to ZGW `Rol` with `omschrijvingGeneriek` for generic role categories. Results map to `Resultaat` with `archiefnominatie` and `archiefactietermijn`. Decisions map to `Besluit` with `ingangsdatum`, `vervaldatum`, `publicatie_indicatie`. ZGW BRC controller fully implemented.
-- **Schema.org**: Roles typed as `schema:Role`, decisions as `schema:ChooseAction` in `procest_register.json`.
-- **CMMN 1.1**: Role assignments follow CMMN case participant patterns.
-- **Archivering**: Result types include `archiveAction` (retain/destroy) and `retentionPeriod` (ISO 8601 duration) per Dutch archival standards (Archiefwet).
-- **WCAG 2.1 AA**: ParticipantsSection uses sufficient contrast and text labels. Decision validity indicators (not yet implemented) must not rely solely on color.
-- **Wet open overheid (WOO)**: Decision publication requirements align with WOO transparency obligations.
-
-### Specificity Assessment
-
-- **Roles**: Well-specified and mostly implemented. The MVP scenarios are clear and actionable.
-- **Results**: Well-specified but implementation is incomplete. The result creation flow during case closure needs UI work.
-- **Decisions**: Well-specified but entirely unimplemented in the frontend. The data model exists in the register config, and the ZGW API layer exists, but no Procest-native UI exists.
-- **Open questions:**
- - Should role type enforcement be strict (reject) or advisory (warn)?
- - How should external contacts (non-Nextcloud users) be represented as participants?
- - Should decision publication trigger an n8n workflow or a direct API call?
- - How does the result creation flow interact with case status transitions (must the case transition to a final status after result is recorded)?
+# Roles & Decisions Specification
+
+## Purpose
+
+Roles define the relationship between participants (Nextcloud users or external contacts) and cases -- who is involved and in what capacity. Results record the formal outcome of a completed case, linking to a predefined result type that controls archival rules. Decisions are formal administrative choices made on cases, with legal validity periods and publication requirements.
+
+Together, these three entities govern participation, outcomes, and formal decision-making within the case lifecycle.
+
+**Standards**: Schema.org (`Role`, `ChooseAction`), CMMN (case outcomes, case participants), ZGW (`Rol`, `Resultaat`, `Besluit`, `RolType`, `ResultaatType`, `BesluitType`)
+**Primary feature tier**: MVP (roles, results), V1 (decisions, role types, result types, decision types)
+
+---
+
+## Data Model
+
+### Role Entity
+
+Stored as an OpenRegister object in the `procest` register under the `role` schema.
+
+| Property | Type | Schema.org/ZGW | Required | Default |
+|----------|------|----------------|----------|---------|
+| `name` | string (max 255) | `schema:roleName` / `omschrijving` | Yes | — |
+| `description` | string | `schema:description` / `roltoelichting` | No | — |
+| `roleType` | reference (UUID to RoleType) | — / `omschrijvingGeneriek` (via RoleType) | Yes | — |
+| `case` | reference (UUID to Case) | — / `zaak` | Yes | — |
+| `participant` | string (Nextcloud user UID or contact reference) | `schema:agent` / `betrokkene` | Yes | — |
+
+### Role Type Entity
+
+Stored as an OpenRegister object in the `procest` register under the `roleType` schema.
+
+| Property | Type | ZGW Mapping | Required |
+|----------|------|-------------|----------|
+| `name` | string (max 255) | `omschrijving` | Yes |
+| `caseType` | reference (UUID to CaseType) | `zaaktype` | Yes |
+| `genericRole` | enum | `omschrijvingGeneriek` | Yes |
+
+### Standard Generic Roles
+
+These are the fixed set of generic role categories, derived from ZGW but internationally applicable.
+
+| Generic Role | ZGW Dutch | Description | Typical Use |
+|-------------|-----------|-------------|-------------|
+| `initiator` | Initiator | Started the case | Citizen/applicant who submitted the request |
+| `handler` | Behandelaar | Processes the case | Civil servant assigned to handle the case |
+| `advisor` | Adviseur | Provides advice | Technical or legal advisor consulted |
+| `decision_maker` | Beslisser | Makes decisions | Authority who signs off on decisions |
+| `stakeholder` | Belanghebbende | Has interest in outcome | Neighbor, affected party |
+| `coordinator` | Zaakcoordinator | Coordinates the case | Team lead overseeing case progress |
+| `contact` | Klantcontacter | Contact person | Front-desk agent, customer contact |
+| `co_initiator` | Mede-initiator | Co-initiator | Joint applicant or co-requester |
+
+### Result Entity
+
+Stored as an OpenRegister object in the `procest` register under the `result` schema.
+
+| Property | Type | Source | Required |
+|----------|------|--------|----------|
+| `name` | string (max 255) | `schema:name` | Yes |
+| `description` | string | `schema:description` | No |
+| `case` | reference (UUID to Case) | Parent case | Yes |
+| `resultType` | reference (UUID to ResultType) | ResultType definition | Yes |
+
+### Result Type Entity
+
+Stored as an OpenRegister object in the `procest` register under the `resultType` schema.
+
+| Property | Type | ZGW Mapping | Required |
+|----------|------|-------------|----------|
+| `name` | string (max 255) | `omschrijving` | Yes |
+| `description` | string | `toelichting` | No |
+| `caseType` | reference (UUID to CaseType) | `zaaktype` | Yes |
+| `archiveAction` | enum: `retain`, `destroy` | `archiefnominatie` | No |
+| `retentionPeriod` | duration (ISO 8601, e.g., "P20Y") | `archiefactietermijn` | No |
+| `retentionDateSource` | enum | `afleidingswijze` | No |
+
+### Decision Entity
+
+Stored as an OpenRegister object in the `procest` register under the `decision` schema.
+
+| Property | Type | Schema.org/ZGW | Required | Default |
+|----------|------|----------------|----------|---------|
+| `title` | string (max 255) | `schema:name` | Yes | — |
+| `description` | string | `schema:description` / `toelichting` | No | — |
+| `case` | reference (UUID to Case) | — / `zaak` | Yes | — |
+| `decisionType` | reference (UUID to DecisionType) | — / `besluittype` | No | — |
+| `decidedBy` | string (Nextcloud user UID) | `schema:agent` | No | — |
+| `decidedAt` | datetime (ISO 8601) | `schema:endTime` / `datum` | No | current timestamp |
+| `effectiveDate` | date (ISO 8601) | `schema:startTime` / `ingangsdatum` | No | — |
+| `expiryDate` | date (ISO 8601) | `schema:endTime` / `vervaldatum` | No | — |
+
+### Decision Type Entity
+
+Stored as an OpenRegister object in the `procest` register under the `decisionType` schema.
+
+| Property | Type | ZGW Mapping | Required |
+|----------|------|-------------|----------|
+| `name` | string (max 255) | `omschrijving` | Yes |
+| `description` | string | `toelichting` | No |
+| `category` | string | `besluitcategorie` | No |
+| `objectionPeriod` | duration (ISO 8601) | `reactietermijn` | No |
+| `publicationRequired` | boolean | `publicatie_indicatie` | Yes |
+| `publicationPeriod` | duration (ISO 8601) | `publicatietermijn` | No |
+
+---
+
+## Requirements
+
+### REQ-ROLE-001: Role Assignment on Cases
+
+**Tier**: MVP
+
+The system MUST support assigning roles to participants on cases. A role links a participant (Nextcloud user or contact reference) to a case with a specific role type.
+
+#### Scenario: Assign a handler to a case
+
+- GIVEN a case #2024-042 "Bouwvergunning Keizersgracht" exists
+- AND a role type "Behandelaar" (genericRole: `handler`) exists for the case's type "Omgevingsvergunning"
+- WHEN the coordinator assigns Nextcloud user "jan.devries" as handler
+- THEN the system MUST create a role object in the `role` schema with:
+ - `name`: "Behandelaar"
+ - `roleType`: UUID of the "Behandelaar" role type
+ - `case`: UUID of case #2024-042
+ - `participant`: "jan.devries"
+- AND the handler MUST appear in the Participants section of the case detail view
+- AND the case's `assignee` field SHOULD also be set to "jan.devries" (handler shortcut)
+- AND the audit trail MUST record the role assignment
+
+#### Scenario: Assign initiator from Pipelinq request-to-case conversion
+
+- GIVEN a Pipelinq request #REQ-2024-089 is being converted to a case
+- AND the requesting contact is "Petra Jansen" (contact ref: "contact-uuid-petra")
+- AND the case type has a role type "Aanvrager" (genericRole: `initiator`)
+- WHEN the case is created from the request
+- THEN the system SHOULD automatically create a role with:
+ - `roleType`: UUID of the "Aanvrager" role type
+ - `participant`: "contact-uuid-petra"
+ - `case`: UUID of the new case
+- AND the initiator MUST appear in the Participants section under "Initiator"
+
+#### Scenario: Assign multiple participants with different roles
+
+- GIVEN case #2024-042 already has:
+ - Handler: "jan.devries" (Jan de Vries)
+ - Initiator: "contact-uuid-petra" (Petra Jansen)
+- WHEN the coordinator adds an advisor with participant "dr.k.bakker"
+- THEN the system MUST create a new role object for the advisor
+- AND all three participants MUST be visible in the case detail:
+ ```
+ Handler: Jan de Vries [Reassign]
+ Initiator: Petra Jansen (Acme Corp)
+ Advisor: Dr. K. Bakker
+ ```
+- AND each role MUST show the participant display name and role type label
+
+#### Scenario: Assign the same participant with multiple roles
+
+- GIVEN "jan.devries" is already the handler on case #2024-042
+- WHEN the coordinator also assigns "jan.devries" as the coordinator role
+- THEN the system MUST create a second role object for the coordinator assignment
+- AND the Participants section MUST show Jan de Vries listed under both roles
+
+#### Scenario: Reassign a handler
+
+- GIVEN case #2024-042 has handler "jan.devries" (Jan de Vries)
+- WHEN the coordinator clicks "Reassign" and selects "maria.bakker" (Maria Bakker)
+- THEN the existing handler role MUST be updated with `participant`: "maria.bakker"
+- AND the case `assignee` field SHOULD be updated to "maria.bakker"
+- AND "maria.bakker" SHOULD receive a notification about the assignment
+- AND the audit trail MUST record the reassignment from "jan.devries" to "maria.bakker"
+
+#### Scenario: Remove a role from a case
+
+- GIVEN case #2024-042 has an advisor role for "dr.k.bakker"
+- WHEN the coordinator removes the advisor role
+- THEN the role object MUST be deleted from OpenRegister
+- AND "Dr. K. Bakker" MUST no longer appear in the Participants section
+- AND the audit trail MUST record the removal
+
+---
+
+### REQ-ROLE-002: Role Type Enforcement from Case Type
+
+**Tier**: V1
+
+The system SHOULD enforce that only role types linked to the case's case type can be assigned. This prevents assigning roles that are not applicable to the case type.
+
+#### Scenario: Only allowed role types are available for assignment
+
+- GIVEN case type "Omgevingsvergunning" has role types:
+ - "Aanvrager" (genericRole: `initiator`)
+ - "Behandelaar" (genericRole: `handler`)
+ - "Technisch adviseur" (genericRole: `advisor`)
+ - "Beslisser" (genericRole: `decision_maker`)
+- WHEN the user opens the "Add Participant" dialog on a case of this type
+- THEN only these four role types MUST be available for selection
+- AND role types from other case types MUST NOT appear
+
+#### Scenario: Reject assignment of a role type not linked to the case type
+
+- GIVEN case type "Klacht behandeling" has only role types: "Klager" (initiator), "Behandelaar" (handler)
+- WHEN the user attempts to assign a role with genericRole `advisor` to a case of this type
+- THEN the system MUST reject the assignment
+- AND the error message MUST indicate that the role type is not allowed for this case type
+
+#### Scenario: Case type with no role types defined
+
+- GIVEN case type "Melding" has no role types configured (V1 feature not yet configured)
+- WHEN the user attempts to add a participant to a case of this type
+- THEN the system SHOULD allow assignment with any generic role as fallback
+- OR the system SHOULD display a message that role types need to be configured by an admin
+
+---
+
+### REQ-ROLE-003: Handler Assignment Shortcut
+
+**Tier**: MVP
+
+The system MUST provide a convenient handler assignment mechanism that creates the handler role and updates the case's `assignee` field in a single action.
+
+#### Scenario: Quick handler assignment from case list
+
+- GIVEN the case list shows case #2024-050 "Bouwvergunning Prinsengracht" with handler "---"
+- WHEN the user clicks the handler cell and selects "Jan de Vries"
+- THEN the system MUST create a handler role for "jan.devries" on the case
+- AND the case `assignee` MUST be set to "jan.devries"
+- AND the case list MUST immediately reflect the new handler
+
+#### Scenario: Handler assignment from case detail
+
+- GIVEN case #2024-050 has no handler assigned
+- WHEN the user clicks "Assign Handler" in the Participants section
+- THEN a user picker MUST appear showing Nextcloud users
+- AND selecting "jan.devries" MUST create both the role and update the case assignee
+
+---
+
+### REQ-ROLE-004: Role-Based Case Access
+
+**Tier**: V1
+
+The system SHOULD support controlling who can see and edit a case based on their assigned role.
+
+#### Scenario: Handler has full edit access
+
+- GIVEN "jan.devries" is assigned as handler on case #2024-042
+- WHEN Jan views the case
+- THEN Jan MUST have full edit access: update case fields, change status, manage tasks, manage roles
+
+#### Scenario: Advisor has read access plus task assignment
+
+- GIVEN "dr.k.bakker" is assigned as advisor on case #2024-042
+- WHEN Dr. Bakker views the case
+- THEN Dr. Bakker MUST have read access to all case details
+- AND Dr. Bakker SHOULD be able to complete tasks assigned to them
+- AND Dr. Bakker MUST NOT be able to change the case status or manage other roles
+
+#### Scenario: Unassigned user cannot access a restricted case
+
+- GIVEN case #2024-042 has confidentiality `case_sensitive`
+- AND "pieter.smit" has no role on the case
+- WHEN "pieter.smit" attempts to view the case
+- THEN the system SHOULD deny access based on RBAC rules
+- AND the case MUST NOT appear in Pieter's case list
+
+---
+
+### REQ-RESULT-001: Case Result Recording
+
+**Tier**: MVP
+
+The system MUST support recording a result when a case is being completed. Each case MUST have at most one result. The result links to a predefined result type from the case type.
+
+#### Scenario: Record a result on case completion
+
+- GIVEN case #2024-042 "Bouwvergunning Keizersgracht" has status "Besluitvorming" (the status before final)
+- AND the case type "Omgevingsvergunning" has result types: "Vergunning verleend", "Vergunning geweigerd", "Ingetrokken"
+- WHEN the handler Jan de Vries records the result "Vergunning verleend"
+- THEN the system MUST create a result object with:
+ - `name`: "Vergunning verleend"
+ - `case`: UUID of case #2024-042
+ - `resultType`: UUID of the "Vergunning verleend" result type
+- AND the case `result` reference MUST point to this result object
+- AND the case `endDate` MUST be set to the current date
+- AND the case status MUST transition to "Afgehandeld" (the final status)
+- AND the audit trail MUST record the result and case closure
+
+#### Scenario: Result type determines archival rules
+
+- GIVEN the result type "Vergunning verleend" has:
+ - archiveAction: `retain`
+ - retentionPeriod: "P20Y" (20 years)
+ - retentionDateSource: `case_completed`
+- WHEN this result is recorded on case #2024-042
+- THEN the system MUST store the archival metadata linked to the case
+- AND the retention end date MUST be calculated as endDate + 20 years
+
+#### Scenario: Result with "Denied" outcome
+
+- GIVEN case #2024-038 "Subsidie innovatie" is being closed
+- WHEN the handler Maria Bakker records result "Subsidie afgewezen" (archiveAction: `destroy`, retentionPeriod: "P10Y")
+- THEN the result MUST be created and linked to the case
+- AND the case MUST be marked as completed with endDate set
+
+#### Scenario: Choose from predefined result types
+
+- GIVEN case type "Omgevingsvergunning" has 3 result types configured
+- WHEN the user initiates case closure on case #2024-042
+- THEN the system MUST present the 3 result types as a selectable list
+- AND the user MUST select one before completing the case
+- AND free-text result entry MUST NOT be allowed (the result must match a defined result type)
+
+#### Scenario: Attempt to record a result with an invalid result type
+
+- GIVEN case type "Omgevingsvergunning" has result types: "Vergunning verleend", "Vergunning geweigerd", "Ingetrokken"
+- WHEN the user attempts to record a result with a result type UUID belonging to case type "Klacht behandeling"
+- THEN the system MUST reject the result
+- AND the error message MUST indicate that the result type does not belong to this case type
+
+#### Scenario: Attempt to record a second result on a case
+
+- GIVEN case #2024-042 already has a result "Vergunning verleend"
+- WHEN the user attempts to record another result
+- THEN the system MUST reject the operation
+- AND the error message MUST indicate that a case can have at most one result
+
+#### Scenario: Case without result types configured
+
+- GIVEN case type "Melding" has no result types defined (MVP without V1 type configuration)
+- WHEN the handler closes the case
+- THEN the system MUST allow case closure without selecting a result type
+- AND a generic result with the case closure information MUST be recorded
+
+---
+
+### REQ-RESULT-002: Result Type Configuration
+
+**Tier**: V1
+
+Admin users MUST be able to configure result types per case type, including archival rules.
+
+#### Scenario: Create a result type with archival rules
+
+- GIVEN the admin is editing case type "Omgevingsvergunning"
+- WHEN the admin creates a result type:
+ - name: "Vergunning verleend"
+ - archiveAction: `retain`
+ - retentionPeriod: "P20Y"
+ - retentionDateSource: `case_completed`
+- THEN the result type MUST be created and linked to the case type
+- AND the result type MUST appear in the Result Types section of the case type admin page
+
+#### Scenario: Edit a result type's archival rules
+
+- GIVEN result type "Vergunning geweigerd" for case type "Omgevingsvergunning" has retentionPeriod "P10Y"
+- WHEN the admin changes retentionPeriod to "P7Y"
+- THEN the result type MUST be updated
+- AND existing cases that used this result type MUST NOT be retroactively affected
+
+#### Scenario: Delete a result type that is not in use
+
+- GIVEN result type "Ingetrokken" for case type "Omgevingsvergunning" is not referenced by any case result
+- WHEN the admin deletes the result type
+- THEN the result type MUST be removed from the case type
+- AND it MUST no longer appear as an option during case closure
+
+#### Scenario: Attempt to delete a result type that is in use
+
+- GIVEN result type "Vergunning verleend" is referenced by 5 existing case results
+- WHEN the admin attempts to delete it
+- THEN the system SHOULD warn the admin that 5 cases reference this result type
+- AND the system SHOULD either prevent deletion or mark the result type as inactive (not available for new results but still resolves for existing ones)
+
+---
+
+### REQ-DECISION-001: Decision CRUD
+
+**Tier**: V1
+
+The system SHOULD support creating, reading, updating, and deleting formal decisions linked to cases. Decisions represent administrative determinations with potential legal effect.
+
+#### Scenario: Create a decision on a case
+
+- GIVEN case #2024-042 "Bouwvergunning Keizersgracht" is in status "Besluitvorming"
+- AND the case type has a decision type "Omgevingsvergunning besluit"
+- WHEN the decision maker "dr.k.bakker" records a decision:
+ - title: "Omgevingsvergunning verleend Keizersgracht 100"
+ - description: "Vergunning verleend voor de verbouwing van het pand op Keizersgracht 100 conform ingediende bouwtekeningen."
+ - decisionType: UUID of "Omgevingsvergunning besluit"
+ - effectiveDate: "2026-03-01"
+ - expiryDate: "2031-03-01"
+- THEN the system MUST create a decision object in the `decision` schema with:
+ - `case`: UUID of case #2024-042
+ - `decidedBy`: "dr.k.bakker"
+ - `decidedAt`: current timestamp
+ - All provided fields stored correctly
+- AND the decision MUST appear in the Decisions section of the case detail view
+- AND the audit trail MUST record the decision creation
+
+#### Scenario: Create a decision with default decidedAt
+
+- GIVEN the user records a decision without explicitly setting `decidedAt`
+- THEN `decidedAt` MUST default to the current timestamp
+- AND the decision date MUST be displayed in the case detail
+
+#### Scenario: View decisions on case detail
+
+- GIVEN case #2024-042 has 2 decisions:
+ - "Omgevingsvergunning verleend" (decidedAt: 2026-02-25, effectiveDate: 2026-03-01, expiryDate: 2031-03-01)
+ - "Voorwaardelijk gebruik terrein" (decidedAt: 2026-02-20, effectiveDate: 2026-02-20, expiryDate: 2027-02-20)
+- WHEN the user views the case detail
+- THEN both decisions MUST be displayed in the Decisions section
+- AND each decision MUST show: title, decided date, decided by, validity period (effective to expiry)
+- AND decisions MUST be sorted by decidedAt descending (most recent first)
+
+#### Scenario: Update a decision's description
+
+- GIVEN decision "Omgevingsvergunning verleend" exists on case #2024-042
+- WHEN the decision maker updates the description to add additional conditions
+- THEN the decision object MUST be updated via the OpenRegister API
+- AND the audit trail MUST record the modification
+
+#### Scenario: Delete a decision
+
+- GIVEN decision "Voorwaardelijk gebruik terrein" exists on case #2024-042
+- WHEN the user deletes the decision
+- THEN the decision object MUST be removed from OpenRegister
+- AND it MUST no longer appear in the case detail
+- AND the audit trail MUST record the deletion
+
+---
+
+### REQ-DECISION-002: Decision Validity Periods
+
+**Tier**: V1
+
+The system SHOULD support tracking the validity period of decisions (effectiveDate to expiryDate) and provide indicators when decisions are nearing expiry or have expired.
+
+#### Scenario: Decision with validity period display
+
+- GIVEN a decision "Omgevingsvergunning verleend" with effectiveDate "2026-03-01" and expiryDate "2031-03-01"
+- AND today is 2026-02-25
+- WHEN the user views the decision
+- THEN the validity period MUST be displayed as "Mar 1, 2026 -- Mar 1, 2031"
+- AND the status MUST show "Not yet effective" (effective date is in the future)
+
+#### Scenario: Active decision
+
+- GIVEN a decision with effectiveDate "2026-01-01" and expiryDate "2031-01-01"
+- AND today is 2026-06-15
+- THEN the decision MUST be displayed as "Active"
+- AND the remaining validity SHOULD be displayed (e.g., "4 years, 6 months remaining")
+
+#### Scenario: Decision nearing expiry
+
+- GIVEN a decision with expiryDate "2026-03-15"
+- AND today is 2026-02-25 (18 days before expiry)
+- THEN the decision SHOULD show an amber warning indicator
+- AND the warning SHOULD indicate "Expires in 18 days"
+
+#### Scenario: Expired decision
+
+- GIVEN a decision with expiryDate "2025-12-31"
+- AND today is 2026-02-25
+- THEN the decision MUST be displayed as "Expired"
+- AND an expired indicator MUST be shown in red
+
+#### Scenario: Decision without expiry date
+
+- GIVEN a decision with effectiveDate "2026-03-01" and no expiryDate
+- THEN the validity MUST be displayed as "From Mar 1, 2026" (no end date)
+- AND the decision MUST be treated as indefinitely valid once effective
+
+#### Scenario: Decision without any dates
+
+- GIVEN a decision with no effectiveDate and no expiryDate
+- THEN no validity period MUST be displayed
+- AND only the decidedAt date MUST be shown
+
+---
+
+### REQ-DECISION-003: Decision Types from Case Type
+
+**Tier**: V1
+
+The system SHOULD support linking decision types to case types. When creating a decision on a case, only decision types allowed by the case's case type SHOULD be offered.
+
+#### Scenario: Only allowed decision types are available
+
+- GIVEN case type "Omgevingsvergunning" has decision types:
+ - "Omgevingsvergunning besluit" (publicationRequired: true, objectionPeriod: "P6W")
+ - "Voorlopige voorziening" (publicationRequired: false)
+- WHEN the user creates a decision on a case of this type
+- THEN only these two decision types MUST be available for selection
+- AND the user MAY also create a decision without a decision type (free-form decision)
+
+#### Scenario: Decision type provides default publication rules
+
+- GIVEN decision type "Omgevingsvergunning besluit" has publicationRequired: true and publicationPeriod: "P6W"
+- WHEN a decision of this type is created
+- THEN the system SHOULD indicate that the decision requires publication
+- AND the publication deadline SHOULD be calculated from the decidedAt date
+
+#### Scenario: Create a decision without a decision type
+
+- GIVEN a case where the case type has decision types configured
+- WHEN the user creates a decision and selects "No type" or leaves decision type empty
+- THEN the system MUST allow the decision to be created without a decision type
+- AND all other required fields (title) MUST still be validated
+
+---
+
+### REQ-DECISION-004: Decision Validation
+
+**Tier**: V1
+
+The system MUST validate decision data to ensure consistency and completeness.
+
+#### Scenario: Decision title is required
+
+- GIVEN the user is creating a new decision
+- WHEN the user submits without a title
+- THEN the system MUST reject the request with a validation error
+- AND the error message MUST indicate that `title` is required
+
+#### Scenario: Decision case reference is required
+
+- GIVEN the user is creating a new decision
+- WHEN the user submits without a case reference
+- THEN the system MUST reject the request with a validation error
+- AND the error message MUST indicate that `case` is required
+
+#### Scenario: Expiry date must be after effective date
+
+- GIVEN the user sets effectiveDate "2026-03-01" and expiryDate "2026-02-01"
+- WHEN the user submits the decision
+- THEN the system MUST reject the request
+- AND the error message MUST indicate that expiryDate must be after effectiveDate
+
+#### Scenario: DecidedBy must be a valid Nextcloud user
+
+- GIVEN the user sets decidedBy to "nonexistent.user"
+- WHEN the user submits the decision
+- THEN the system SHOULD warn or reject that the user does not exist
+- AND the system MAY allow the value if it is a free-text reference (external decision maker)
+
+---
+
+### REQ-ROLE-005: Participant Display on Case Detail
+
+**Tier**: MVP
+
+The case detail view MUST display all assigned participants grouped by role type, as shown in the design wireframes.
+
+#### Scenario: Full participant section display
+
+- GIVEN case #2024-042 has the following roles:
+ - Handler: Jan de Vries ("jan.devries")
+ - Initiator: Petra Jansen ("contact-uuid-petra", company "Acme Corp")
+ - Advisor: Dr. K. Bakker ("dr.k.bakker")
+- WHEN the user views the case detail page
+- THEN the Participants section MUST display:
+ ```
+ PARTICIPANTS
+
+ Handler:
+ [avatar] Jan de Vries
+ [Reassign]
+
+ Initiator:
+ [avatar] Petra Jansen (Acme Corp)
+
+ Advisor:
+ [avatar] Dr. K. Bakker
+
+ [+ Add Participant]
+ ```
+- AND each participant MUST show their display name resolved from Nextcloud user or contact reference
+- AND the handler role MUST have a "Reassign" action
+- AND the "Add Participant" button MUST open a dialog to select role type and participant
+
+#### Scenario: No participants assigned
+
+- GIVEN a newly created case #2024-051 with no role assignments
+- WHEN the user views the case detail
+- THEN the Participants section MUST show an empty state
+- AND a prominent "Assign Handler" action MUST be visible
+- AND an "Add Participant" button MUST be available
+
+#### Scenario: External contact as participant
+
+- GIVEN "Petra Jansen" is a contact in Nextcloud Contacts (not a Nextcloud user)
+- WHEN her role is displayed on the case
+- THEN the system MUST resolve the contact reference to show her display name
+- AND the system SHOULD show the organization ("Acme Corp") if available from the contact record
+- AND the participant MUST be distinguished from Nextcloud users (e.g., different icon or label)
+
+---
+
+### REQ-ROLE-006: Role Validation
+
+**Tier**: MVP
+
+The system MUST validate role assignments to ensure data integrity.
+
+#### Scenario: Participant is required
+
+- GIVEN the user is creating a new role on a case
+- WHEN the user submits without selecting a participant
+- THEN the system MUST reject the request
+- AND the error message MUST indicate that `participant` is required
+
+#### Scenario: Role type is required
+
+- GIVEN the user is creating a new role on a case
+- WHEN the user submits without selecting a role type
+- THEN the system MUST reject the request
+- AND the error message MUST indicate that `roleType` is required
+
+#### Scenario: Case reference is required
+
+- GIVEN the user is creating a new role
+- WHEN the user submits without a case reference
+- THEN the system MUST reject the request
+- AND the error message MUST indicate that `case` is required
+
+#### Scenario: Validate that the referenced case exists
+
+- GIVEN the user submits a role with `case` set to a non-existent UUID
+- THEN the system MUST reject the request
+- AND the error message MUST indicate that the referenced case does not exist
+
+---
+
+### REQ-DECISION-005: Decisions Section on Case Detail
+
+**Tier**: V1
+
+The case detail view MUST display all decisions linked to the case.
+
+#### Scenario: Decisions section with no decisions
+
+- GIVEN case #2024-042 has no decisions recorded
+- WHEN the user views the case detail
+- THEN the Decisions section MUST display "(no decisions yet)"
+- AND an "Add Decision" button MUST be visible
+
+#### Scenario: Decisions section with multiple decisions
+
+- GIVEN case #2024-042 has 2 decisions
+- WHEN the user views the case detail
+- THEN both decisions MUST be listed with:
+ - Title
+ - Decided by (user display name)
+ - Decided at (date)
+ - Validity period (if set)
+ - Decision type (if set)
+- AND each decision MUST be clickable to view/edit details
+
+---
+
+## Error Scenarios Summary
+
+| Error | Expected Behavior | Tier |
+|-------|-------------------|------|
+| Assign role type not linked to case type | Reject with "Role type not allowed for this case type" | V1 |
+| Record result with invalid result type | Reject with "Result type does not belong to this case type" | V1 |
+| Record second result on a case | Reject with "Case already has a result" | MVP |
+| Create decision without title | Reject with validation error "title is required" | V1 |
+| Create decision with expiryDate before effectiveDate | Reject with "expiryDate must be after effectiveDate" | V1 |
+| Create role without participant | Reject with "participant is required" | MVP |
+| Create role referencing non-existent case | Reject with "Referenced case does not exist" | MVP |
+| Assign handler to non-existent user | Reject with "User does not exist" | MVP |
+
+---
+
+## Accessibility
+
+All roles and decisions interfaces MUST comply with WCAG AA:
+
+- Participant display names MUST have sufficient contrast
+- Role type selection MUST be keyboard-accessible
+- Decision validity indicators MUST NOT rely solely on color (use text labels alongside color)
+- The "Add Participant" dialog MUST be focusable and navigable by keyboard
+- Screen readers MUST announce role type and participant name for each entry
+
+---
+
+## Performance
+
+- The Participants section MUST resolve user/contact display names within 1 second
+- Decision validity calculations MUST be performed client-side (no extra API call)
+- Role and result operations MUST complete within 2 seconds
+- The case detail page MUST load participants, results, and decisions in parallel with other sections
+
+---
+
+### Current Implementation Status
+
+**Roles: Substantially implemented (MVP). Results: Partially implemented. Decisions: Not implemented.**
+
+**Roles -- Implemented (with file paths):**
+- **ParticipantsSection**: `src/views/cases/components/ParticipantsSection.vue` -- displays all roles on a case, grouped by role type name. Resolves participant display names via Nextcloud OCS API (`/ocs/v2.php/cloud/users/{uid}`). Shows initials avatar, role type label, and participant name. Supports "Add Participant" button and "Reassign" action on handler roles. Supports "Remove" action on non-handler roles (REQ-ROLE-001, REQ-ROLE-005).
+- **AddParticipantDialog**: `src/views/cases/components/AddParticipantDialog.vue` -- dialog for adding participants with role type selection and user picker. Supports pre-selecting handler role type (REQ-ROLE-003).
+- **Handler reassignment**: `ParticipantsSection.vue` includes inline reassign UI with NcSelect user picker. Updates both the role object's `participant` and the case's `assignee` field (REQ-ROLE-003).
+- **Role removal**: Supported via delete button with confirmation dialog.
+- **Role schema**: Defined in `lib/Settings/procest_register.json` with properties: `name`, `description`, `roleType`, `case`, `participant` (REQ matching the data model).
+- **RoleType schema**: Defined in `procest_register.json` with `name`, `caseType`, `genericRole` properties. The `genericRole` enum includes: `initiator`, `handler`, `advisor`, `decision_maker`, `stakeholder`, `coordinator`, `contact`, `co_initiator`.
+- **Data fetching**: Roles fetched via `objectStore.fetchCollection('role', { '_filters[case]': caseId })`. Role types fetched in parallel.
+- **Display name resolution**: `resolveDisplayNames()` method fetches Nextcloud user info per participant UID.
+- **User picker**: `fetchUsers()` fetches available users from `/ocs/v2.php/cloud/users/details`.
+
+**Roles -- Not yet implemented:**
+- **REQ-ROLE-002: Role type enforcement (V1)**: No validation that assigned role types belong to the case's case type. All role types are shown in the picker regardless of case type.
+- **REQ-ROLE-004: Role-based case access (V1)**: No RBAC enforcement based on role assignments. All users with app access can see all cases.
+- **REQ-ROLE-006: Role validation**: Client-side validation exists in the dialog, but server-side validation of participant existence and case reference validity is delegated to OpenRegister schema validation.
+- **Notifications**: No Nextcloud notification sent when a handler is assigned or reassigned.
+- **External contacts**: Only Nextcloud users are supported as participants. No integration with Nextcloud Contacts for external party references.
+
+**Results -- Partially implemented:**
+- **ResultSection**: `src/views/cases/components/ResultSection.vue` -- displays a single result with name, description, and result type. Resolves result type name from the `resultTypes` array.
+- **Result schema**: Defined in `procest_register.json` with `name`, `description`, `case`, `resultType` properties.
+- **ResultType schema**: Defined in `procest_register.json` with `name`, `description`, `caseType`, `archiveAction`, `retentionPeriod`, `retentionDateSource` properties.
+- **Not implemented**: Result creation UI (selecting from predefined result types during case closure), archival metadata display, result type management in admin settings (REQ-RESULT-002), enforcement of one-result-per-case.
+
+**Decisions -- Not implemented:**
+- **Decision schema**: Defined in `procest_register.json` with `title`, `description`, `case`, `decisionType`, `decidedBy`, `decidedAt`, `effectiveDate`, `expiryDate` properties.
+- **DecisionType schema**: Defined in `procest_register.json` with `name`, `description`, `category`, `objectionPeriod`, `publicationRequired`, `publicationPeriod` properties.
+- **No UI exists** for creating, viewing, editing, or deleting decisions on cases. No Decisions section on the case detail page. No validity period tracking or expiry indicators.
+- The ZGW BRC (Besluiten) controller (`lib/Controller/BrcController.php`) provides ZGW-compliant decision API endpoints, but no frontend consumes them.
+
+### Standards & References
+
+- **ZGW APIs (VNG Realisatie)**: Roles map to ZGW `Rol` with `omschrijvingGeneriek` for generic role categories. Results map to `Resultaat` with `archiefnominatie` and `archiefactietermijn`. Decisions map to `Besluit` with `ingangsdatum`, `vervaldatum`, `publicatie_indicatie`. ZGW BRC controller fully implemented.
+- **Schema.org**: Roles typed as `schema:Role`, decisions as `schema:ChooseAction` in `procest_register.json`.
+- **CMMN 1.1**: Role assignments follow CMMN case participant patterns.
+- **Archivering**: Result types include `archiveAction` (retain/destroy) and `retentionPeriod` (ISO 8601 duration) per Dutch archival standards (Archiefwet).
+- **WCAG 2.1 AA**: ParticipantsSection uses sufficient contrast and text labels. Decision validity indicators (not yet implemented) must not rely solely on color.
+- **Wet open overheid (WOO)**: Decision publication requirements align with WOO transparency obligations.
+
+### Specificity Assessment
+
+- **Roles**: Well-specified and mostly implemented. The MVP scenarios are clear and actionable.
+- **Results**: Well-specified but implementation is incomplete. The result creation flow during case closure needs UI work.
+- **Decisions**: Well-specified but entirely unimplemented in the frontend. The data model exists in the register config, and the ZGW API layer exists, but no Procest-native UI exists.
+- **Open questions:**
+ - Should role type enforcement be strict (reject) or advisory (warn)?
+ - How should external contacts (non-Nextcloud users) be represented as participants?
+ - Should decision publication trigger an n8n workflow or a direct API call?
+ - How does the result creation flow interact with case status transitions (must the case transition to a final status after result is recorded)?
diff --git a/openspec/specs/stuf-support/spec.md b/openspec/specs/stuf-support/spec.md
index 3fd5b2e3..d98d07c4 100644
--- a/openspec/specs/stuf-support/spec.md
+++ b/openspec/specs/stuf-support/spec.md
@@ -1,716 +1,716 @@
-# StUF Protocol Support Specification
-
-## Purpose
-
-Procest currently implements ZGW APIs (Zaken, Catalogi, Documenten, Besluiten) for case management via REST controllers (`ZrcController`, `ZtcController`, `DrcController`, `BrcController`). However, many Dutch municipalities still rely on StUF (Standaard Uitwisseling Formaat) -- especially StUF-ZKN (Zaak-Kennis) for case management and StUF-BG (Basis Gemeentelijke) for person/address lookups. This spec defines how Procest supports StUF alongside ZGW, providing a dual API surface over the same OpenRegister case data.
-
-StUF support enables Procest to integrate with legacy form systems (e.g., formulierenmotoren that submit cases via StUF-ZKN), legacy case systems during migration periods, and BRP person lookups via StUF-BG. The approach leverages OpenConnector's existing SOAP infrastructure (SOAPService with StUF-ZKN `edcLk01` awareness) for outbound StUF calls, while adding inbound StUF endpoints to Procest for receiving SOAP messages from legacy consumers.
-
-**Standards**: StUF 3.01, StUF-ZKN 3.10, StUF-BG 3.10, ZGW APIs (VNG), RGBZ, GEMMA
-**Feature tier**: V1 (outbound StUF via OpenConnector), V2 (inbound StUF endpoints in Procest)
-
----
-
-## Architecture Overview
-
-```
- Inbound StUF (V2)
- Legacy systems send SOAP messages to Procest
- ┌──────────────────────────────────┐
- │ Legacy Formulierensysteem │
- │ Legacy Zaaksysteem │
- └────────────┬─────────────────────┘
- │ SOAP (StUF-ZKN/BG)
- ┌────────────▼─────────────────────┐
- │ Procest StUF Controller │
- │ - Raw XML POST handler │
- │ - StUF message parser │
- │ - StUF → OpenRegister mapper │
- └────────────┬─────────────────────┘
- │ Internal API
-┌──────────────────────────────────────▼──────────────────────┐
-│ OpenRegister (procest register) │
-│ - case, caseType, status, role, result, decision schemas │
-│ - Single source of truth for all case data │
-└──────────────────────────────────────▲──────────────────────┘
- │ Internal API
- ┌────────────┴─────────────────────┐
- │ OpenConnector (SOAP outbound) │
- │ - SOAPService (existing) │
- │ - StUF-ZKN/BG message builder │
- │ - mTLS / WS-Security auth │
- └────────────┬─────────────────────┘
- │ SOAP (StUF-ZKN/BG)
- ┌────────────▼─────────────────────┐
- │ External Legacy Services │
- │ - BRP (StUF-BG personen) │
- │ - Legacy zaaksysteem │
- │ - Gemeentelijk DMS │
- └──────────────────────────────────┘
- Outbound StUF (V1)
- Procest/OpenConnector queries legacy systems
-```
-
-### Coexistence Principle
-
-StUF and ZGW share the same underlying OpenRegister data. A case created via StUF-ZKN `zakLk01` is stored in the same `case` schema and is immediately visible via the ZGW Zaken API (`ZrcController`) and the Procest frontend. Conversely, a case created via the ZGW API or UI can be queried via StUF-ZKN `zakLv01`. The translation layer maps between the two representations without duplicating data.
-
----
-
-## Data Model Mapping
-
-### StUF-ZKN to Procest Case Mapping
-
-| StUF-ZKN Field | XML Path | Procest Case Property | ZGW Mapping | Notes |
-|----------------|----------|----------------------|-------------|-------|
-| `zaakidentificatie` | `zakLk01/object/identificatie` | `identifier` | `identificatie` | Auto-generated if not provided |
-| `omschrijving` | `zakLk01/object/omschrijving` | `title` | `omschrijving` | Required |
-| `toelichting` | `zakLk01/object/toelichting` | `description` | `toelichting` | Optional |
-| `startdatum` | `zakLk01/object/startdatum` | `startDate` | `startdatum` | Format: `YYYYMMDD` -> ISO 8601 |
-| `einddatum` | `zakLk01/object/einddatum` | `endDate` | `einddatum` | Format: `YYYYMMDD` -> ISO 8601 |
-| `einddatumGepland` | `zakLk01/object/einddatumGepland` | `plannedEndDate` | `einddatumGepland` | |
-| `uiterlijkeEinddatumAfdoening` | `zakLk01/object/uiterlijkeEinddatumAfdoening` | `deadline` | `uiterlijkeEinddatumAfdoening` | |
-| `isVan/gerelateerde/code` | Nested element | `caseType` (resolved by code) | `zaaktype` | Resolved via caseType lookup |
-| `vertrouwelijkAanduiding` | `zakLk01/object/vertrouwelijkAanduiding` | `confidentiality` | `vertrouwelijkheidaanduiding` | Enum mapping required |
-| `heeftAlsInitiator` | Nested BSN/vestigingsnummer | `role` (genericRole=initiator) | `rol` | Creates a Role object |
-| `heeftAlsBehandelaar` | Nested medewerker | `assignee` + role | `rol` | Handler assignment |
-
-### StUF-ZKN Status Mapping
-
-| StUF-ZKN Field | XML Path | Procest Property | Notes |
-|----------------|----------|-----------------|-------|
-| `datumStatusGezet` | `heeft/datumStatusGezet` | StatusRecord `dateSet` | Format: `YYYYMMDDHHmmss` -> ISO 8601 |
-| `gpiStatusType/code` | Nested element | StatusRecord `statusType` | Resolved via statusType lookup |
-| `statustoelichting` | `heeft/statustoelichting` | StatusRecord `description` | |
-
-### StUF-BG Person Mapping
-
-| StUF-BG Field | XML Path | Description | OpenRegister Property |
-|---------------|----------|-------------|----------------------|
-| `inp.bsn` | `npsLv01/gelijk/inp.bsn` | Burgerservicenummer | `bsn` |
-| `geslachtsnaam` | `npsLa01/antwoord/.../geslachtsnaam` | Family name | `lastName` |
-| `voorvoegselGeslachtsnaam` | Nested element | Name prefix | `namePrefix` |
-| `voornamen` | Nested element | Given names | `firstName` |
-| `geboortedatum` | `inp.geboortedatum` | Date of birth | `dateOfBirth` |
-| `verblijfsadres` | Nested AOA element | Residential address | `address` (composite) |
-| `sub.verblijfBuitenland` | Nested element | Foreign address | `foreignAddress` |
-
-### Confidentiality Enum Mapping
-
-| StUF Value | Procest Value | ZGW Dutch |
-|------------|--------------|-----------|
-| `OPENBAAR` | `public` | openbaar |
-| `BEPERKT OPENBAAR` | `restricted` | beperkt_openbaar |
-| `INTERN` | `internal` | intern |
-| `ZAAKVERTROUWELIJK` | `case_sensitive` | zaakvertrouwelijk |
-| `VERTROUWELIJK` | `confidential` | vertrouwelijk |
-| `CONFIDENTIEEL` | `highly_confidential` | confidentieel |
-| `GEHEIM` | `secret` | geheim |
-| `ZEER GEHEIM` | `top_secret` | zeer_geheim |
-
----
-
-## Requirements
-
----
-
-### REQ-STUF-001: Outbound StUF-BG Person Lookup via OpenConnector
-
-**Feature tier**: V1
-
-The system MUST support querying external StUF-BG services for person data (BRP) via OpenConnector's SOAP infrastructure. This enables case handlers to look up citizen information by BSN when creating or processing cases.
-
-#### Scenario STUF-001a: Look up person by BSN
-
-- GIVEN an OpenConnector source configured with type `soap` pointing to a StUF-BG endpoint (e.g., `https://brp.gemeente.nl/StUF/bg0310`)
-- AND the source has valid authentication (mTLS certificate or WS-Security credentials)
-- AND the source has stuurgegevens configured (zender: `Procest`, ontvanger: `BRP`)
-- WHEN a case handler requests person lookup for BSN `999993653`
-- THEN the system MUST construct a StUF-BG `npsLv01` SOAP envelope with the BSN in `gelijk/inp.bsn`
-- AND send it to the configured StUF-BG endpoint via OpenConnector's SOAPService
-- AND parse the `npsLa01` response extracting: `geslachtsnaam`, `voorvoegselGeslachtsnaam`, `voornamen`, `geboortedatum`, `verblijfsadres`
-- AND return the person data as a JSON object to the Procest frontend
-
-#### Scenario STUF-001b: Person not found in BRP
-
-- GIVEN a valid StUF-BG source configuration
-- WHEN a lookup is performed for BSN `000000000` (non-existent)
-- THEN the StUF-BG service returns a `npsLa01` response with zero results (empty `antwoord`)
-- AND the system MUST display: "No person found for BSN 000000000"
-
-#### Scenario STUF-001c: StUF-BG fault handling
-
-- GIVEN a valid StUF-BG source configuration
-- WHEN the StUF service returns a `Fo02` fault message (e.g., `StUF055: Niet geautoriseerd`)
-- THEN the system MUST parse the fault code and fault string from the `Fo02` envelope
-- AND return a structured error: `{ "code": "StUF055", "message": "Niet geautoriseerd", "detail": "..." }`
-- AND log the fault at WARNING level
-
----
-
-### REQ-STUF-002: Outbound StUF-ZKN Case Notification
-
-**Feature tier**: V1
-
-The system MUST support sending case status updates to legacy systems via StUF-ZKN messages. This enables Procest to notify legacy zaaksystemen or DMS systems when case events occur.
-
-#### Scenario STUF-002a: Send status update notification
-
-- GIVEN an OpenConnector endpoint configured as type `soap` pointing to a legacy zaaksysteem's StUF-ZKN service
-- AND the case "2026-042" has its status changed from "Ontvangen" to "In behandeling"
-- AND the case type is configured with a StUF notification endpoint
-- WHEN the status change is committed
-- THEN the system MUST construct a StUF-ZKN `zakLk01` SOAP envelope with `mutatiesoort="W"` (wijziging)
-- AND include the updated zaak data: `identificatie`, `omschrijving`, `status` (with `datumStatusGezet` and status type code)
-- AND populate `stuurgegevens` with: `zender` (Procest organization code), `ontvanger` (legacy system code), `referentienummer` (UUID), `tijdstipBericht` (current ISO timestamp)
-- AND send the message via OpenConnector's SOAPService
-
-#### Scenario STUF-002b: Send case creation notification
-
-- GIVEN a StUF notification endpoint is configured
-- WHEN a new case "2026-043" is created via the Procest UI or ZGW API
-- THEN the system MUST construct a StUF-ZKN `zakLk01` SOAP envelope with `mutatiesoort="T"` (toevoeging)
-- AND include all initial zaak data including initiator role (`heeftAlsInitiator`)
-- AND send the message via OpenConnector
-
-#### Scenario STUF-002c: Handle notification delivery failure
-
-- GIVEN a StUF notification endpoint is configured but unreachable
-- WHEN a case status change triggers a notification
-- AND the SOAP call fails with a connection timeout
-- THEN the system MUST NOT block the status change (the case update proceeds)
-- AND the system MUST log the delivery failure at ERROR level
-- AND the system SHOULD queue the notification for retry (via OpenConnector's retry mechanism)
-- AND the audit trail MUST record: "StUF notification to [endpoint] failed: connection timeout"
-
----
-
-### REQ-STUF-003: Outbound StUF-ZKN Document Linking
-
-**Feature tier**: V1
-
-The system MUST support sending document metadata to legacy DMS systems via StUF-ZKN `edcLk01` messages when documents are uploaded to a case. OpenConnector's SOAPService already has `edcLk01` awareness (base64 content handling).
-
-#### Scenario STUF-003a: Notify legacy DMS of new document
-
-- GIVEN a case "2026-042" linked to a StUF-ZKN endpoint
-- AND a document "Bouwtekening.pdf" (1.2 MB) is uploaded to the case
-- WHEN the document upload is committed
-- THEN the system MUST construct a StUF-ZKN `edcLk01` SOAP envelope with `mutatiesoort="T"`
-- AND include: `identificatie` (document ID), `titel` ("Bouwtekening.pdf"), `formaat` ("application/pdf"), `inhoud` (base64-encoded content), `vertrouwelijkAanduiding`
-- AND include the zaak reference: `isRelevantVoor/gerelateerde/identificatie` = "2026-042"
-- AND send via OpenConnector's SOAPService (which already handles `edcLk01` content decoding)
-
-#### Scenario STUF-003b: Large document handling
-
-- GIVEN a document larger than 20 MB
-- WHEN the system prepares the `edcLk01` message
-- THEN the system SHOULD use MTOM (Message Transmission Optimization Mechanism) for binary content
-- OR the system MAY skip the `inhoud` element and include only metadata with a download reference
-
----
-
-### REQ-STUF-004: StUF Stuurgegevens Configuration
-
-**Feature tier**: V1
-
-The system MUST support configuring StUF `stuurgegevens` (message routing metadata) for each StUF endpoint. Stuurgegevens are mandatory on every StUF message and identify the sender and receiver.
-
-#### Scenario STUF-004a: Configure stuurgegevens for a StUF source
-
-- GIVEN an admin configuring a new StUF endpoint in OpenConnector
-- WHEN the admin opens the source configuration
-- THEN the form MUST include fields for:
- - `zender.organisatie` (sending organization code, e.g., "0363" for Amsterdam)
- - `zender.applicatie` (sending application name, e.g., "Procest")
- - `ontvanger.organisatie` (receiving organization code)
- - `ontvanger.applicatie` (receiving application name)
-- AND these values MUST be stored with the source configuration
-
-#### Scenario STUF-004b: Stuurgegevens populated on outbound messages
-
-- GIVEN a StUF source with stuurgegevens configured: zender = `{ organisatie: "0363", applicatie: "Procest" }`, ontvanger = `{ organisatie: "0363", applicatie: "BRP" }`
-- WHEN any outbound StUF message is constructed
-- THEN the `stuurgegevens` element MUST contain:
- - `zender/organisatie` = "0363"
- - `zender/applicatie` = "Procest"
- - `ontvanger/organisatie` = "0363"
- - `ontvanger/applicatie` = "BRP"
- - `referentienummer` = newly generated UUID
- - `tijdstipBericht` = current timestamp in `YYYYMMDDHHmmss` format
-
----
-
-### REQ-STUF-005: StUF-ZKN zaakIdentificatie Generation
-
-**Feature tier**: V1
-
-The system SHOULD support the `genereerZaakIdentificatie` service call for obtaining zaak identifiers from external systems that manage identifier sequences.
-
-#### Scenario STUF-005a: Obtain identifier from legacy system
-
-- GIVEN a StUF-ZKN endpoint that supports `genereerZaakIdentificatie_Di02`
-- AND the case type "Omgevingsvergunning" is configured to use external identifier generation
-- WHEN a new case of this type is being created
-- THEN the system MUST send a `genereerZaakIdentificatie_Di02` message to the configured endpoint
-- AND parse the `genereerZaakIdentificatie_Du02` response to extract the `zaakidentificatie`
-- AND use this identifier as the case's `identifier` instead of auto-generating one
-
-#### Scenario STUF-005b: Fallback to local generation
-
-- GIVEN a case type configured for external identifier generation
-- AND the external StUF endpoint is unreachable
-- WHEN a new case is being created
-- THEN the system MUST fall back to local identifier generation (format: `YYYY-NNN`)
-- AND log a warning: "External zaakidentificatie generation failed, using local identifier"
-
----
-
-### REQ-STUF-006: Outbound StUF Authentication
-
-**Feature tier**: V1
-
-The system MUST support the authentication methods required by Dutch government StUF endpoints. OpenConnector's existing certificate handling and AuthenticationService provide the foundation.
-
-#### Scenario STUF-006a: mTLS with PKIoverheid certificate
-
-- GIVEN a StUF endpoint requiring PKIoverheid mutual TLS
-- AND the admin has uploaded a client certificate and private key in OpenConnector's source configuration
-- WHEN the system sends a StUF SOAP message
-- THEN the SOAP call MUST include the client certificate for mTLS
-- AND the server's certificate MUST be validated against the PKIoverheid certificate chain
-
-#### Scenario STUF-006b: WS-Security UsernameToken
-
-- GIVEN a StUF endpoint requiring WS-Security UsernameToken authentication
-- AND the admin has configured username and password in the source configuration
-- WHEN the system sends a StUF SOAP message
-- THEN the SOAP envelope Header MUST include a `wsse:Security` element with a `wsse:UsernameToken` containing the configured credentials
-- AND the password SHOULD be sent as `wsse:Password Type="PasswordDigest"` (nonce + timestamp + password hash)
-
-#### Scenario STUF-006c: Certificate renewal warning
-
-- GIVEN a StUF source with a PKIoverheid client certificate expiring in 30 days
-- WHEN an admin views the source configuration
-- THEN the system SHOULD display a warning: "Client certificate expires on [date]. Renew before expiry to prevent service interruption."
-
----
-
-### REQ-STUF-007: Inbound StUF-ZKN Case Creation
-
-**Feature tier**: V2
-
-The system MUST accept incoming StUF-ZKN `zakLk01` messages (with `mutatiesoort="T"`) to create cases from legacy form systems or legacy case systems pushing data to Procest. This is the SOAP server challenge -- Nextcloud routes are REST-based, so the inbound StUF endpoint is implemented as a raw POST handler that parses SOAP XML.
-
-#### Scenario STUF-007a: Receive zakLk01 to create a case
-
-- GIVEN a Procest StUF endpoint exposed at `/apps/procest/api/stuf/zaken`
-- AND the endpoint accepts `POST` with `Content-Type: text/xml` or `application/soap+xml`
-- WHEN a legacy formulierensysteem sends a StUF-ZKN `zakLk01` SOAP message with `mutatiesoort="T"` containing:
- - `omschrijving` = "Aanvraag omgevingsvergunning Dorpsstraat 1"
- - `startdatum` = "20260316"
- - `zaaktype/code` = "OV-001"
- - `heeftAlsInitiator/gerelateerde/inp.bsn` = "999993653"
-- THEN the system MUST parse the SOAP envelope and extract the StUF-ZKN message body
-- AND map `omschrijving` to case `title`
-- AND convert `startdatum` from `YYYYMMDD` to ISO 8601 date
-- AND resolve `zaaktype/code` "OV-001" to the matching Procest case type
-- AND create an OpenRegister object in the `procest` register with the `case` schema
-- AND create a Role object with `genericRole = "initiator"` and BSN `999993653`
-- AND auto-calculate the `deadline` from the case type's `processingDeadline`
-- AND return a StUF `Bv01` (bevestigingsbericht) with the generated `zaakidentificatie`
-
-#### Scenario STUF-007b: Reject zakLk01 with unknown case type
-
-- GIVEN the StUF endpoint receives a `zakLk01` with `zaaktype/code` = "UNKNOWN-001"
-- AND no Procest case type has a matching code
-- WHEN the message is processed
-- THEN the system MUST return a StUF `Fo01` fault message with:
- - `foutcode` = "StUF058"
- - `foutbeschrijving` = "Onbekend zaaktype: UNKNOWN-001"
- - `plek` = "server"
-
-#### Scenario STUF-007c: Reject invalid XML
-
-- GIVEN the StUF endpoint receives a POST with malformed XML
-- WHEN the system attempts to parse the message
-- THEN the system MUST return a SOAP Fault with `faultcode = "Client"` and `faultstring = "Ongeldig XML bericht"`
-
-#### Scenario STUF-007d: Validate stuurgegevens on inbound messages
-
-- GIVEN the StUF endpoint is configured with expected `ontvanger` codes
-- WHEN a `zakLk01` arrives with `ontvanger/applicatie` = "WrongApp"
-- THEN the system MUST return a StUF `Fo01` fault with `foutcode` = "StUF001" and `foutbeschrijving` = "Onbekende ontvanger"
-
----
-
-### REQ-STUF-008: Inbound StUF-ZKN Case Query
-
-**Feature tier**: V2
-
-The system MUST accept incoming StUF-ZKN `zakLv01` (zaak opvragen) messages and respond with `zakLa01` (zaak antwoord) messages containing case data from OpenRegister.
-
-#### Scenario STUF-008a: Query case by identifier
-
-- GIVEN a case "2026-042" exists in the `procest` register with title "Bouwvergunning Keizersgracht 100", status "In behandeling", startDate "2026-01-15"
-- WHEN a legacy system sends a `zakLv01` with `gelijk/identificatie` = "2026-042"
-- THEN the system MUST return a `zakLa01` SOAP response containing:
- - `identificatie` = "2026-042"
- - `omschrijving` = "Bouwvergunning Keizersgracht 100"
- - `startdatum` = "20260115"
- - Current status with `datumStatusGezet` and status type code
- - Related roles (`heeftAlsInitiator`, `heeftAlsBehandelaar`)
- - Related documents (if `scope` requests them)
-
-#### Scenario STUF-008b: Query with scope filtering
-
-- GIVEN a `zakLv01` request with a `scope` element requesting only `identificatie`, `omschrijving`, and `startdatum`
-- WHEN the system processes the request
-- THEN the `zakLa01` response MUST include only the requested fields
-- AND omitted fields MUST NOT appear in the response (not even as empty elements)
-
-#### Scenario STUF-008c: Query with no results
-
-- GIVEN a `zakLv01` with `gelijk/identificatie` = "9999-999" (non-existent)
-- WHEN the system processes the request
-- THEN the system MUST return a `zakLa01` with an empty `antwoord` element (zero objects)
-
-#### Scenario STUF-008d: Query with maximumAantal
-
-- GIVEN 50 cases matching the query criteria
-- AND the `zakLv01` specifies `maximumAantal` = 10
-- WHEN the system processes the request
-- THEN the `zakLa01` MUST contain at most 10 zaak objects
-
----
-
-### REQ-STUF-009: Inbound StUF-ZKN Case Update
-
-**Feature tier**: V2
-
-The system MUST accept incoming StUF-ZKN `zakLk01` messages with `mutatiesoort="W"` (wijziging) to update existing cases.
-
-#### Scenario STUF-009a: Update case via zakLk01
-
-- GIVEN a case "2026-042" exists with title "Bouwvergunning Keizersgracht 100"
-- WHEN a legacy system sends a `zakLk01` with `mutatiesoort="W"` and `identificatie` = "2026-042" and `omschrijving` = "Bouwvergunning Keizersgracht 100 - gewijzigd"
-- THEN the system MUST update the case title to "Bouwvergunning Keizersgracht 100 - gewijzigd"
-- AND the audit trail MUST record the update with source "StUF-ZKN"
-- AND the system MUST return a `Bv01` bevestiging
-
-#### Scenario STUF-009b: Update case status via zakLk01
-
-- GIVEN a case "2026-042" at status "Ontvangen"
-- WHEN a `zakLk01` with `mutatiesoort="W"` includes a new status element with `gpiStatusType/code` = "INBEH" and `datumStatusGezet` = "20260316120000"
-- THEN the system MUST resolve "INBEH" to the matching Procest status type
-- AND create a new StatusRecord with the specified date
-- AND all case type validation rules MUST be enforced (required properties, required documents)
-
-#### Scenario STUF-009c: Reject update for non-existent case
-
-- GIVEN no case with identifier "9999-999" exists
-- WHEN a `zakLk01` with `mutatiesoort="W"` and `identificatie` = "9999-999" arrives
-- THEN the system MUST return a `Fo01` fault with `foutcode` = "StUF064" and `foutbeschrijving` = "Zaak niet gevonden: 9999-999"
-
----
-
-### REQ-STUF-010: Inbound StUF-ZKN Document Handling
-
-**Feature tier**: V2
-
-The system MUST accept incoming StUF-ZKN `edcLk01` messages to link documents to cases.
-
-#### Scenario STUF-010a: Receive document via edcLk01
-
-- GIVEN a case "2026-042" exists
-- WHEN a legacy system sends an `edcLk01` with `mutatiesoort="T"` containing:
- - `identificatie` = "DOC-2026-001"
- - `titel` = "Bouwtekening.pdf"
- - `formaat` = "application/pdf"
- - `inhoud` = base64-encoded PDF content
- - `isRelevantVoor/gerelateerde/identificatie` = "2026-042"
-- THEN the system MUST decode the base64 `inhoud`
-- AND store the document in Nextcloud Files under the case's folder
-- AND create a caseDocument object linking the document to the case
-- AND return a `Bv01` bevestiging
-
-#### Scenario STUF-010b: Document without content (metadata only)
-
-- GIVEN an `edcLk01` with document metadata but no `inhoud` element
-- WHEN the message is processed
-- THEN the system MUST create a caseDocument object with the metadata
-- AND mark the document as "metadata only -- no content received"
-
----
-
-### REQ-STUF-011: StUF XML Message Processing
-
-**Feature tier**: V1 (outbound), V2 (inbound)
-
-The system MUST correctly handle StUF XML namespaces, date formats, noValue attributes, and message structure.
-
-#### Scenario STUF-011a: XML namespace handling
-
-- GIVEN a StUF-ZKN message is being constructed or parsed
-- THEN the system MUST correctly handle these namespaces:
- - `http://www.egem.nl/StUF/StUF0301` (StUF base)
- - `http://www.egem.nl/StUF/sector/zkn/0310` (StUF-ZKN)
- - `http://www.egem.nl/StUF/sector/bg/0310` (StUF-BG)
- - `http://www.w3.org/2001/XMLSchema-instance` (xsi)
- - `http://www.opengis.net/gml` (gml, for geometry)
-
-#### Scenario STUF-011b: Date format conversion
-
-- GIVEN a date value "2026-03-16" in ISO 8601 format (Procest internal)
-- WHEN the value is included in an outbound StUF message
-- THEN the value MUST be converted to StUF format: "20260316"
-- AND datetime values MUST use format: "YYYYMMDDHHmmss" (e.g., "20260316143000")
-
-#### Scenario STUF-011c: noValue attribute handling
-
-- GIVEN a case property that has no value (null/empty)
-- WHEN the property is included in an outbound StUF message
-- THEN the element MUST include the appropriate `StUF:noValue` attribute:
- - `geenWaarde` -- explicitly set to no value
- - `waardeOnbekend` -- value exists but is unknown
- - `nietOndersteund` -- field not supported by the system
- - `vastgesteldOnbekend` -- officially determined as unknown
-
-#### Scenario STUF-011d: Validate outbound XML against XSD
-
-- GIVEN bundled StUF-ZKN 3.10 and StUF-BG 3.10 XSD schemas
-- WHEN an outbound StUF message is constructed
-- THEN the system SHOULD validate the XML against the relevant XSD before sending
-- AND if validation fails, the system MUST log the validation errors and NOT send the invalid message
-
----
-
-### REQ-STUF-012: StUF-BG Inbound Person Query
-
-**Feature tier**: V2
-
-The system SHOULD accept incoming StUF-BG `npsLv01` messages to expose person data stored in OpenRegister. This enables legacy systems to query Procest as if it were a BRP source.
-
-#### Scenario STUF-012a: Receive person query
-
-- GIVEN the Procest StUF endpoint at `/apps/procest/api/stuf/personen`
-- AND a person object with BSN "999993653" exists in OpenRegister
-- WHEN a legacy system sends a StUF-BG `npsLv01` with `gelijk/inp.bsn` = "999993653"
-- THEN the system MUST return a `npsLa01` response with the person data mapped from OpenRegister to StUF-BG XML
-
-#### Scenario STUF-012b: Person query with scope
-
-- GIVEN a `npsLv01` with scope requesting only `inp.bsn`, `geslachtsnaam`, and `geboortedatum`
-- WHEN the system processes the request
-- THEN the `npsLa01` MUST include only the requested fields
-
----
-
-### REQ-STUF-013: StUF Field Mapping Configuration
-
-**Feature tier**: V1
-
-The system MUST store field mappings between StUF XML paths and OpenRegister object properties as configurable mapping objects. Default mappings for ZGW-zaak and BRP-person data MUST be pre-seeded.
-
-#### Scenario STUF-013a: Pre-seeded zaak field mapping
-
-- GIVEN the Procest app is installed and the repair step runs
-- THEN a default StUF-ZKN field mapping MUST be created in OpenRegister containing mappings for all fields listed in the "StUF-ZKN to Procest Case Mapping" table above
-- AND the mapping MUST include date format transformations (StUF `YYYYMMDD` to ISO 8601 and vice versa)
-
-#### Scenario STUF-013b: Custom field mapping
-
-- GIVEN a municipality uses a StUF extension with custom fields (e.g., `gem:kenmerk` for a local reference number)
-- WHEN an admin adds a custom mapping entry: StUF path `gem:kenmerk` -> OpenRegister property `localReference`
-- THEN the system MUST apply this mapping during StUF message parsing and construction
-- AND the custom mapping MUST NOT override default mappings unless explicitly configured
-
-#### Scenario STUF-013c: Value transformation in mapping
-
-- GIVEN a mapping entry for `vertrouwelijkAanduiding` with a value transformation table (StUF enum values to Procest enum values)
-- WHEN a StUF message contains `vertrouwelijkAanduiding` = "ZAAKVERTROUWELIJK"
-- THEN the system MUST transform the value to `case_sensitive` using the mapping's transformation table
-
----
-
-### REQ-STUF-014: SOAP Server Within Nextcloud
-
-**Feature tier**: V2
-
-Exposing inbound StUF endpoints within Nextcloud requires a SOAP server implementation. Since Nextcloud routes are REST-based, the StUF controller accepts raw XML POSTs and processes them as SOAP messages without using PHP's built-in SoapServer (which requires WSDL mode and conflicts with Nextcloud's routing).
-
-#### Scenario STUF-014a: Raw SOAP POST handling
-
-- GIVEN a Procest route `/apps/procest/api/stuf/{service}` registered as a raw POST endpoint
-- WHEN a SOAP message arrives with `Content-Type: text/xml; charset=utf-8`
-- THEN the controller MUST read the raw request body (`php://input`)
-- AND parse the SOAP envelope using PHP's DOMDocument or SimpleXML
-- AND extract the SOAP Body content
-- AND dispatch to the appropriate handler based on the root element name (e.g., `zakLk01`, `zakLv01`, `npsLv01`)
-- AND construct a SOAP envelope response with the appropriate content
-- AND return with `Content-Type: text/xml; charset=utf-8`
-
-#### Scenario STUF-014b: WSDL serving
-
-- GIVEN bundled WSDL files for StUF-ZKN and StUF-BG services
-- WHEN a client sends a GET request to `/apps/procest/api/stuf/zaken?wsdl`
-- THEN the system MUST return the StUF-ZKN WSDL file with `Content-Type: text/xml`
-- AND the WSDL's `soap:address location` MUST reflect the actual Procest endpoint URL
-
-#### Scenario STUF-014c: SOAPAction header routing
-
-- GIVEN a SOAP request with `SOAPAction: "http://www.egem.nl/StUF/sector/zkn/0310/zakLk01"`
-- WHEN the controller processes the request
-- THEN the SOAPAction header MAY be used as a secondary dispatch mechanism alongside XML body inspection
-
----
-
-### REQ-STUF-015: Dual API Coexistence
-
-**Feature tier**: V1
-
-The system MUST ensure that StUF and ZGW APIs provide consistent views of the same data. Changes made via one protocol MUST be immediately visible via the other.
-
-#### Scenario STUF-015a: Case created via StUF visible in ZGW
-
-- GIVEN a case created via inbound StUF-ZKN `zakLk01` with identifier "2026-042"
-- WHEN a ZGW API client sends GET `/api/v1/zaken?identificatie=2026-042` to the ZrcController
-- THEN the case MUST be returned with all fields populated from the StUF-created data
-- AND the `url` field MUST contain the ZGW-style self-link
-
-#### Scenario STUF-015b: Case created via ZGW queryable via StUF
-
-- GIVEN a case created via the ZGW Zaken API with identifier "2026-043"
-- WHEN a legacy system sends a StUF-ZKN `zakLv01` with `gelijk/identificatie` = "2026-043"
-- THEN the case MUST be returned in the `zakLa01` response with all fields correctly mapped to StUF-ZKN XML
-
-#### Scenario STUF-015c: Case updated via UI reflected in StUF query
-
-- GIVEN a case "2026-042" with status "Ontvangen"
-- WHEN a handler changes the status to "In behandeling" via the Procest frontend
-- AND a legacy system immediately sends a `zakLv01` for "2026-042"
-- THEN the response MUST show the new status "In behandeling" with the correct `datumStatusGezet`
-
----
-
-## SOAP Server Challenge
-
-Hosting a SOAP server within Nextcloud presents architectural challenges:
-
-1. **Nextcloud routing is REST-based**: All routes go through `routes.php` and expect JSON request/response. StUF requires raw XML POST handling with SOAP envelope wrapping.
-
-2. **PHP SoapServer limitations**: PHP's built-in `SoapServer` class operates in WSDL mode and expects to handle the full HTTP lifecycle. Within Nextcloud's controller framework, this conflicts with the existing request/response handling.
-
-3. **Proposed solution**: Implement a `StufController` that extends `OCP\AppFramework\Controller` and:
- - Registers routes for `/api/stuf/zaken` and `/api/stuf/personen`
- - Reads raw XML from `php://input`
- - Parses XML using DOMDocument (not SoapServer)
- - Dispatches to a `StufMessageHandler` service
- - Constructs SOAP XML responses manually
- - Returns `DataDisplayResponse` with `Content-Type: text/xml`
-
-4. **Alternative approach**: Use OpenConnector as a SOAP proxy -- OpenConnector could host the SOAP endpoint (it already has SOAPService) and forward parsed data to Procest's REST API. This avoids the SOAP-in-Nextcloud problem but adds a network hop and dependency.
-
----
-
-## Dependencies
-
-- **OpenConnector stuf-adapter spec** (`openconnector/openspec/specs/stuf-adapter/spec.md`): Provides the SOAP infrastructure (SOAPService), certificate handling, and source type configuration. Procest leverages OpenConnector for all outbound StUF communication.
-- **OpenRegister**: All case data stored as objects; field mapping configurations stored as OpenRegister objects.
-- **Procest case-management spec** (`../case-management/spec.md`): The case data model, status lifecycle, validation rules, and audit trail that StUF messages map to/from.
-- **Procest roles-decisions spec** (`../roles-decisions/spec.md`): Role entities created from StUF `heeftAlsInitiator`/`heeftAlsBehandelaar` data.
-- **Procest openregister-integration spec** (`../openregister-integration/spec.md`): The `procest` register and 12 schemas that store all data.
-- **PHP DOMDocument / SimpleXML**: For XML parsing and construction (bundled with PHP).
-- **StUF XSD schema packages**: StUF-BG 3.10 and StUF-ZKN 3.10 XSD files for validation.
-- **Existing ZGW controllers** (`ZrcController`, `ZtcController`, `DrcController`, `BrcController`): The REST API surface that coexists with StUF.
-
----
-
-## Current Implementation Status
-
-### Using Mock Register Data
-
-This spec depends on the **BRP** mock register for testing StUF-BG person lookup (REQ-STUF-001) and the **BAG** mock register for address data.
-
-**Loading the registers:**
-```bash
-# Load BRP register (35 persons, register slug: "brp", schema: "ingeschreven-persoon")
-docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/brp_register.json
-
-# Load BAG register (32 addresses + 21 objects + 21 buildings, register slug: "bag")
-docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/bag_register.json
-```
-
-**Test data for this spec's use cases:**
-- **StUF-BG npsLv01 person lookup**: BSN `999993653` (Suzanne Moulin) -- test outbound BRP query and npsLa01 response mapping
-- **StUF-BG person not found**: BSN `000000000` -- test empty npsLa01 response handling
-- **StUF-ZKN with initiator BSN**: BSN `999990627` (Stephan Janssen) -- test inbound zakLk01 with `heeftAlsInitiator/inp.bsn`
-- **StUF-BG inbound query (REQ-STUF-012)**: BSN `999992570` (Albert Vogel) -- test serving person data via StUF-BG endpoint
-
-### Procest
-
-**No StUF implementation exists.** Grep for "stuf", "StUF", and "soap" in `procest/lib/` returns zero results. All current API communication is via ZGW REST APIs through the four ZGW controllers.
-
-**Implemented (ZGW foundation that StUF maps to):**
-- ZGW Zaken API via `ZrcController` -- zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten, zaakobjecten, klantcontacten
-- ZGW Catalogi API via `ZtcController` -- zaaktypen, statustypen, resultaattypen, roltypen, besluittypen
-- ZGW Documenten API via `DrcController` -- enkelvoudiginformatieobjecten
-- ZGW Besluiten API via `BrcController` -- besluiten
-- ZGW business rules via `ZgwBusinessRulesService` and `ZgwZrcRulesService`
-- OpenRegister schemas for all 12 entity types
-
-### OpenConnector
-
-**Partial SOAP infrastructure exists** (see `openconnector/openspec/specs/stuf-adapter/spec.md` for details):
-- `SOAPService` with generic SOAP client, WSDL-driven requests, SOAP 1.1/1.2 support
-- Specific StUF-ZKN `edcLk01` handling (base64 document content decoding)
-- Source type `soap` with WSDL URL and authentication configuration
-- Certificate handling for mTLS (PKIoverheid)
-- `CallService` SOAP routing (type `soap` -> SOAPService)
-- **NOT implemented in OpenConnector**: Inbound SOAP server, StUF field mappings, WSDL bundling, stuurgegevens, namespace handling, noValue attributes, fault message handling (Fo01/Fo02/Fo03/Bv03)
-
----
-
-## Standards & References
-
-- **StUF 3.01**: Standaard Uitwisseling Formaat, base standard. Defines the SOAP message structure, stuurgegevens, kennisgevingen (Lk01), vraag/antwoord (Lv01/La01), bevestiging (Bv01/Bv03), and foutmeldingen (Fo01/Fo02/Fo03). Maintained by VNG Realisatie. https://www.gemmaonline.nl/index.php/StUF_Berichtenstandaard
-- **StUF-ZKN 3.10**: Sectormodel Zaak-/Documentservices, built on StUF 3.01. Defines zaak (zak), document (edc), status, and related message types. The "e" extension (3.10e) adds extra message types. https://www.gemmaonline.nl/index.php/Sectormodel_Zaken:_StUF-ZKN
-- **StUF-BG 3.10**: Sectormodel Basisgegevens, built on StUF 3.01. Defines person (nps), address (adr), and other base registry data types. https://www.gemmaonline.nl/index.php/Sectormodel_Basisgegevens:_StUF-BG
-- **RGBZ 2.0 (Referentiemodel Gemeentelijke Basisgegevens Zaken)**: The information model underlying StUF-ZKN, defining zaak, status, document, besluit, and their relationships. The same model underlies ZGW APIs.
-- **ZGW APIs (VNG)**: The modern REST-based successor to StUF-ZKN. Procest already implements these via ZrcController, ZtcController, DrcController, BrcController. https://vng-realisatie.github.io/gemma-zaken/
-- **GEMMA**: Gemeentelijke Model Architectuur, the reference architecture for Dutch municipalities. Defines how StUF and ZGW fit in the municipal information landscape. https://www.gemmaonline.nl/
-- **Logius**: Dutch government IT authority responsible for PKIoverheid certificates and DigiKoppeling (the transport standard for government-to-government communication, which mandates how StUF messages are exchanged).
-- **DigiKoppeling**: Transport standard (WUS/ebMS) for Dutch government interoperability. StUF messages are typically exchanged over DigiKoppeling WUS (SOAP+WS-Security). https://www.logius.nl/domeinen/gegevensuitwisseling/digikoppeling
-- **WS-Security (OASIS)**: SOAP message security standard. UsernameToken and X.509 Token profiles are used by Dutch government StUF endpoints.
-- **PKIoverheid**: Dutch government PKI for mTLS authentication on production StUF endpoints.
-- **MTOM (Message Transmission Optimization Mechanism)**: W3C standard for efficient binary content in SOAP messages, relevant for large document transfers via edcLk01.
-
----
-
-## Specificity Assessment
-
-### Sufficient for implementation
-- Complete field mapping tables between StUF-ZKN/BG and Procest case model
-- Clear separation of outbound (V1, via OpenConnector) and inbound (V2, SOAP server) concerns
-- Detailed Gherkin scenarios for each message type with concrete data examples
-- Explicit SOAP server architecture proposal addressing the Nextcloud routing challenge
-- Coexistence principle ensuring data consistency between StUF and ZGW APIs
-- Authentication methods (mTLS, WS-Security) aligned with existing OpenConnector capabilities
-- Reference to OpenConnector's existing SOAPService and edcLk01 handling
-
-### Missing or ambiguous
-- **StUF version negotiation**: The spec targets 3.01/3.10 but some municipalities may run older versions. Version detection and fallback behavior is not defined.
-- **Async response patterns**: StUF supports asynchronous Bv03/Fo03 callback patterns for long-running operations. The callback mechanism (how Procest receives async responses) is not detailed.
-- **Performance requirements**: No throughput or latency SLAs for SOAP message processing.
-- **Mapping object schema**: REQ-STUF-013 describes configurable field mappings but does not define the OpenRegister schema structure for mapping objects (which fields, what register).
-- **Multi-source routing**: Can multiple StUF endpoints be configured for different case types, or is there one global StUF endpoint?
-- **StUF-ZKN 3.10e extensions**: The "e" extension adds extra message types not covered in this spec.
-- **Archival-related StUF messages**: StUF defines messages for archival transfers (overbrenging) which relate to the case result archival rules but are not covered here.
-- **Rate limiting and access control**: How are inbound StUF endpoints secured beyond stuurgegevens validation? IP whitelisting? Client certificate validation on the inbound side?
-
-### Open questions
-1. Should inbound StUF endpoints be hosted in Procest directly or proxied via OpenConnector (which already has SOAPService)?
-2. Which StUF version(s) must be supported on day one -- 3.01 only, 3.10 only, or both?
-3. Should the field mapping configuration be stored in the `procest` register or in OpenConnector's register?
-4. How should large document content in inbound `edcLk01` messages be handled -- streamed to disk or loaded into memory?
-5. Is DigiKoppeling (WUS profile) compliance required, or is plain HTTPS with WS-Security sufficient?
+# StUF Protocol Support Specification
+
+## Purpose
+
+Procest currently implements ZGW APIs (Zaken, Catalogi, Documenten, Besluiten) for case management via REST controllers (`ZrcController`, `ZtcController`, `DrcController`, `BrcController`). However, many Dutch municipalities still rely on StUF (Standaard Uitwisseling Formaat) -- especially StUF-ZKN (Zaak-Kennis) for case management and StUF-BG (Basis Gemeentelijke) for person/address lookups. This spec defines how Procest supports StUF alongside ZGW, providing a dual API surface over the same OpenRegister case data.
+
+StUF support enables Procest to integrate with legacy form systems (e.g., formulierenmotoren that submit cases via StUF-ZKN), legacy case systems during migration periods, and BRP person lookups via StUF-BG. The approach leverages OpenConnector's existing SOAP infrastructure (SOAPService with StUF-ZKN `edcLk01` awareness) for outbound StUF calls, while adding inbound StUF endpoints to Procest for receiving SOAP messages from legacy consumers.
+
+**Standards**: StUF 3.01, StUF-ZKN 3.10, StUF-BG 3.10, ZGW APIs (VNG), RGBZ, GEMMA
+**Feature tier**: V1 (outbound StUF via OpenConnector), V2 (inbound StUF endpoints in Procest)
+
+---
+
+## Architecture Overview
+
+```
+ Inbound StUF (V2)
+ Legacy systems send SOAP messages to Procest
+ ┌──────────────────────────────────┐
+ │ Legacy Formulierensysteem │
+ │ Legacy Zaaksysteem │
+ └────────────┬─────────────────────┘
+ │ SOAP (StUF-ZKN/BG)
+ ┌────────────▼─────────────────────┐
+ │ Procest StUF Controller │
+ │ - Raw XML POST handler │
+ │ - StUF message parser │
+ │ - StUF → OpenRegister mapper │
+ └────────────┬─────────────────────┘
+ │ Internal API
+┌──────────────────────────────────────▼──────────────────────┐
+│ OpenRegister (procest register) │
+│ - case, caseType, status, role, result, decision schemas │
+│ - Single source of truth for all case data │
+└──────────────────────────────────────▲──────────────────────┘
+ │ Internal API
+ ┌────────────┴─────────────────────┐
+ │ OpenConnector (SOAP outbound) │
+ │ - SOAPService (existing) │
+ │ - StUF-ZKN/BG message builder │
+ │ - mTLS / WS-Security auth │
+ └────────────┬─────────────────────┘
+ │ SOAP (StUF-ZKN/BG)
+ ┌────────────▼─────────────────────┐
+ │ External Legacy Services │
+ │ - BRP (StUF-BG personen) │
+ │ - Legacy zaaksysteem │
+ │ - Gemeentelijk DMS │
+ └──────────────────────────────────┘
+ Outbound StUF (V1)
+ Procest/OpenConnector queries legacy systems
+```
+
+### Coexistence Principle
+
+StUF and ZGW share the same underlying OpenRegister data. A case created via StUF-ZKN `zakLk01` is stored in the same `case` schema and is immediately visible via the ZGW Zaken API (`ZrcController`) and the Procest frontend. Conversely, a case created via the ZGW API or UI can be queried via StUF-ZKN `zakLv01`. The translation layer maps between the two representations without duplicating data.
+
+---
+
+## Data Model Mapping
+
+### StUF-ZKN to Procest Case Mapping
+
+| StUF-ZKN Field | XML Path | Procest Case Property | ZGW Mapping | Notes |
+|----------------|----------|----------------------|-------------|-------|
+| `zaakidentificatie` | `zakLk01/object/identificatie` | `identifier` | `identificatie` | Auto-generated if not provided |
+| `omschrijving` | `zakLk01/object/omschrijving` | `title` | `omschrijving` | Required |
+| `toelichting` | `zakLk01/object/toelichting` | `description` | `toelichting` | Optional |
+| `startdatum` | `zakLk01/object/startdatum` | `startDate` | `startdatum` | Format: `YYYYMMDD` -> ISO 8601 |
+| `einddatum` | `zakLk01/object/einddatum` | `endDate` | `einddatum` | Format: `YYYYMMDD` -> ISO 8601 |
+| `einddatumGepland` | `zakLk01/object/einddatumGepland` | `plannedEndDate` | `einddatumGepland` | |
+| `uiterlijkeEinddatumAfdoening` | `zakLk01/object/uiterlijkeEinddatumAfdoening` | `deadline` | `uiterlijkeEinddatumAfdoening` | |
+| `isVan/gerelateerde/code` | Nested element | `caseType` (resolved by code) | `zaaktype` | Resolved via caseType lookup |
+| `vertrouwelijkAanduiding` | `zakLk01/object/vertrouwelijkAanduiding` | `confidentiality` | `vertrouwelijkheidaanduiding` | Enum mapping required |
+| `heeftAlsInitiator` | Nested BSN/vestigingsnummer | `role` (genericRole=initiator) | `rol` | Creates a Role object |
+| `heeftAlsBehandelaar` | Nested medewerker | `assignee` + role | `rol` | Handler assignment |
+
+### StUF-ZKN Status Mapping
+
+| StUF-ZKN Field | XML Path | Procest Property | Notes |
+|----------------|----------|-----------------|-------|
+| `datumStatusGezet` | `heeft/datumStatusGezet` | StatusRecord `dateSet` | Format: `YYYYMMDDHHmmss` -> ISO 8601 |
+| `gpiStatusType/code` | Nested element | StatusRecord `statusType` | Resolved via statusType lookup |
+| `statustoelichting` | `heeft/statustoelichting` | StatusRecord `description` | |
+
+### StUF-BG Person Mapping
+
+| StUF-BG Field | XML Path | Description | OpenRegister Property |
+|---------------|----------|-------------|----------------------|
+| `inp.bsn` | `npsLv01/gelijk/inp.bsn` | Burgerservicenummer | `bsn` |
+| `geslachtsnaam` | `npsLa01/antwoord/.../geslachtsnaam` | Family name | `lastName` |
+| `voorvoegselGeslachtsnaam` | Nested element | Name prefix | `namePrefix` |
+| `voornamen` | Nested element | Given names | `firstName` |
+| `geboortedatum` | `inp.geboortedatum` | Date of birth | `dateOfBirth` |
+| `verblijfsadres` | Nested AOA element | Residential address | `address` (composite) |
+| `sub.verblijfBuitenland` | Nested element | Foreign address | `foreignAddress` |
+
+### Confidentiality Enum Mapping
+
+| StUF Value | Procest Value | ZGW Dutch |
+|------------|--------------|-----------|
+| `OPENBAAR` | `public` | openbaar |
+| `BEPERKT OPENBAAR` | `restricted` | beperkt_openbaar |
+| `INTERN` | `internal` | intern |
+| `ZAAKVERTROUWELIJK` | `case_sensitive` | zaakvertrouwelijk |
+| `VERTROUWELIJK` | `confidential` | vertrouwelijk |
+| `CONFIDENTIEEL` | `highly_confidential` | confidentieel |
+| `GEHEIM` | `secret` | geheim |
+| `ZEER GEHEIM` | `top_secret` | zeer_geheim |
+
+---
+
+## Requirements
+
+---
+
+### REQ-STUF-001: Outbound StUF-BG Person Lookup via OpenConnector
+
+**Feature tier**: V1
+
+The system MUST support querying external StUF-BG services for person data (BRP) via OpenConnector's SOAP infrastructure. This enables case handlers to look up citizen information by BSN when creating or processing cases.
+
+#### Scenario STUF-001a: Look up person by BSN
+
+- GIVEN an OpenConnector source configured with type `soap` pointing to a StUF-BG endpoint (e.g., `https://brp.gemeente.nl/StUF/bg0310`)
+- AND the source has valid authentication (mTLS certificate or WS-Security credentials)
+- AND the source has stuurgegevens configured (zender: `Procest`, ontvanger: `BRP`)
+- WHEN a case handler requests person lookup for BSN `999993653`
+- THEN the system MUST construct a StUF-BG `npsLv01` SOAP envelope with the BSN in `gelijk/inp.bsn`
+- AND send it to the configured StUF-BG endpoint via OpenConnector's SOAPService
+- AND parse the `npsLa01` response extracting: `geslachtsnaam`, `voorvoegselGeslachtsnaam`, `voornamen`, `geboortedatum`, `verblijfsadres`
+- AND return the person data as a JSON object to the Procest frontend
+
+#### Scenario STUF-001b: Person not found in BRP
+
+- GIVEN a valid StUF-BG source configuration
+- WHEN a lookup is performed for BSN `000000000` (non-existent)
+- THEN the StUF-BG service returns a `npsLa01` response with zero results (empty `antwoord`)
+- AND the system MUST display: "No person found for BSN 000000000"
+
+#### Scenario STUF-001c: StUF-BG fault handling
+
+- GIVEN a valid StUF-BG source configuration
+- WHEN the StUF service returns a `Fo02` fault message (e.g., `StUF055: Niet geautoriseerd`)
+- THEN the system MUST parse the fault code and fault string from the `Fo02` envelope
+- AND return a structured error: `{ "code": "StUF055", "message": "Niet geautoriseerd", "detail": "..." }`
+- AND log the fault at WARNING level
+
+---
+
+### REQ-STUF-002: Outbound StUF-ZKN Case Notification
+
+**Feature tier**: V1
+
+The system MUST support sending case status updates to legacy systems via StUF-ZKN messages. This enables Procest to notify legacy zaaksystemen or DMS systems when case events occur.
+
+#### Scenario STUF-002a: Send status update notification
+
+- GIVEN an OpenConnector endpoint configured as type `soap` pointing to a legacy zaaksysteem's StUF-ZKN service
+- AND the case "2026-042" has its status changed from "Ontvangen" to "In behandeling"
+- AND the case type is configured with a StUF notification endpoint
+- WHEN the status change is committed
+- THEN the system MUST construct a StUF-ZKN `zakLk01` SOAP envelope with `mutatiesoort="W"` (wijziging)
+- AND include the updated zaak data: `identificatie`, `omschrijving`, `status` (with `datumStatusGezet` and status type code)
+- AND populate `stuurgegevens` with: `zender` (Procest organization code), `ontvanger` (legacy system code), `referentienummer` (UUID), `tijdstipBericht` (current ISO timestamp)
+- AND send the message via OpenConnector's SOAPService
+
+#### Scenario STUF-002b: Send case creation notification
+
+- GIVEN a StUF notification endpoint is configured
+- WHEN a new case "2026-043" is created via the Procest UI or ZGW API
+- THEN the system MUST construct a StUF-ZKN `zakLk01` SOAP envelope with `mutatiesoort="T"` (toevoeging)
+- AND include all initial zaak data including initiator role (`heeftAlsInitiator`)
+- AND send the message via OpenConnector
+
+#### Scenario STUF-002c: Handle notification delivery failure
+
+- GIVEN a StUF notification endpoint is configured but unreachable
+- WHEN a case status change triggers a notification
+- AND the SOAP call fails with a connection timeout
+- THEN the system MUST NOT block the status change (the case update proceeds)
+- AND the system MUST log the delivery failure at ERROR level
+- AND the system SHOULD queue the notification for retry (via OpenConnector's retry mechanism)
+- AND the audit trail MUST record: "StUF notification to [endpoint] failed: connection timeout"
+
+---
+
+### REQ-STUF-003: Outbound StUF-ZKN Document Linking
+
+**Feature tier**: V1
+
+The system MUST support sending document metadata to legacy DMS systems via StUF-ZKN `edcLk01` messages when documents are uploaded to a case. OpenConnector's SOAPService already has `edcLk01` awareness (base64 content handling).
+
+#### Scenario STUF-003a: Notify legacy DMS of new document
+
+- GIVEN a case "2026-042" linked to a StUF-ZKN endpoint
+- AND a document "Bouwtekening.pdf" (1.2 MB) is uploaded to the case
+- WHEN the document upload is committed
+- THEN the system MUST construct a StUF-ZKN `edcLk01` SOAP envelope with `mutatiesoort="T"`
+- AND include: `identificatie` (document ID), `titel` ("Bouwtekening.pdf"), `formaat` ("application/pdf"), `inhoud` (base64-encoded content), `vertrouwelijkAanduiding`
+- AND include the zaak reference: `isRelevantVoor/gerelateerde/identificatie` = "2026-042"
+- AND send via OpenConnector's SOAPService (which already handles `edcLk01` content decoding)
+
+#### Scenario STUF-003b: Large document handling
+
+- GIVEN a document larger than 20 MB
+- WHEN the system prepares the `edcLk01` message
+- THEN the system SHOULD use MTOM (Message Transmission Optimization Mechanism) for binary content
+- OR the system MAY skip the `inhoud` element and include only metadata with a download reference
+
+---
+
+### REQ-STUF-004: StUF Stuurgegevens Configuration
+
+**Feature tier**: V1
+
+The system MUST support configuring StUF `stuurgegevens` (message routing metadata) for each StUF endpoint. Stuurgegevens are mandatory on every StUF message and identify the sender and receiver.
+
+#### Scenario STUF-004a: Configure stuurgegevens for a StUF source
+
+- GIVEN an admin configuring a new StUF endpoint in OpenConnector
+- WHEN the admin opens the source configuration
+- THEN the form MUST include fields for:
+ - `zender.organisatie` (sending organization code, e.g., "0363" for Amsterdam)
+ - `zender.applicatie` (sending application name, e.g., "Procest")
+ - `ontvanger.organisatie` (receiving organization code)
+ - `ontvanger.applicatie` (receiving application name)
+- AND these values MUST be stored with the source configuration
+
+#### Scenario STUF-004b: Stuurgegevens populated on outbound messages
+
+- GIVEN a StUF source with stuurgegevens configured: zender = `{ organisatie: "0363", applicatie: "Procest" }`, ontvanger = `{ organisatie: "0363", applicatie: "BRP" }`
+- WHEN any outbound StUF message is constructed
+- THEN the `stuurgegevens` element MUST contain:
+ - `zender/organisatie` = "0363"
+ - `zender/applicatie` = "Procest"
+ - `ontvanger/organisatie` = "0363"
+ - `ontvanger/applicatie` = "BRP"
+ - `referentienummer` = newly generated UUID
+ - `tijdstipBericht` = current timestamp in `YYYYMMDDHHmmss` format
+
+---
+
+### REQ-STUF-005: StUF-ZKN zaakIdentificatie Generation
+
+**Feature tier**: V1
+
+The system SHOULD support the `genereerZaakIdentificatie` service call for obtaining zaak identifiers from external systems that manage identifier sequences.
+
+#### Scenario STUF-005a: Obtain identifier from legacy system
+
+- GIVEN a StUF-ZKN endpoint that supports `genereerZaakIdentificatie_Di02`
+- AND the case type "Omgevingsvergunning" is configured to use external identifier generation
+- WHEN a new case of this type is being created
+- THEN the system MUST send a `genereerZaakIdentificatie_Di02` message to the configured endpoint
+- AND parse the `genereerZaakIdentificatie_Du02` response to extract the `zaakidentificatie`
+- AND use this identifier as the case's `identifier` instead of auto-generating one
+
+#### Scenario STUF-005b: Fallback to local generation
+
+- GIVEN a case type configured for external identifier generation
+- AND the external StUF endpoint is unreachable
+- WHEN a new case is being created
+- THEN the system MUST fall back to local identifier generation (format: `YYYY-NNN`)
+- AND log a warning: "External zaakidentificatie generation failed, using local identifier"
+
+---
+
+### REQ-STUF-006: Outbound StUF Authentication
+
+**Feature tier**: V1
+
+The system MUST support the authentication methods required by Dutch government StUF endpoints. OpenConnector's existing certificate handling and AuthenticationService provide the foundation.
+
+#### Scenario STUF-006a: mTLS with PKIoverheid certificate
+
+- GIVEN a StUF endpoint requiring PKIoverheid mutual TLS
+- AND the admin has uploaded a client certificate and private key in OpenConnector's source configuration
+- WHEN the system sends a StUF SOAP message
+- THEN the SOAP call MUST include the client certificate for mTLS
+- AND the server's certificate MUST be validated against the PKIoverheid certificate chain
+
+#### Scenario STUF-006b: WS-Security UsernameToken
+
+- GIVEN a StUF endpoint requiring WS-Security UsernameToken authentication
+- AND the admin has configured username and password in the source configuration
+- WHEN the system sends a StUF SOAP message
+- THEN the SOAP envelope Header MUST include a `wsse:Security` element with a `wsse:UsernameToken` containing the configured credentials
+- AND the password SHOULD be sent as `wsse:Password Type="PasswordDigest"` (nonce + timestamp + password hash)
+
+#### Scenario STUF-006c: Certificate renewal warning
+
+- GIVEN a StUF source with a PKIoverheid client certificate expiring in 30 days
+- WHEN an admin views the source configuration
+- THEN the system SHOULD display a warning: "Client certificate expires on [date]. Renew before expiry to prevent service interruption."
+
+---
+
+### REQ-STUF-007: Inbound StUF-ZKN Case Creation
+
+**Feature tier**: V2
+
+The system MUST accept incoming StUF-ZKN `zakLk01` messages (with `mutatiesoort="T"`) to create cases from legacy form systems or legacy case systems pushing data to Procest. This is the SOAP server challenge -- Nextcloud routes are REST-based, so the inbound StUF endpoint is implemented as a raw POST handler that parses SOAP XML.
+
+#### Scenario STUF-007a: Receive zakLk01 to create a case
+
+- GIVEN a Procest StUF endpoint exposed at `/apps/procest/api/stuf/zaken`
+- AND the endpoint accepts `POST` with `Content-Type: text/xml` or `application/soap+xml`
+- WHEN a legacy formulierensysteem sends a StUF-ZKN `zakLk01` SOAP message with `mutatiesoort="T"` containing:
+ - `omschrijving` = "Aanvraag omgevingsvergunning Dorpsstraat 1"
+ - `startdatum` = "20260316"
+ - `zaaktype/code` = "OV-001"
+ - `heeftAlsInitiator/gerelateerde/inp.bsn` = "999993653"
+- THEN the system MUST parse the SOAP envelope and extract the StUF-ZKN message body
+- AND map `omschrijving` to case `title`
+- AND convert `startdatum` from `YYYYMMDD` to ISO 8601 date
+- AND resolve `zaaktype/code` "OV-001" to the matching Procest case type
+- AND create an OpenRegister object in the `procest` register with the `case` schema
+- AND create a Role object with `genericRole = "initiator"` and BSN `999993653`
+- AND auto-calculate the `deadline` from the case type's `processingDeadline`
+- AND return a StUF `Bv01` (bevestigingsbericht) with the generated `zaakidentificatie`
+
+#### Scenario STUF-007b: Reject zakLk01 with unknown case type
+
+- GIVEN the StUF endpoint receives a `zakLk01` with `zaaktype/code` = "UNKNOWN-001"
+- AND no Procest case type has a matching code
+- WHEN the message is processed
+- THEN the system MUST return a StUF `Fo01` fault message with:
+ - `foutcode` = "StUF058"
+ - `foutbeschrijving` = "Onbekend zaaktype: UNKNOWN-001"
+ - `plek` = "server"
+
+#### Scenario STUF-007c: Reject invalid XML
+
+- GIVEN the StUF endpoint receives a POST with malformed XML
+- WHEN the system attempts to parse the message
+- THEN the system MUST return a SOAP Fault with `faultcode = "Client"` and `faultstring = "Ongeldig XML bericht"`
+
+#### Scenario STUF-007d: Validate stuurgegevens on inbound messages
+
+- GIVEN the StUF endpoint is configured with expected `ontvanger` codes
+- WHEN a `zakLk01` arrives with `ontvanger/applicatie` = "WrongApp"
+- THEN the system MUST return a StUF `Fo01` fault with `foutcode` = "StUF001" and `foutbeschrijving` = "Onbekende ontvanger"
+
+---
+
+### REQ-STUF-008: Inbound StUF-ZKN Case Query
+
+**Feature tier**: V2
+
+The system MUST accept incoming StUF-ZKN `zakLv01` (zaak opvragen) messages and respond with `zakLa01` (zaak antwoord) messages containing case data from OpenRegister.
+
+#### Scenario STUF-008a: Query case by identifier
+
+- GIVEN a case "2026-042" exists in the `procest` register with title "Bouwvergunning Keizersgracht 100", status "In behandeling", startDate "2026-01-15"
+- WHEN a legacy system sends a `zakLv01` with `gelijk/identificatie` = "2026-042"
+- THEN the system MUST return a `zakLa01` SOAP response containing:
+ - `identificatie` = "2026-042"
+ - `omschrijving` = "Bouwvergunning Keizersgracht 100"
+ - `startdatum` = "20260115"
+ - Current status with `datumStatusGezet` and status type code
+ - Related roles (`heeftAlsInitiator`, `heeftAlsBehandelaar`)
+ - Related documents (if `scope` requests them)
+
+#### Scenario STUF-008b: Query with scope filtering
+
+- GIVEN a `zakLv01` request with a `scope` element requesting only `identificatie`, `omschrijving`, and `startdatum`
+- WHEN the system processes the request
+- THEN the `zakLa01` response MUST include only the requested fields
+- AND omitted fields MUST NOT appear in the response (not even as empty elements)
+
+#### Scenario STUF-008c: Query with no results
+
+- GIVEN a `zakLv01` with `gelijk/identificatie` = "9999-999" (non-existent)
+- WHEN the system processes the request
+- THEN the system MUST return a `zakLa01` with an empty `antwoord` element (zero objects)
+
+#### Scenario STUF-008d: Query with maximumAantal
+
+- GIVEN 50 cases matching the query criteria
+- AND the `zakLv01` specifies `maximumAantal` = 10
+- WHEN the system processes the request
+- THEN the `zakLa01` MUST contain at most 10 zaak objects
+
+---
+
+### REQ-STUF-009: Inbound StUF-ZKN Case Update
+
+**Feature tier**: V2
+
+The system MUST accept incoming StUF-ZKN `zakLk01` messages with `mutatiesoort="W"` (wijziging) to update existing cases.
+
+#### Scenario STUF-009a: Update case via zakLk01
+
+- GIVEN a case "2026-042" exists with title "Bouwvergunning Keizersgracht 100"
+- WHEN a legacy system sends a `zakLk01` with `mutatiesoort="W"` and `identificatie` = "2026-042" and `omschrijving` = "Bouwvergunning Keizersgracht 100 - gewijzigd"
+- THEN the system MUST update the case title to "Bouwvergunning Keizersgracht 100 - gewijzigd"
+- AND the audit trail MUST record the update with source "StUF-ZKN"
+- AND the system MUST return a `Bv01` bevestiging
+
+#### Scenario STUF-009b: Update case status via zakLk01
+
+- GIVEN a case "2026-042" at status "Ontvangen"
+- WHEN a `zakLk01` with `mutatiesoort="W"` includes a new status element with `gpiStatusType/code` = "INBEH" and `datumStatusGezet` = "20260316120000"
+- THEN the system MUST resolve "INBEH" to the matching Procest status type
+- AND create a new StatusRecord with the specified date
+- AND all case type validation rules MUST be enforced (required properties, required documents)
+
+#### Scenario STUF-009c: Reject update for non-existent case
+
+- GIVEN no case with identifier "9999-999" exists
+- WHEN a `zakLk01` with `mutatiesoort="W"` and `identificatie` = "9999-999" arrives
+- THEN the system MUST return a `Fo01` fault with `foutcode` = "StUF064" and `foutbeschrijving` = "Zaak niet gevonden: 9999-999"
+
+---
+
+### REQ-STUF-010: Inbound StUF-ZKN Document Handling
+
+**Feature tier**: V2
+
+The system MUST accept incoming StUF-ZKN `edcLk01` messages to link documents to cases.
+
+#### Scenario STUF-010a: Receive document via edcLk01
+
+- GIVEN a case "2026-042" exists
+- WHEN a legacy system sends an `edcLk01` with `mutatiesoort="T"` containing:
+ - `identificatie` = "DOC-2026-001"
+ - `titel` = "Bouwtekening.pdf"
+ - `formaat` = "application/pdf"
+ - `inhoud` = base64-encoded PDF content
+ - `isRelevantVoor/gerelateerde/identificatie` = "2026-042"
+- THEN the system MUST decode the base64 `inhoud`
+- AND store the document in Nextcloud Files under the case's folder
+- AND create a caseDocument object linking the document to the case
+- AND return a `Bv01` bevestiging
+
+#### Scenario STUF-010b: Document without content (metadata only)
+
+- GIVEN an `edcLk01` with document metadata but no `inhoud` element
+- WHEN the message is processed
+- THEN the system MUST create a caseDocument object with the metadata
+- AND mark the document as "metadata only -- no content received"
+
+---
+
+### REQ-STUF-011: StUF XML Message Processing
+
+**Feature tier**: V1 (outbound), V2 (inbound)
+
+The system MUST correctly handle StUF XML namespaces, date formats, noValue attributes, and message structure.
+
+#### Scenario STUF-011a: XML namespace handling
+
+- GIVEN a StUF-ZKN message is being constructed or parsed
+- THEN the system MUST correctly handle these namespaces:
+ - `http://www.egem.nl/StUF/StUF0301` (StUF base)
+ - `http://www.egem.nl/StUF/sector/zkn/0310` (StUF-ZKN)
+ - `http://www.egem.nl/StUF/sector/bg/0310` (StUF-BG)
+ - `http://www.w3.org/2001/XMLSchema-instance` (xsi)
+ - `http://www.opengis.net/gml` (gml, for geometry)
+
+#### Scenario STUF-011b: Date format conversion
+
+- GIVEN a date value "2026-03-16" in ISO 8601 format (Procest internal)
+- WHEN the value is included in an outbound StUF message
+- THEN the value MUST be converted to StUF format: "20260316"
+- AND datetime values MUST use format: "YYYYMMDDHHmmss" (e.g., "20260316143000")
+
+#### Scenario STUF-011c: noValue attribute handling
+
+- GIVEN a case property that has no value (null/empty)
+- WHEN the property is included in an outbound StUF message
+- THEN the element MUST include the appropriate `StUF:noValue` attribute:
+ - `geenWaarde` -- explicitly set to no value
+ - `waardeOnbekend` -- value exists but is unknown
+ - `nietOndersteund` -- field not supported by the system
+ - `vastgesteldOnbekend` -- officially determined as unknown
+
+#### Scenario STUF-011d: Validate outbound XML against XSD
+
+- GIVEN bundled StUF-ZKN 3.10 and StUF-BG 3.10 XSD schemas
+- WHEN an outbound StUF message is constructed
+- THEN the system SHOULD validate the XML against the relevant XSD before sending
+- AND if validation fails, the system MUST log the validation errors and NOT send the invalid message
+
+---
+
+### REQ-STUF-012: StUF-BG Inbound Person Query
+
+**Feature tier**: V2
+
+The system SHOULD accept incoming StUF-BG `npsLv01` messages to expose person data stored in OpenRegister. This enables legacy systems to query Procest as if it were a BRP source.
+
+#### Scenario STUF-012a: Receive person query
+
+- GIVEN the Procest StUF endpoint at `/apps/procest/api/stuf/personen`
+- AND a person object with BSN "999993653" exists in OpenRegister
+- WHEN a legacy system sends a StUF-BG `npsLv01` with `gelijk/inp.bsn` = "999993653"
+- THEN the system MUST return a `npsLa01` response with the person data mapped from OpenRegister to StUF-BG XML
+
+#### Scenario STUF-012b: Person query with scope
+
+- GIVEN a `npsLv01` with scope requesting only `inp.bsn`, `geslachtsnaam`, and `geboortedatum`
+- WHEN the system processes the request
+- THEN the `npsLa01` MUST include only the requested fields
+
+---
+
+### REQ-STUF-013: StUF Field Mapping Configuration
+
+**Feature tier**: V1
+
+The system MUST store field mappings between StUF XML paths and OpenRegister object properties as configurable mapping objects. Default mappings for ZGW-zaak and BRP-person data MUST be pre-seeded.
+
+#### Scenario STUF-013a: Pre-seeded zaak field mapping
+
+- GIVEN the Procest app is installed and the repair step runs
+- THEN a default StUF-ZKN field mapping MUST be created in OpenRegister containing mappings for all fields listed in the "StUF-ZKN to Procest Case Mapping" table above
+- AND the mapping MUST include date format transformations (StUF `YYYYMMDD` to ISO 8601 and vice versa)
+
+#### Scenario STUF-013b: Custom field mapping
+
+- GIVEN a municipality uses a StUF extension with custom fields (e.g., `gem:kenmerk` for a local reference number)
+- WHEN an admin adds a custom mapping entry: StUF path `gem:kenmerk` -> OpenRegister property `localReference`
+- THEN the system MUST apply this mapping during StUF message parsing and construction
+- AND the custom mapping MUST NOT override default mappings unless explicitly configured
+
+#### Scenario STUF-013c: Value transformation in mapping
+
+- GIVEN a mapping entry for `vertrouwelijkAanduiding` with a value transformation table (StUF enum values to Procest enum values)
+- WHEN a StUF message contains `vertrouwelijkAanduiding` = "ZAAKVERTROUWELIJK"
+- THEN the system MUST transform the value to `case_sensitive` using the mapping's transformation table
+
+---
+
+### REQ-STUF-014: SOAP Server Within Nextcloud
+
+**Feature tier**: V2
+
+Exposing inbound StUF endpoints within Nextcloud requires a SOAP server implementation. Since Nextcloud routes are REST-based, the StUF controller accepts raw XML POSTs and processes them as SOAP messages without using PHP's built-in SoapServer (which requires WSDL mode and conflicts with Nextcloud's routing).
+
+#### Scenario STUF-014a: Raw SOAP POST handling
+
+- GIVEN a Procest route `/apps/procest/api/stuf/{service}` registered as a raw POST endpoint
+- WHEN a SOAP message arrives with `Content-Type: text/xml; charset=utf-8`
+- THEN the controller MUST read the raw request body (`php://input`)
+- AND parse the SOAP envelope using PHP's DOMDocument or SimpleXML
+- AND extract the SOAP Body content
+- AND dispatch to the appropriate handler based on the root element name (e.g., `zakLk01`, `zakLv01`, `npsLv01`)
+- AND construct a SOAP envelope response with the appropriate content
+- AND return with `Content-Type: text/xml; charset=utf-8`
+
+#### Scenario STUF-014b: WSDL serving
+
+- GIVEN bundled WSDL files for StUF-ZKN and StUF-BG services
+- WHEN a client sends a GET request to `/apps/procest/api/stuf/zaken?wsdl`
+- THEN the system MUST return the StUF-ZKN WSDL file with `Content-Type: text/xml`
+- AND the WSDL's `soap:address location` MUST reflect the actual Procest endpoint URL
+
+#### Scenario STUF-014c: SOAPAction header routing
+
+- GIVEN a SOAP request with `SOAPAction: "http://www.egem.nl/StUF/sector/zkn/0310/zakLk01"`
+- WHEN the controller processes the request
+- THEN the SOAPAction header MAY be used as a secondary dispatch mechanism alongside XML body inspection
+
+---
+
+### REQ-STUF-015: Dual API Coexistence
+
+**Feature tier**: V1
+
+The system MUST ensure that StUF and ZGW APIs provide consistent views of the same data. Changes made via one protocol MUST be immediately visible via the other.
+
+#### Scenario STUF-015a: Case created via StUF visible in ZGW
+
+- GIVEN a case created via inbound StUF-ZKN `zakLk01` with identifier "2026-042"
+- WHEN a ZGW API client sends GET `/api/v1/zaken?identificatie=2026-042` to the ZrcController
+- THEN the case MUST be returned with all fields populated from the StUF-created data
+- AND the `url` field MUST contain the ZGW-style self-link
+
+#### Scenario STUF-015b: Case created via ZGW queryable via StUF
+
+- GIVEN a case created via the ZGW Zaken API with identifier "2026-043"
+- WHEN a legacy system sends a StUF-ZKN `zakLv01` with `gelijk/identificatie` = "2026-043"
+- THEN the case MUST be returned in the `zakLa01` response with all fields correctly mapped to StUF-ZKN XML
+
+#### Scenario STUF-015c: Case updated via UI reflected in StUF query
+
+- GIVEN a case "2026-042" with status "Ontvangen"
+- WHEN a handler changes the status to "In behandeling" via the Procest frontend
+- AND a legacy system immediately sends a `zakLv01` for "2026-042"
+- THEN the response MUST show the new status "In behandeling" with the correct `datumStatusGezet`
+
+---
+
+## SOAP Server Challenge
+
+Hosting a SOAP server within Nextcloud presents architectural challenges:
+
+1. **Nextcloud routing is REST-based**: All routes go through `routes.php` and expect JSON request/response. StUF requires raw XML POST handling with SOAP envelope wrapping.
+
+2. **PHP SoapServer limitations**: PHP's built-in `SoapServer` class operates in WSDL mode and expects to handle the full HTTP lifecycle. Within Nextcloud's controller framework, this conflicts with the existing request/response handling.
+
+3. **Proposed solution**: Implement a `StufController` that extends `OCP\AppFramework\Controller` and:
+ - Registers routes for `/api/stuf/zaken` and `/api/stuf/personen`
+ - Reads raw XML from `php://input`
+ - Parses XML using DOMDocument (not SoapServer)
+ - Dispatches to a `StufMessageHandler` service
+ - Constructs SOAP XML responses manually
+ - Returns `DataDisplayResponse` with `Content-Type: text/xml`
+
+4. **Alternative approach**: Use OpenConnector as a SOAP proxy -- OpenConnector could host the SOAP endpoint (it already has SOAPService) and forward parsed data to Procest's REST API. This avoids the SOAP-in-Nextcloud problem but adds a network hop and dependency.
+
+---
+
+## Dependencies
+
+- **OpenConnector stuf-adapter spec** (`openconnector/openspec/specs/stuf-adapter/spec.md`): Provides the SOAP infrastructure (SOAPService), certificate handling, and source type configuration. Procest leverages OpenConnector for all outbound StUF communication.
+- **OpenRegister**: All case data stored as objects; field mapping configurations stored as OpenRegister objects.
+- **Procest case-management spec** (`../case-management/spec.md`): The case data model, status lifecycle, validation rules, and audit trail that StUF messages map to/from.
+- **Procest roles-decisions spec** (`../roles-decisions/spec.md`): Role entities created from StUF `heeftAlsInitiator`/`heeftAlsBehandelaar` data.
+- **Procest openregister-integration spec** (`../openregister-integration/spec.md`): The `procest` register and 12 schemas that store all data.
+- **PHP DOMDocument / SimpleXML**: For XML parsing and construction (bundled with PHP).
+- **StUF XSD schema packages**: StUF-BG 3.10 and StUF-ZKN 3.10 XSD files for validation.
+- **Existing ZGW controllers** (`ZrcController`, `ZtcController`, `DrcController`, `BrcController`): The REST API surface that coexists with StUF.
+
+---
+
+## Current Implementation Status
+
+### Using Mock Register Data
+
+This spec depends on the **BRP** mock register for testing StUF-BG person lookup (REQ-STUF-001) and the **BAG** mock register for address data.
+
+**Loading the registers:**
+```bash
+# Load BRP register (35 persons, register slug: "brp", schema: "ingeschreven-persoon")
+docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/brp_register.json
+
+# Load BAG register (32 addresses + 21 objects + 21 buildings, register slug: "bag")
+docker exec -u www-data nextcloud php occ openregister:load-register /var/www/html/custom_apps/openregister/lib/Settings/bag_register.json
+```
+
+**Test data for this spec's use cases:**
+- **StUF-BG npsLv01 person lookup**: BSN `999993653` (Suzanne Moulin) -- test outbound BRP query and npsLa01 response mapping
+- **StUF-BG person not found**: BSN `000000000` -- test empty npsLa01 response handling
+- **StUF-ZKN with initiator BSN**: BSN `999990627` (Stephan Janssen) -- test inbound zakLk01 with `heeftAlsInitiator/inp.bsn`
+- **StUF-BG inbound query (REQ-STUF-012)**: BSN `999992570` (Albert Vogel) -- test serving person data via StUF-BG endpoint
+
+### Procest
+
+**No StUF implementation exists.** Grep for "stuf", "StUF", and "soap" in `procest/lib/` returns zero results. All current API communication is via ZGW REST APIs through the four ZGW controllers.
+
+**Implemented (ZGW foundation that StUF maps to):**
+- ZGW Zaken API via `ZrcController` -- zaken, statussen, resultaten, rollen, zaakeigenschappen, zaakinformatieobjecten, zaakobjecten, klantcontacten
+- ZGW Catalogi API via `ZtcController` -- zaaktypen, statustypen, resultaattypen, roltypen, besluittypen
+- ZGW Documenten API via `DrcController` -- enkelvoudiginformatieobjecten
+- ZGW Besluiten API via `BrcController` -- besluiten
+- ZGW business rules via `ZgwBusinessRulesService` and `ZgwZrcRulesService`
+- OpenRegister schemas for all 12 entity types
+
+### OpenConnector
+
+**Partial SOAP infrastructure exists** (see `openconnector/openspec/specs/stuf-adapter/spec.md` for details):
+- `SOAPService` with generic SOAP client, WSDL-driven requests, SOAP 1.1/1.2 support
+- Specific StUF-ZKN `edcLk01` handling (base64 document content decoding)
+- Source type `soap` with WSDL URL and authentication configuration
+- Certificate handling for mTLS (PKIoverheid)
+- `CallService` SOAP routing (type `soap` -> SOAPService)
+- **NOT implemented in OpenConnector**: Inbound SOAP server, StUF field mappings, WSDL bundling, stuurgegevens, namespace handling, noValue attributes, fault message handling (Fo01/Fo02/Fo03/Bv03)
+
+---
+
+## Standards & References
+
+- **StUF 3.01**: Standaard Uitwisseling Formaat, base standard. Defines the SOAP message structure, stuurgegevens, kennisgevingen (Lk01), vraag/antwoord (Lv01/La01), bevestiging (Bv01/Bv03), and foutmeldingen (Fo01/Fo02/Fo03). Maintained by VNG Realisatie. https://www.gemmaonline.nl/index.php/StUF_Berichtenstandaard
+- **StUF-ZKN 3.10**: Sectormodel Zaak-/Documentservices, built on StUF 3.01. Defines zaak (zak), document (edc), status, and related message types. The "e" extension (3.10e) adds extra message types. https://www.gemmaonline.nl/index.php/Sectormodel_Zaken:_StUF-ZKN
+- **StUF-BG 3.10**: Sectormodel Basisgegevens, built on StUF 3.01. Defines person (nps), address (adr), and other base registry data types. https://www.gemmaonline.nl/index.php/Sectormodel_Basisgegevens:_StUF-BG
+- **RGBZ 2.0 (Referentiemodel Gemeentelijke Basisgegevens Zaken)**: The information model underlying StUF-ZKN, defining zaak, status, document, besluit, and their relationships. The same model underlies ZGW APIs.
+- **ZGW APIs (VNG)**: The modern REST-based successor to StUF-ZKN. Procest already implements these via ZrcController, ZtcController, DrcController, BrcController. https://vng-realisatie.github.io/gemma-zaken/
+- **GEMMA**: Gemeentelijke Model Architectuur, the reference architecture for Dutch municipalities. Defines how StUF and ZGW fit in the municipal information landscape. https://www.gemmaonline.nl/
+- **Logius**: Dutch government IT authority responsible for PKIoverheid certificates and DigiKoppeling (the transport standard for government-to-government communication, which mandates how StUF messages are exchanged).
+- **DigiKoppeling**: Transport standard (WUS/ebMS) for Dutch government interoperability. StUF messages are typically exchanged over DigiKoppeling WUS (SOAP+WS-Security). https://www.logius.nl/domeinen/gegevensuitwisseling/digikoppeling
+- **WS-Security (OASIS)**: SOAP message security standard. UsernameToken and X.509 Token profiles are used by Dutch government StUF endpoints.
+- **PKIoverheid**: Dutch government PKI for mTLS authentication on production StUF endpoints.
+- **MTOM (Message Transmission Optimization Mechanism)**: W3C standard for efficient binary content in SOAP messages, relevant for large document transfers via edcLk01.
+
+---
+
+## Specificity Assessment
+
+### Sufficient for implementation
+- Complete field mapping tables between StUF-ZKN/BG and Procest case model
+- Clear separation of outbound (V1, via OpenConnector) and inbound (V2, SOAP server) concerns
+- Detailed Gherkin scenarios for each message type with concrete data examples
+- Explicit SOAP server architecture proposal addressing the Nextcloud routing challenge
+- Coexistence principle ensuring data consistency between StUF and ZGW APIs
+- Authentication methods (mTLS, WS-Security) aligned with existing OpenConnector capabilities
+- Reference to OpenConnector's existing SOAPService and edcLk01 handling
+
+### Missing or ambiguous
+- **StUF version negotiation**: The spec targets 3.01/3.10 but some municipalities may run older versions. Version detection and fallback behavior is not defined.
+- **Async response patterns**: StUF supports asynchronous Bv03/Fo03 callback patterns for long-running operations. The callback mechanism (how Procest receives async responses) is not detailed.
+- **Performance requirements**: No throughput or latency SLAs for SOAP message processing.
+- **Mapping object schema**: REQ-STUF-013 describes configurable field mappings but does not define the OpenRegister schema structure for mapping objects (which fields, what register).
+- **Multi-source routing**: Can multiple StUF endpoints be configured for different case types, or is there one global StUF endpoint?
+- **StUF-ZKN 3.10e extensions**: The "e" extension adds extra message types not covered in this spec.
+- **Archival-related StUF messages**: StUF defines messages for archival transfers (overbrenging) which relate to the case result archival rules but are not covered here.
+- **Rate limiting and access control**: How are inbound StUF endpoints secured beyond stuurgegevens validation? IP whitelisting? Client certificate validation on the inbound side?
+
+### Open questions
+1. Should inbound StUF endpoints be hosted in Procest directly or proxied via OpenConnector (which already has SOAPService)?
+2. Which StUF version(s) must be supported on day one -- 3.01 only, 3.10 only, or both?
+3. Should the field mapping configuration be stored in the `procest` register or in OpenConnector's register?
+4. How should large document content in inbound `edcLk01` messages be handled -- streamed to disk or loaded into memory?
+5. Is DigiKoppeling (WUS profile) compliance required, or is plain HTTPS with WS-Security sufficient?
diff --git a/openspec/specs/task-management/spec.md b/openspec/specs/task-management/spec.md
index 64612603..22b6481b 100644
--- a/openspec/specs/task-management/spec.md
+++ b/openspec/specs/task-management/spec.md
@@ -1,793 +1,793 @@
-# Task Management Specification
-
-## Purpose
-
-Tasks represent work items within a case. They follow CMMN 1.1 HumanTask concepts and are semantically typed as `schema:Action`. Tasks can be assigned to Nextcloud users, have due dates and priorities, and follow an independent lifecycle within the parent case. Tasks are the primary mechanism for distributing and tracking work across case handlers, advisors, and other participants.
-
-**Standards**: CMMN 1.1 (HumanTask, PlanItem lifecycle), Schema.org (`Action`, `actionStatus`), BPMN 2.0 (task patterns)
-**Primary feature tier**: MVP
-**Extended features**: V1 (kanban, checklists, dependencies, templates), Enterprise (automation)
-
----
-
-## Data Model
-
-### Task Entity
-
-Stored as an OpenRegister object in the `procest` register under the `task` schema.
-
-| Property | Type | CMMN/Schema.org | Required | Default |
-|----------|------|----------------|----------|---------|
-| `title` | string (max 255) | `schema:name` | Yes | — |
-| `description` | string | `schema:description` | No | — |
-| `status` | enum | CMMN PlanItem states | Yes | `available` |
-| `assignee` | string (Nextcloud user UID) | CMMN assignee | No | — |
-| `case` | reference (UUID) | CMMN parent case | Yes | — |
-| `dueDate` | datetime (ISO 8601) | `schema:endTime` | No | — |
-| `priority` | enum: `low`, `normal`, `high`, `urgent` | `schema:priority` | No | `normal` |
-| `completedDate` | datetime (ISO 8601) | `schema:endTime` | Auto (set on completion) | — |
-
-### Task Status Lifecycle (CMMN PlanItem)
-
-```
- ┌──────────┐
- │ available│
- └────┬─────┘
- │ start
- v
- ┌──────────┐
- ┌────│ active │────┐
- │ └──────────┘ │
- complete terminate
- │ │
- v v
- ┌───────────┐ ┌────────────┐
- │ completed │ │ terminated │
- └───────────┘ └────────────┘
-
- ┌──────────┐
- │ disabled │ (set from available only)
- └──────────┘
-```
-
-| Status | CMMN State | Description | Allowed Transitions From |
-|--------|-----------|-------------|--------------------------|
-| `available` | Available | Task can be started | (initial state) |
-| `active` | Active | Task is being worked on | `available` |
-| `completed` | Completed | Task finished successfully | `active` |
-| `terminated` | Terminated | Task stopped before completion | `available`, `active` |
-| `disabled` | Disabled | Task not applicable | `available` |
-
-### Priority Levels
-
-| Priority | Sort Weight | Visual Indicator |
-|----------|------------|------------------|
-| `urgent` | 1 (highest) | Red badge |
-| `high` | 2 | Orange badge |
-| `normal` | 3 | No badge (default) |
-| `low` | 4 (lowest) | Grey badge |
-
----
-
-## Requirements
-
-### REQ-TASK-001: Task CRUD
-
-**Tier**: MVP
-
-The system MUST support creating, reading, updating, and deleting tasks linked to cases. All task objects are stored in OpenRegister under the `procest` register, `task` schema.
-
-#### Scenario: Create a task linked to a case
-
-- GIVEN a case #2024-042 "Bouwvergunning Keizersgracht" exists with status "In behandeling"
-- AND the current user "jan.devries" has access to the case
-- WHEN the user creates a task with title "Controleer bouwtekeningen" and assigns it to case #2024-042
-- THEN the system MUST create an OpenRegister object in the `task` schema
-- AND the task `case` property MUST contain the UUID of case #2024-042
-- AND the task `status` MUST default to `available`
-- AND the task `priority` MUST default to `normal`
-- AND the `completedDate` MUST be null
-- AND the audit trail MUST record the creation event with the creating user
-
-#### Scenario: Create a task with all optional fields
-
-- GIVEN a case #2024-048 "Subsidie verduurzaming" exists
-- WHEN the user creates a task with:
- - title: "Beoordeel begroting"
- - description: "Controleer of de ingediende begroting voldoet aan de subsidievoorwaarden"
- - assignee: "maria.bakker"
- - dueDate: "2026-03-01T17:00:00Z"
- - priority: "high"
-- THEN all properties MUST be stored correctly on the task object
-- AND the task `status` MUST still default to `available`
-
-#### Scenario: Read a single task
-
-- GIVEN a task with UUID "task-uuid-001" exists in case #2024-042
-- WHEN the frontend requests `GET /index.php/apps/openregister/api/objects/procest/task/task-uuid-001`
-- THEN the response MUST include all task properties
-- AND the `case` reference MUST be resolvable to the parent case object
-
-#### Scenario: Update a task's description
-
-- GIVEN an existing task "Controleer bouwtekeningen" with status `available`
-- WHEN the user updates the description to "Controleer bouwtekeningen inclusief constructieberekening"
-- THEN the system MUST update the task object via `PUT` to the OpenRegister API
-- AND the audit trail MUST record the update with the changed fields
-
-#### Scenario: Delete a task
-
-- GIVEN a task "Verouderde controle" with status `available` in case #2024-042
-- WHEN the user deletes the task
-- THEN the system MUST call `DELETE` on the OpenRegister API
-- AND the task MUST no longer appear in the case's task list
-- AND the audit trail MUST record the deletion
-
-#### Scenario: Attempt to create a task without a title (validation error)
-
-- GIVEN the user is creating a new task for case #2024-042
-- WHEN the user submits the form with an empty title
-- THEN the system MUST reject the request with a validation error
-- AND the error message MUST indicate that `title` is required
-- AND no task object MUST be created
-
-#### Scenario: Attempt to create a task without a case reference (validation error)
-
-- GIVEN the user is creating a new task
-- WHEN the user submits the form without selecting a parent case
-- THEN the system MUST reject the request with a validation error
-- AND the error message MUST indicate that `case` is required
-
-#### Scenario: Attempt to create a task referencing a non-existent case
-
-- GIVEN no case exists with UUID "non-existent-uuid"
-- WHEN the user submits a task creation with `case` set to "non-existent-uuid"
-- THEN the system MUST reject the request
-- AND the error message MUST indicate that the referenced case does not exist
-
----
-
-### REQ-TASK-002: Task Status Lifecycle
-
-**Tier**: MVP
-
-The system MUST enforce the CMMN PlanItem lifecycle for task status transitions. Invalid transitions MUST be rejected.
-
-#### Scenario: Start a task (available to active)
-
-- GIVEN a task "Controleer bouwtekeningen" with status `available`
-- AND the task is assigned to "jan.devries"
-- WHEN the user changes the status to `active`
-- THEN the status MUST change to `active`
-- AND the audit trail MUST record the status transition with timestamp and user
-
-#### Scenario: Complete a task (active to completed)
-
-- GIVEN a task "Controleer bouwtekeningen" with status `active` assigned to "jan.devries"
-- WHEN "jan.devries" marks the task as completed
-- THEN the status MUST change to `completed`
-- AND the `completedDate` MUST be set to the current timestamp (ISO 8601)
-- AND the audit trail MUST record the completion
-
-#### Scenario: Terminate an active task
-
-- GIVEN a task "Locatiebezoek plannen" with status `active`
-- WHEN the user terminates the task with reason "Niet meer nodig na telefonisch contact"
-- THEN the status MUST change to `terminated`
-- AND the task MUST remain visible in the case timeline (not deleted)
-- AND the audit trail MUST record the termination
-
-#### Scenario: Terminate an available task
-
-- GIVEN a task "Extra advies inwinnen" with status `available`
-- WHEN the user terminates the task
-- THEN the status MUST change to `terminated`
-
-#### Scenario: Disable an available task
-
-- GIVEN a task "Welstandstoets uitvoeren" with status `available`
-- WHEN the user disables the task (not applicable for this case)
-- THEN the status MUST change to `disabled`
-
-#### Scenario: Reject invalid transition - complete an available task
-
-- GIVEN a task "Controleer bouwtekeningen" with status `available`
-- WHEN the user attempts to change the status directly to `completed`
-- THEN the system MUST reject the transition
-- AND the error message MUST indicate that a task must be `active` before it can be `completed`
-- AND the task status MUST remain `available`
-
-#### Scenario: Reject invalid transition - reactivate a completed task
-
-- GIVEN a task "Intake controle" with status `completed` and `completedDate` "2026-01-20T14:30:00Z"
-- WHEN the user attempts to change the status back to `active`
-- THEN the system MUST reject the transition
-- AND the error message MUST indicate that completed tasks cannot be reactivated
-- AND the `completedDate` MUST remain unchanged
-
-#### Scenario: Reject invalid transition - disable an active task
-
-- GIVEN a task "Beoordeel aanvraag" with status `active`
-- WHEN the user attempts to change the status to `disabled`
-- THEN the system MUST reject the transition
-- AND the error message MUST indicate that only `available` tasks can be disabled
-
----
-
-### REQ-TASK-003: Task Assignment
-
-**Tier**: MVP
-
-The system MUST support assigning tasks to Nextcloud users by their user UID. Unassigned tasks are allowed.
-
-#### Scenario: Assign a task to a user
-
-- GIVEN an available task "Controleer bouwtekeningen" in case #2024-042
-- WHEN the user assigns it to Nextcloud user "jan.devries"
-- THEN the task `assignee` MUST be set to "jan.devries"
-- AND the task MUST appear in Jan de Vries's "My Work" view
-- AND the audit trail MUST record the assignment
-
-#### Scenario: Assign a task to a user with notification
-
-- GIVEN an available task "Beoordeel constructieberekening"
-- WHEN the user assigns it to "pieter.smit"
-- THEN the assigned user SHOULD receive a Nextcloud notification
-- AND the notification MUST include the task title and the parent case reference
-
-#### Scenario: Reassign a task to a different user
-
-- GIVEN a task "Review documenten" assigned to "jan.devries" with status `active`
-- WHEN the coordinator reassigns it to "maria.bakker"
-- THEN the `assignee` MUST be updated to "maria.bakker"
-- AND "maria.bakker" SHOULD receive an assignment notification
-- AND the audit trail MUST record the reassignment from "jan.devries" to "maria.bakker"
-- AND the task MUST remain in its current status (`active`)
-
-#### Scenario: Unassign a task
-
-- GIVEN a task "Verzamel informatie" assigned to "jan.devries" with status `available`
-- WHEN the user removes the assignee
-- THEN the `assignee` MUST be set to null
-- AND the task MUST no longer appear in Jan's "My Work" view
-
-#### Scenario: Attempt to assign a task to a non-existent user
-
-- GIVEN a task "Controleer bouwtekeningen"
-- WHEN the user attempts to assign it to "nonexistent.user"
-- THEN the system MUST reject the assignment
-- AND the error message MUST indicate that the user does not exist in Nextcloud
-
-#### Scenario: Create a task with immediate assignment
-
-- GIVEN a case #2024-042 exists
-- WHEN the user creates a task with title "Situatietekening controleren" and assignee "jan.devries" in a single operation
-- THEN the task MUST be created with the assignee already set
-- AND the task MUST appear immediately in Jan's "My Work" view
-
----
-
-### REQ-TASK-004: Task List View
-
-**Tier**: MVP
-
-The system MUST provide a list view for tasks with search, sorting, and filtering capabilities. The list view MUST support both a global task list (all tasks) and a case-scoped task list (tasks for a specific case).
-
-#### Scenario: View the global task list
-
-- GIVEN 23 tasks exist across 8 cases
-- WHEN the user navigates to the Tasks section
-- THEN the system MUST display a paginated list of tasks
-- AND each task row MUST show: title, parent case reference (ID + title), status, assignee, due date, and priority
-
-#### Scenario: View tasks for a specific case
-
-- GIVEN case #2024-042 "Bouwvergunning Keizersgracht" has 5 tasks
-- WHEN the user views the case detail page
-- THEN all 5 tasks MUST be displayed in the Tasks section
-- AND tasks MUST be sorted by status (available/active first) then by priority (urgent first) then by due date (earliest first)
-- AND the task count MUST show completion progress (e.g., "3/5")
-
-#### Scenario: Filter tasks by status
-
-- GIVEN 23 tasks exist with statuses: 4 available, 6 active, 12 completed, 1 terminated
-- WHEN the user filters by status "active"
-- THEN only the 6 active tasks MUST be shown
-- AND the filter MUST be clearly indicated in the UI
-
-#### Scenario: Filter tasks by assignee
-
-- GIVEN tasks assigned to "jan.devries" (8 tasks), "maria.bakker" (6 tasks), and unassigned (9 tasks)
-- WHEN the user filters by assignee "jan.devries"
-- THEN only the 8 tasks assigned to Jan MUST be shown
-
-#### Scenario: Filter tasks by case
-
-- GIVEN the user is on the global task list
-- WHEN the user selects case filter "Case #2024-042"
-- THEN only tasks belonging to case #2024-042 MUST be shown
-
-#### Scenario: Sort tasks by due date
-
-- GIVEN tasks with various due dates
-- WHEN the user sorts by due date ascending
-- THEN tasks MUST be ordered with the earliest due date first
-- AND tasks without a due date MUST appear at the end
-
-#### Scenario: Sort tasks by priority
-
-- GIVEN tasks with priorities: 2 urgent, 3 high, 10 normal, 2 low
-- WHEN the user sorts by priority descending
-- THEN tasks MUST be ordered: urgent, high, normal, low
-
-#### Scenario: Search tasks by title
-
-- GIVEN tasks with titles including "bouwtekeningen", "constructie", "situatie"
-- WHEN the user searches for "bouwtekeningen"
-- THEN only tasks whose title contains "bouwtekeningen" MUST be shown
-
-#### Scenario: View "My Tasks" (personal task list)
-
-- GIVEN "jan.devries" has 7 tasks assigned across cases #2024-042, #2024-048, and #2024-050
-- WHEN Jan views the "My Work" section
-- THEN all 7 of his tasks MUST be displayed
-- AND each task MUST show which case it belongs to (case ID and title)
-- AND tasks MUST be grouped by urgency: overdue first, then due this week, then upcoming
-
-#### Scenario: Empty task list
-
-- GIVEN a case #2024-051 with no tasks
-- WHEN the user views the case detail page
-- THEN the Tasks section MUST display an empty state message
-- AND a prominent "Add Task" button MUST be visible
-
----
-
-### REQ-TASK-005: Task Due Dates and Priorities
-
-**Tier**: MVP
-
-The system MUST support due dates and priority levels on tasks. Overdue tasks MUST be visually highlighted.
-
-#### Scenario: Set a due date on a task
-
-- GIVEN a task "Controleer bouwtekeningen" without a due date
-- WHEN the user sets the due date to "2026-02-26T17:00:00Z"
-- THEN the `dueDate` MUST be stored on the task object
-- AND the due date MUST be displayed on the task card as "Feb 26"
-
-#### Scenario: Overdue task highlighting in list view
-
-- GIVEN a task "Review documenten" with dueDate "2026-02-20T17:00:00Z" and status `active`
-- AND today is February 25, 2026
-- THEN the task MUST be visually marked as overdue (red indicator)
-- AND the overdue duration MUST be displayed (e.g., "5 days overdue")
-
-#### Scenario: Overdue task highlighting on kanban card
-
-- GIVEN a task card on the kanban board for "Review documenten" with dueDate in the past
-- AND the task status is `active`
-- THEN the card MUST display a red overdue warning (e.g., "1 day overdue")
-- AND the due date text MUST be styled in red
-
-#### Scenario: Completed task is not marked overdue
-
-- GIVEN a task "Intake controle" with dueDate "2026-01-15" and status `completed` and completedDate "2026-01-14"
-- THEN the task MUST NOT be marked as overdue, even though the due date is in the past
-- AND the card MUST show the completion date with a green checkmark
-
-#### Scenario: Task due today indicator
-
-- GIVEN a task "Beoordeel constructie" with dueDate set to today
-- AND the task status is `active`
-- THEN the task MUST be highlighted with an amber/yellow "Due today" indicator
-
-#### Scenario: Set priority on a task
-
-- GIVEN a task "Controleer bouwtekeningen" with default priority `normal`
-- WHEN the user changes the priority to `high`
-- THEN the `priority` MUST be updated to `high`
-- AND the task card MUST display a priority badge (orange "high" badge as per kanban card anatomy)
-
-#### Scenario: Priority affects sort order
-
-- GIVEN the following active tasks:
- - "Draft besluit" with priority `urgent`, dueDate Mar 5
- - "Review documenten" with priority `high`, dueDate Feb 26
- - "Verzamel info" with priority `normal`, dueDate Feb 28
-- WHEN the user views the task list sorted by priority
-- THEN the order MUST be: "Draft besluit" (urgent), "Review documenten" (high), "Verzamel info" (normal)
-
----
-
-### REQ-TASK-006: Task Card Display
-
-**Tier**: MVP (list), V1 (kanban cards)
-
-Task cards MUST display key information following the card anatomy defined in the design wireframes.
-
-#### Scenario: Task card anatomy in list view
-
-- GIVEN a task with:
- - title: "Review documenten"
- - case: #2024-042 "Bouwvergunning Keizersgracht"
- - dueDate: "2026-02-26"
- - assignee: "jan.devries" (display name "Jan de Vries")
- - priority: `high`
- - status: `active`
-- WHEN the task is rendered in the list view
-- THEN the card MUST display:
- - The task title "Review documenten" (clickable, navigates to case detail)
- - The parent case reference "Case #2024-042" (clickable, navigates to case)
- - The due date formatted as "Feb 26"
- - The assignee name "Jan" or "Jan de Vries" with avatar
- - A priority badge "high" (orange)
-
-#### Scenario: Task card on kanban board
-
-- GIVEN the same task as above displayed on the kanban board
-- THEN the card MUST be positioned in the "Active" column
-- AND the card MUST follow the anatomy:
- ```
- ┌──────────────────┐
- │ Review documenten│ (title)
- │ Case #042 │ (parent case reference)
- │ Feb 26 │ (due date)
- │ Jan │ (assignee)
- │ high │ (priority badge)
- └──────────────────┘
- ```
-
-#### Scenario: Unassigned task card
-
-- GIVEN a task "Controleer regelgeving" with no assignee
-- WHEN the card is rendered
-- THEN the assignee field MUST show a dash "—" or "Unassigned" placeholder
-- AND the card MUST still display all other fields normally
-
----
-
-### REQ-TASK-007: Kanban Board View
-
-**Tier**: V1
-
-The system MUST provide a kanban board view for tasks, with columns corresponding to CMMN task statuses. The board MUST support drag-and-drop to change task status.
-
-#### Scenario: View tasks as kanban board
-
-- GIVEN tasks exist with statuses: 4 available, 6 active, 12 completed, 1 terminated
-- WHEN the user switches to the board view via the "Board | List" toggle
-- THEN the system MUST display four columns: "Available" (4 tasks), "Active" (6 tasks), "Completed" (12 tasks), "Terminated" (1 task)
-- AND each column header MUST show the task count
-- AND tasks within each column MUST be sorted by priority (urgent first) then due date (earliest first)
-
-#### Scenario: Toggle between board and list view
-
-- GIVEN the user is on the task list view
-- WHEN the user clicks the "Board" toggle
-- THEN the view MUST switch to the kanban board layout
-- AND the current filters (case, assignee, priority) MUST be preserved across the toggle
-- AND when switching back to "List", the filters MUST still be active
-
-#### Scenario: Drag task from Available to Active
-
-- GIVEN a task card "Controleer bouwtekeningen" in the "Available" column
-- WHEN the user drags the card to the "Active" column
-- THEN the system MUST update the task status to `active` via the OpenRegister API
-- AND the card MUST move to the "Active" column
-- AND the column counts MUST update (Available -1, Active +1)
-- AND the audit trail MUST record the status change
-
-#### Scenario: Drag task from Active to Completed
-
-- GIVEN a task card "Site visit uitvoeren" in the "Active" column
-- WHEN the user drags the card to the "Completed" column
-- THEN the system MUST update the task status to `completed`
-- AND `completedDate` MUST be set to the current timestamp
-- AND the card MUST show a completion checkmark
-
-#### Scenario: Prevent invalid drag (Completed to Active)
-
-- GIVEN a task card "Intake controle" in the "Completed" column
-- WHEN the user attempts to drag it to the "Active" column
-- THEN the system MUST reject the drop (invalid transition per CMMN lifecycle)
-- AND the card MUST snap back to the "Completed" column
-- AND a brief error message SHOULD inform the user that completed tasks cannot be reactivated
-
-#### Scenario: Filter kanban by case
-
-- GIVEN the user selects case filter "Case #2024-042" on the kanban board
-- THEN only tasks belonging to case #2024-042 MUST be shown in each column
-- AND the column counts MUST reflect the filtered totals
-
-#### Scenario: Filter kanban by assignee
-
-- GIVEN the user selects assignee filter "Jan de Vries"
-- THEN only tasks assigned to "jan.devries" MUST be shown across all columns
-
-#### Scenario: Kanban board with no tasks
-
-- GIVEN no tasks exist (or all are filtered out)
-- THEN the board MUST display empty columns with a helpful message
-- AND an "Add Task" button MUST be available
-
----
-
-### REQ-TASK-008: Task Completion
-
-**Tier**: MVP
-
-When a task is completed, the system MUST automatically set the `completedDate` and enforce lifecycle rules.
-
-#### Scenario: Complete a task and record completion date
-
-- GIVEN a task "Locatiebezoek" with status `active` and no `completedDate`
-- WHEN the user marks it as completed at 2026-02-25T14:30:00Z
-- THEN the `status` MUST change to `completed`
-- AND the `completedDate` MUST be set to "2026-02-25T14:30:00Z"
-- AND the task MUST remain visible in the case timeline with a green checkmark
-
-#### Scenario: Attempt to complete an already-completed task
-
-- GIVEN a task "Intake controle" already has status `completed` and completedDate "2026-01-20T10:00:00Z"
-- WHEN the user attempts to complete it again
-- THEN the system MUST reject the operation (no-op or error)
-- AND the `completedDate` MUST remain "2026-01-20T10:00:00Z"
-
-#### Scenario: Task completion updates case progress
-
-- GIVEN case #2024-042 has 5 tasks, 2 of which are completed
-- WHEN the user completes a third task
-- THEN the case detail Tasks section MUST show updated progress "3/5"
-
----
-
-### REQ-TASK-009: Task Checklist (Sub-Items)
-
-**Tier**: V1
-
-The system SHOULD support checklists within tasks for detailed work breakdown. Checklist items are lightweight items stored as part of the task object (not separate OpenRegister objects).
-
-#### Scenario: Add checklist items to a task
-
-- GIVEN a task "Beoordeel aanvraag" with status `active`
-- WHEN the user adds checklist items:
- - "Controleer volledigheid formulier"
- - "Verifieer bijlagen"
- - "Check regelgeving"
-- THEN the task object MUST store these items as an ordered list
-- AND each item MUST have a `checked` boolean (default: false) and a `label` string
-
-#### Scenario: Check off a checklist item
-
-- GIVEN a task with 3 checklist items, all unchecked
-- WHEN the user checks "Controleer volledigheid formulier"
-- THEN that item's `checked` MUST be set to true
-- AND the task card SHOULD show checklist progress (e.g., "1/3")
-
-#### Scenario: Reorder checklist items
-
-- GIVEN a task with 3 checklist items
-- WHEN the user drags "Check regelgeving" to the first position
-- THEN the order MUST be updated in the stored list
-
-#### Scenario: Checklist completion does not auto-complete the task
-
-- GIVEN a task with 3 checklist items, all checked
-- THEN the task status MUST NOT automatically change to `completed`
-- AND the user MUST still explicitly complete the task
-
----
-
-### REQ-TASK-010: Task Dependencies
-
-**Tier**: V1
-
-The system SHOULD support declaring dependencies between tasks ("blocked by" relationships). Dependencies are advisory: they provide visual indicators but do not strictly prevent work.
-
-#### Scenario: Declare a task dependency
-
-- GIVEN task A "Draft besluit" and task B "Review documenten" in case #2024-042
-- WHEN the user sets task A as "blocked by" task B
-- THEN task A MUST store a reference to task B's UUID as a dependency
-- AND task A's card MUST show a "blocked" indicator while task B is not completed
-
-#### Scenario: Dependency resolved when blocking task completes
-
-- GIVEN task A "Draft besluit" is blocked by task B "Review documenten"
-- WHEN task B is completed
-- THEN task A's "blocked" indicator MUST be removed
-- AND task A MUST remain in its current status (the indicator is visual only)
-
-#### Scenario: View dependency chain
-
-- GIVEN task A is blocked by task B, and task B is blocked by task C
-- WHEN the user views task A's dependencies
-- THEN the system SHOULD show the full dependency chain: A depends on B depends on C
-- AND the system SHOULD warn if the chain creates a circular dependency
-
-#### Scenario: Prevent circular dependencies
-
-- GIVEN task A is blocked by task B
-- WHEN the user attempts to set task B as "blocked by" task A
-- THEN the system MUST reject the circular dependency
-- AND the error message MUST indicate that a circular dependency was detected
-
----
-
-### REQ-TASK-011: Task Templates per Case Type
-
-**Tier**: V1
-
-The system SHOULD support defining task templates on case types. When a case of that type is created, the user can choose to instantiate the template tasks.
-
-#### Scenario: Define task template on case type
-
-- GIVEN the admin is editing case type "Omgevingsvergunning"
-- WHEN the admin defines a task template with:
- - "Intake controle" (priority: high, relative due: +3 days)
- - "Locatiebezoek plannen" (priority: normal, relative due: +14 days)
- - "Beoordeel aanvraag" (priority: high, relative due: +28 days)
- - "Draft besluit" (priority: urgent, relative due: +42 days)
- - "Verstuur resultaat" (priority: normal, relative due: +56 days)
-- THEN the template MUST be saved on the case type configuration
-
-#### Scenario: Apply task template on case creation
-
-- GIVEN case type "Omgevingsvergunning" has 5 template tasks
-- WHEN the user creates a new case of this type with start date "2026-03-01"
-- THEN the system SHOULD offer to create the template tasks
-- AND if accepted, 5 task objects MUST be created, each as an independent OpenRegister object
-- AND relative due dates MUST be calculated from the case start date (e.g., "Intake controle" due date = 2026-03-04)
-- AND all template tasks MUST be created with status `available`
-
-#### Scenario: Skip task template on case creation
-
-- GIVEN case type "Omgevingsvergunning" has 5 template tasks
-- WHEN the user creates a new case and declines the template
-- THEN no tasks MUST be created automatically
-- AND the user can add tasks manually later
-
----
-
-### REQ-TASK-012: Automated Task Creation on Case Status Change
-
-**Tier**: Enterprise
-
-The system MAY support automatically creating tasks when a case transitions to a specific status.
-
-#### Scenario: Auto-create tasks on status change
-
-- GIVEN case type "Omgevingsvergunning" has a rule: "When status changes to Besluitvorming, create task 'Draft besluit'"
-- WHEN case #2024-042 transitions from "In behandeling" to "Besluitvorming"
-- THEN the system MUST automatically create a task "Draft besluit"
-- AND the task MUST be linked to case #2024-042
-- AND the task MUST inherit default values from the automation rule (assignee, priority, relative due date)
-
-#### Scenario: Auto-created task notification
-
-- GIVEN an automated task creation rule exists
-- WHEN the rule fires and creates a task assigned to "jan.devries"
-- THEN "jan.devries" SHOULD receive a notification that a task was auto-created
-- AND the notification MUST indicate it was system-generated
-
----
-
-### REQ-TASK-013: Overdue Task Management
-
-**Tier**: MVP
-
-The system MUST provide clear visual indicators for overdue tasks and support filtering/sorting by overdue status.
-
-#### Scenario: Overdue task in My Work view
-
-- GIVEN "jan.devries" has an active task "Review documenten" with dueDate "2026-02-20"
-- AND today is 2026-02-25
-- THEN the task MUST appear in the "Overdue" section of Jan's My Work view
-- AND the overdue indicator MUST show "5 days overdue" in red
-
-#### Scenario: Multiple overdue tasks sorted by urgency
-
-- GIVEN "jan.devries" has overdue tasks:
- - "Review documenten" (5 days overdue, priority: high)
- - "Verzamel informatie" (2 days overdue, priority: normal)
- - "Controleer bijlagen" (1 day overdue, priority: urgent)
-- WHEN Jan views his My Work
-- THEN overdue tasks MUST be sorted by priority first (urgent, high, normal), then by overdue duration (most overdue first within the same priority)
-- AND the resulting order MUST be: "Controleer bijlagen", "Review documenten", "Verzamel informatie"
-
-#### Scenario: Task becomes overdue
-
-- GIVEN a task "Beoordeel begroting" with dueDate "2026-02-25T17:00:00Z" and status `active`
-- WHEN the current time passes "2026-02-25T17:00:00Z"
-- THEN on the next view render, the task MUST display an overdue indicator
-- AND the task MUST move to the "Overdue" group in My Work
-
-#### Scenario: Terminated or disabled tasks are not shown as overdue
-
-- GIVEN a task "Verouderde controle" with dueDate in the past and status `terminated`
-- THEN the task MUST NOT be marked as overdue
-- AND the task MUST NOT appear in the overdue section of My Work
-
----
-
-## Accessibility
-
-All task management interfaces MUST comply with WCAG AA:
-
-- Task cards MUST have sufficient color contrast for all text and indicators
-- Overdue/priority indicators MUST NOT rely solely on color (use icons and text labels)
-- Kanban drag-and-drop MUST have a keyboard-accessible alternative (e.g., dropdown to change status)
-- Task list MUST be navigable by keyboard (Tab to move between rows, Enter to open)
-- Screen readers MUST be able to identify task status, priority, and overdue state
-
----
-
-## Performance
-
-- The task list MUST load within 2 seconds for up to 100 tasks
-- The kanban board MUST render within 2 seconds for up to 50 cards per column
-- Drag-and-drop status changes MUST provide optimistic UI updates (move the card immediately, then confirm with the API)
-- The My Work view MUST aggregate tasks and cases in a single page load (parallel API calls)
-
----
-
-### Current Implementation Status
-
-**MVP substantially implemented. V1/Enterprise features not implemented.**
-
-**Implemented (with file paths):**
-- **Task schema**: Defined in `lib/Settings/procest_register.json` with properties: `title`, `description`, `status` (enum: available/active/completed/terminated/disabled), `assignee`, `case` (UUID ref), `dueDate`, `priority` (enum: low/normal/high/urgent), `completedDate`. Matches the spec data model exactly.
-- **Task lifecycle**: `src/utils/taskLifecycle.js` implements the full CMMN PlanItem lifecycle with:
- - `TASK_STATUSES` constant object
- - `TRANSITION_MAP`: available -> [active, terminated, disabled], active -> [completed, terminated], completed/terminated/disabled -> [] (terminal)
- - `getAllowedTransitions(currentStatus)`, `validateTransition(from, to)`, `getStatusLabel(status)`, `getTransitionLabel(targetStatus)`, `isTerminalStatus(status)` functions
- - Localized status labels (English, Dutch pending)
-- **Task helpers**: `src/utils/taskHelpers.js` provides utility functions for task display and overdue calculations.
-- **Task list view**: `src/views/tasks/TaskList.vue` -- paginated list of tasks with status, assignee, due date, and priority display (REQ-TASK-004).
-- **Task detail view**: `src/views/tasks/TaskDetail.vue` -- full task detail with editable fields.
-- **Task create dialog**: `src/views/tasks/TaskCreateDialog.vue` -- form for creating new tasks linked to a case.
-- **Task API service**: `src/services/taskApi.js` -- `fetchTasksForCases()` for CalDAV task integration.
-- **Task CRUD**: Via the shared object store (`src/store/modules/object.js`) for OpenRegister-based tasks.
-- **My Work integration**: `src/views/MyWork.vue` includes tasks in the unified view with overdue highlighting, priority indicators, grouped sections (REQ-TASK-013).
-- **Dashboard widgets**: `lib/Dashboard/MyTasksWidget.php` and `src/views/widgets/MyTasksWidget.vue` -- Nextcloud dashboard widget showing assigned tasks. `src/views/dashboard/MyWorkPreview.vue` shows task summary on app dashboard.
-- **Navigation**: `src/navigation/MainMenu.vue` includes "Tasks" menu item linked to `/tasks` route.
-- **Router**: `src/router/index.js` includes routes for `/tasks`, `/tasks/new`, and `/tasks/:id`.
-- **Overdue highlighting**: Implemented in `MyWork.vue` with red indicators and "X days overdue" text (REQ-TASK-005, REQ-TASK-013).
-- **Priority badges**: Priority indicators shown in My Work view for high and urgent priorities.
-
-**Architecture note:** Tasks have a dual implementation:
-1. OpenRegister `task` schema objects (used by the object store for CRUD)
-2. CalDAV tasks via `fetchTasksForCases()` in `src/services/taskApi.js` (used by My Work view)
-
-This duality means some views use OpenRegister tasks while others use CalDAV tasks. The spec assumes a single OpenRegister-based task system.
-
-**Not yet implemented:**
-- **REQ-TASK-007: Kanban board view (V1)**: No kanban/board view with columns per status. No drag-and-drop status transitions. No board/list toggle.
-- **REQ-TASK-009: Task checklists (V1)**: No checklist/sub-item support on tasks.
-- **REQ-TASK-010: Task dependencies (V1)**: No "blocked by" relationship support.
-- **REQ-TASK-011: Task templates per case type (V1)**: No task template configuration on case types. No auto-creation of template tasks on case creation.
-- **REQ-TASK-012: Automated task creation on status change (Enterprise)**: No automation rules for task creation.
-- **Task assignment notifications**: No Nextcloud notifications sent when tasks are assigned or reassigned.
-- **Task search by title**: Basic search may exist via the object store's `_search` parameter, but dedicated task search UI is not prominent.
-- **Keyboard navigation**: No explicit keyboard navigation support in task list or cards.
-- **Screen reader support**: No ARIA attributes for task status, priority, or overdue state.
-
-### Standards & References
-
-- **CMMN 1.1**: Task lifecycle states (Available, Active, Completed, Terminated, Disabled) follow the CMMN PlanItem lifecycle exactly. Transition rules match CMMN specification. Implemented in `src/utils/taskLifecycle.js`.
-- **Schema.org**: Tasks typed as `schema:Action` with `actionStatus` in `procest_register.json`.
-- **BPMN 2.0**: Task patterns for assignment and lifecycle management.
-- **ZGW APIs**: No direct ZGW equivalent for tasks (ZGW does not define a task resource), but tasks complement the ZGW Zaak lifecycle.
-- **WCAG 2.1 AA**: Spec requires color-independent indicators and keyboard accessibility. Partially implemented (text labels for overdue, but no keyboard nav).
-
-### Specificity Assessment
-
-- **MVP requirements are well-specified and mostly implemented.** The task CRUD, lifecycle, assignment, list view, and overdue management are clear and actionable.
-- **V1 features (kanban, checklists, dependencies, templates) are well-specified** with concrete scenarios but not yet implemented.
-- **Task architecture ambiguity**: The dual CalDAV/OpenRegister task system needs resolution. The spec assumes OpenRegister-only tasks.
-- **Open questions:**
- - Should tasks migrate fully to OpenRegister, or should CalDAV integration be maintained for Nextcloud ecosystem compatibility?
- - How should kanban drag-and-drop handle the keyboard-accessible alternative (dropdown vs. move buttons)?
- - Should task templates be stored as JSON arrays on the case type object or as separate OpenRegister objects?
- - How should task dependencies be stored (array of UUIDs on the task object, or separate relation objects)?
+# Task Management Specification
+
+## Purpose
+
+Tasks represent work items within a case. They follow CMMN 1.1 HumanTask concepts and are semantically typed as `schema:Action`. Tasks can be assigned to Nextcloud users, have due dates and priorities, and follow an independent lifecycle within the parent case. Tasks are the primary mechanism for distributing and tracking work across case handlers, advisors, and other participants.
+
+**Standards**: CMMN 1.1 (HumanTask, PlanItem lifecycle), Schema.org (`Action`, `actionStatus`), BPMN 2.0 (task patterns)
+**Primary feature tier**: MVP
+**Extended features**: V1 (kanban, checklists, dependencies, templates), Enterprise (automation)
+
+---
+
+## Data Model
+
+### Task Entity
+
+Stored as an OpenRegister object in the `procest` register under the `task` schema.
+
+| Property | Type | CMMN/Schema.org | Required | Default |
+|----------|------|----------------|----------|---------|
+| `title` | string (max 255) | `schema:name` | Yes | — |
+| `description` | string | `schema:description` | No | — |
+| `status` | enum | CMMN PlanItem states | Yes | `available` |
+| `assignee` | string (Nextcloud user UID) | CMMN assignee | No | — |
+| `case` | reference (UUID) | CMMN parent case | Yes | — |
+| `dueDate` | datetime (ISO 8601) | `schema:endTime` | No | — |
+| `priority` | enum: `low`, `normal`, `high`, `urgent` | `schema:priority` | No | `normal` |
+| `completedDate` | datetime (ISO 8601) | `schema:endTime` | Auto (set on completion) | — |
+
+### Task Status Lifecycle (CMMN PlanItem)
+
+```
+ ┌──────────┐
+ │ available│
+ └────┬─────┘
+ │ start
+ v
+ ┌──────────┐
+ ┌────│ active │────┐
+ │ └──────────┘ │
+ complete terminate
+ │ │
+ v v
+ ┌───────────┐ ┌────────────┐
+ │ completed │ │ terminated │
+ └───────────┘ └────────────┘
+
+ ┌──────────┐
+ │ disabled │ (set from available only)
+ └──────────┘
+```
+
+| Status | CMMN State | Description | Allowed Transitions From |
+|--------|-----------|-------------|--------------------------|
+| `available` | Available | Task can be started | (initial state) |
+| `active` | Active | Task is being worked on | `available` |
+| `completed` | Completed | Task finished successfully | `active` |
+| `terminated` | Terminated | Task stopped before completion | `available`, `active` |
+| `disabled` | Disabled | Task not applicable | `available` |
+
+### Priority Levels
+
+| Priority | Sort Weight | Visual Indicator |
+|----------|------------|------------------|
+| `urgent` | 1 (highest) | Red badge |
+| `high` | 2 | Orange badge |
+| `normal` | 3 | No badge (default) |
+| `low` | 4 (lowest) | Grey badge |
+
+---
+
+## Requirements
+
+### REQ-TASK-001: Task CRUD
+
+**Tier**: MVP
+
+The system MUST support creating, reading, updating, and deleting tasks linked to cases. All task objects are stored in OpenRegister under the `procest` register, `task` schema.
+
+#### Scenario: Create a task linked to a case
+
+- GIVEN a case #2024-042 "Bouwvergunning Keizersgracht" exists with status "In behandeling"
+- AND the current user "jan.devries" has access to the case
+- WHEN the user creates a task with title "Controleer bouwtekeningen" and assigns it to case #2024-042
+- THEN the system MUST create an OpenRegister object in the `task` schema
+- AND the task `case` property MUST contain the UUID of case #2024-042
+- AND the task `status` MUST default to `available`
+- AND the task `priority` MUST default to `normal`
+- AND the `completedDate` MUST be null
+- AND the audit trail MUST record the creation event with the creating user
+
+#### Scenario: Create a task with all optional fields
+
+- GIVEN a case #2024-048 "Subsidie verduurzaming" exists
+- WHEN the user creates a task with:
+ - title: "Beoordeel begroting"
+ - description: "Controleer of de ingediende begroting voldoet aan de subsidievoorwaarden"
+ - assignee: "maria.bakker"
+ - dueDate: "2026-03-01T17:00:00Z"
+ - priority: "high"
+- THEN all properties MUST be stored correctly on the task object
+- AND the task `status` MUST still default to `available`
+
+#### Scenario: Read a single task
+
+- GIVEN a task with UUID "task-uuid-001" exists in case #2024-042
+- WHEN the frontend requests `GET /index.php/apps/openregister/api/objects/procest/task/task-uuid-001`
+- THEN the response MUST include all task properties
+- AND the `case` reference MUST be resolvable to the parent case object
+
+#### Scenario: Update a task's description
+
+- GIVEN an existing task "Controleer bouwtekeningen" with status `available`
+- WHEN the user updates the description to "Controleer bouwtekeningen inclusief constructieberekening"
+- THEN the system MUST update the task object via `PUT` to the OpenRegister API
+- AND the audit trail MUST record the update with the changed fields
+
+#### Scenario: Delete a task
+
+- GIVEN a task "Verouderde controle" with status `available` in case #2024-042
+- WHEN the user deletes the task
+- THEN the system MUST call `DELETE` on the OpenRegister API
+- AND the task MUST no longer appear in the case's task list
+- AND the audit trail MUST record the deletion
+
+#### Scenario: Attempt to create a task without a title (validation error)
+
+- GIVEN the user is creating a new task for case #2024-042
+- WHEN the user submits the form with an empty title
+- THEN the system MUST reject the request with a validation error
+- AND the error message MUST indicate that `title` is required
+- AND no task object MUST be created
+
+#### Scenario: Attempt to create a task without a case reference (validation error)
+
+- GIVEN the user is creating a new task
+- WHEN the user submits the form without selecting a parent case
+- THEN the system MUST reject the request with a validation error
+- AND the error message MUST indicate that `case` is required
+
+#### Scenario: Attempt to create a task referencing a non-existent case
+
+- GIVEN no case exists with UUID "non-existent-uuid"
+- WHEN the user submits a task creation with `case` set to "non-existent-uuid"
+- THEN the system MUST reject the request
+- AND the error message MUST indicate that the referenced case does not exist
+
+---
+
+### REQ-TASK-002: Task Status Lifecycle
+
+**Tier**: MVP
+
+The system MUST enforce the CMMN PlanItem lifecycle for task status transitions. Invalid transitions MUST be rejected.
+
+#### Scenario: Start a task (available to active)
+
+- GIVEN a task "Controleer bouwtekeningen" with status `available`
+- AND the task is assigned to "jan.devries"
+- WHEN the user changes the status to `active`
+- THEN the status MUST change to `active`
+- AND the audit trail MUST record the status transition with timestamp and user
+
+#### Scenario: Complete a task (active to completed)
+
+- GIVEN a task "Controleer bouwtekeningen" with status `active` assigned to "jan.devries"
+- WHEN "jan.devries" marks the task as completed
+- THEN the status MUST change to `completed`
+- AND the `completedDate` MUST be set to the current timestamp (ISO 8601)
+- AND the audit trail MUST record the completion
+
+#### Scenario: Terminate an active task
+
+- GIVEN a task "Locatiebezoek plannen" with status `active`
+- WHEN the user terminates the task with reason "Niet meer nodig na telefonisch contact"
+- THEN the status MUST change to `terminated`
+- AND the task MUST remain visible in the case timeline (not deleted)
+- AND the audit trail MUST record the termination
+
+#### Scenario: Terminate an available task
+
+- GIVEN a task "Extra advies inwinnen" with status `available`
+- WHEN the user terminates the task
+- THEN the status MUST change to `terminated`
+
+#### Scenario: Disable an available task
+
+- GIVEN a task "Welstandstoets uitvoeren" with status `available`
+- WHEN the user disables the task (not applicable for this case)
+- THEN the status MUST change to `disabled`
+
+#### Scenario: Reject invalid transition - complete an available task
+
+- GIVEN a task "Controleer bouwtekeningen" with status `available`
+- WHEN the user attempts to change the status directly to `completed`
+- THEN the system MUST reject the transition
+- AND the error message MUST indicate that a task must be `active` before it can be `completed`
+- AND the task status MUST remain `available`
+
+#### Scenario: Reject invalid transition - reactivate a completed task
+
+- GIVEN a task "Intake controle" with status `completed` and `completedDate` "2026-01-20T14:30:00Z"
+- WHEN the user attempts to change the status back to `active`
+- THEN the system MUST reject the transition
+- AND the error message MUST indicate that completed tasks cannot be reactivated
+- AND the `completedDate` MUST remain unchanged
+
+#### Scenario: Reject invalid transition - disable an active task
+
+- GIVEN a task "Beoordeel aanvraag" with status `active`
+- WHEN the user attempts to change the status to `disabled`
+- THEN the system MUST reject the transition
+- AND the error message MUST indicate that only `available` tasks can be disabled
+
+---
+
+### REQ-TASK-003: Task Assignment
+
+**Tier**: MVP
+
+The system MUST support assigning tasks to Nextcloud users by their user UID. Unassigned tasks are allowed.
+
+#### Scenario: Assign a task to a user
+
+- GIVEN an available task "Controleer bouwtekeningen" in case #2024-042
+- WHEN the user assigns it to Nextcloud user "jan.devries"
+- THEN the task `assignee` MUST be set to "jan.devries"
+- AND the task MUST appear in Jan de Vries's "My Work" view
+- AND the audit trail MUST record the assignment
+
+#### Scenario: Assign a task to a user with notification
+
+- GIVEN an available task "Beoordeel constructieberekening"
+- WHEN the user assigns it to "pieter.smit"
+- THEN the assigned user SHOULD receive a Nextcloud notification
+- AND the notification MUST include the task title and the parent case reference
+
+#### Scenario: Reassign a task to a different user
+
+- GIVEN a task "Review documenten" assigned to "jan.devries" with status `active`
+- WHEN the coordinator reassigns it to "maria.bakker"
+- THEN the `assignee` MUST be updated to "maria.bakker"
+- AND "maria.bakker" SHOULD receive an assignment notification
+- AND the audit trail MUST record the reassignment from "jan.devries" to "maria.bakker"
+- AND the task MUST remain in its current status (`active`)
+
+#### Scenario: Unassign a task
+
+- GIVEN a task "Verzamel informatie" assigned to "jan.devries" with status `available`
+- WHEN the user removes the assignee
+- THEN the `assignee` MUST be set to null
+- AND the task MUST no longer appear in Jan's "My Work" view
+
+#### Scenario: Attempt to assign a task to a non-existent user
+
+- GIVEN a task "Controleer bouwtekeningen"
+- WHEN the user attempts to assign it to "nonexistent.user"
+- THEN the system MUST reject the assignment
+- AND the error message MUST indicate that the user does not exist in Nextcloud
+
+#### Scenario: Create a task with immediate assignment
+
+- GIVEN a case #2024-042 exists
+- WHEN the user creates a task with title "Situatietekening controleren" and assignee "jan.devries" in a single operation
+- THEN the task MUST be created with the assignee already set
+- AND the task MUST appear immediately in Jan's "My Work" view
+
+---
+
+### REQ-TASK-004: Task List View
+
+**Tier**: MVP
+
+The system MUST provide a list view for tasks with search, sorting, and filtering capabilities. The list view MUST support both a global task list (all tasks) and a case-scoped task list (tasks for a specific case).
+
+#### Scenario: View the global task list
+
+- GIVEN 23 tasks exist across 8 cases
+- WHEN the user navigates to the Tasks section
+- THEN the system MUST display a paginated list of tasks
+- AND each task row MUST show: title, parent case reference (ID + title), status, assignee, due date, and priority
+
+#### Scenario: View tasks for a specific case
+
+- GIVEN case #2024-042 "Bouwvergunning Keizersgracht" has 5 tasks
+- WHEN the user views the case detail page
+- THEN all 5 tasks MUST be displayed in the Tasks section
+- AND tasks MUST be sorted by status (available/active first) then by priority (urgent first) then by due date (earliest first)
+- AND the task count MUST show completion progress (e.g., "3/5")
+
+#### Scenario: Filter tasks by status
+
+- GIVEN 23 tasks exist with statuses: 4 available, 6 active, 12 completed, 1 terminated
+- WHEN the user filters by status "active"
+- THEN only the 6 active tasks MUST be shown
+- AND the filter MUST be clearly indicated in the UI
+
+#### Scenario: Filter tasks by assignee
+
+- GIVEN tasks assigned to "jan.devries" (8 tasks), "maria.bakker" (6 tasks), and unassigned (9 tasks)
+- WHEN the user filters by assignee "jan.devries"
+- THEN only the 8 tasks assigned to Jan MUST be shown
+
+#### Scenario: Filter tasks by case
+
+- GIVEN the user is on the global task list
+- WHEN the user selects case filter "Case #2024-042"
+- THEN only tasks belonging to case #2024-042 MUST be shown
+
+#### Scenario: Sort tasks by due date
+
+- GIVEN tasks with various due dates
+- WHEN the user sorts by due date ascending
+- THEN tasks MUST be ordered with the earliest due date first
+- AND tasks without a due date MUST appear at the end
+
+#### Scenario: Sort tasks by priority
+
+- GIVEN tasks with priorities: 2 urgent, 3 high, 10 normal, 2 low
+- WHEN the user sorts by priority descending
+- THEN tasks MUST be ordered: urgent, high, normal, low
+
+#### Scenario: Search tasks by title
+
+- GIVEN tasks with titles including "bouwtekeningen", "constructie", "situatie"
+- WHEN the user searches for "bouwtekeningen"
+- THEN only tasks whose title contains "bouwtekeningen" MUST be shown
+
+#### Scenario: View "My Tasks" (personal task list)
+
+- GIVEN "jan.devries" has 7 tasks assigned across cases #2024-042, #2024-048, and #2024-050
+- WHEN Jan views the "My Work" section
+- THEN all 7 of his tasks MUST be displayed
+- AND each task MUST show which case it belongs to (case ID and title)
+- AND tasks MUST be grouped by urgency: overdue first, then due this week, then upcoming
+
+#### Scenario: Empty task list
+
+- GIVEN a case #2024-051 with no tasks
+- WHEN the user views the case detail page
+- THEN the Tasks section MUST display an empty state message
+- AND a prominent "Add Task" button MUST be visible
+
+---
+
+### REQ-TASK-005: Task Due Dates and Priorities
+
+**Tier**: MVP
+
+The system MUST support due dates and priority levels on tasks. Overdue tasks MUST be visually highlighted.
+
+#### Scenario: Set a due date on a task
+
+- GIVEN a task "Controleer bouwtekeningen" without a due date
+- WHEN the user sets the due date to "2026-02-26T17:00:00Z"
+- THEN the `dueDate` MUST be stored on the task object
+- AND the due date MUST be displayed on the task card as "Feb 26"
+
+#### Scenario: Overdue task highlighting in list view
+
+- GIVEN a task "Review documenten" with dueDate "2026-02-20T17:00:00Z" and status `active`
+- AND today is February 25, 2026
+- THEN the task MUST be visually marked as overdue (red indicator)
+- AND the overdue duration MUST be displayed (e.g., "5 days overdue")
+
+#### Scenario: Overdue task highlighting on kanban card
+
+- GIVEN a task card on the kanban board for "Review documenten" with dueDate in the past
+- AND the task status is `active`
+- THEN the card MUST display a red overdue warning (e.g., "1 day overdue")
+- AND the due date text MUST be styled in red
+
+#### Scenario: Completed task is not marked overdue
+
+- GIVEN a task "Intake controle" with dueDate "2026-01-15" and status `completed` and completedDate "2026-01-14"
+- THEN the task MUST NOT be marked as overdue, even though the due date is in the past
+- AND the card MUST show the completion date with a green checkmark
+
+#### Scenario: Task due today indicator
+
+- GIVEN a task "Beoordeel constructie" with dueDate set to today
+- AND the task status is `active`
+- THEN the task MUST be highlighted with an amber/yellow "Due today" indicator
+
+#### Scenario: Set priority on a task
+
+- GIVEN a task "Controleer bouwtekeningen" with default priority `normal`
+- WHEN the user changes the priority to `high`
+- THEN the `priority` MUST be updated to `high`
+- AND the task card MUST display a priority badge (orange "high" badge as per kanban card anatomy)
+
+#### Scenario: Priority affects sort order
+
+- GIVEN the following active tasks:
+ - "Draft besluit" with priority `urgent`, dueDate Mar 5
+ - "Review documenten" with priority `high`, dueDate Feb 26
+ - "Verzamel info" with priority `normal`, dueDate Feb 28
+- WHEN the user views the task list sorted by priority
+- THEN the order MUST be: "Draft besluit" (urgent), "Review documenten" (high), "Verzamel info" (normal)
+
+---
+
+### REQ-TASK-006: Task Card Display
+
+**Tier**: MVP (list), V1 (kanban cards)
+
+Task cards MUST display key information following the card anatomy defined in the design wireframes.
+
+#### Scenario: Task card anatomy in list view
+
+- GIVEN a task with:
+ - title: "Review documenten"
+ - case: #2024-042 "Bouwvergunning Keizersgracht"
+ - dueDate: "2026-02-26"
+ - assignee: "jan.devries" (display name "Jan de Vries")
+ - priority: `high`
+ - status: `active`
+- WHEN the task is rendered in the list view
+- THEN the card MUST display:
+ - The task title "Review documenten" (clickable, navigates to case detail)
+ - The parent case reference "Case #2024-042" (clickable, navigates to case)
+ - The due date formatted as "Feb 26"
+ - The assignee name "Jan" or "Jan de Vries" with avatar
+ - A priority badge "high" (orange)
+
+#### Scenario: Task card on kanban board
+
+- GIVEN the same task as above displayed on the kanban board
+- THEN the card MUST be positioned in the "Active" column
+- AND the card MUST follow the anatomy:
+ ```
+ ┌──────────────────┐
+ │ Review documenten│ (title)
+ │ Case #042 │ (parent case reference)
+ │ Feb 26 │ (due date)
+ │ Jan │ (assignee)
+ │ high │ (priority badge)
+ └──────────────────┘
+ ```
+
+#### Scenario: Unassigned task card
+
+- GIVEN a task "Controleer regelgeving" with no assignee
+- WHEN the card is rendered
+- THEN the assignee field MUST show a dash "—" or "Unassigned" placeholder
+- AND the card MUST still display all other fields normally
+
+---
+
+### REQ-TASK-007: Kanban Board View
+
+**Tier**: V1
+
+The system MUST provide a kanban board view for tasks, with columns corresponding to CMMN task statuses. The board MUST support drag-and-drop to change task status.
+
+#### Scenario: View tasks as kanban board
+
+- GIVEN tasks exist with statuses: 4 available, 6 active, 12 completed, 1 terminated
+- WHEN the user switches to the board view via the "Board | List" toggle
+- THEN the system MUST display four columns: "Available" (4 tasks), "Active" (6 tasks), "Completed" (12 tasks), "Terminated" (1 task)
+- AND each column header MUST show the task count
+- AND tasks within each column MUST be sorted by priority (urgent first) then due date (earliest first)
+
+#### Scenario: Toggle between board and list view
+
+- GIVEN the user is on the task list view
+- WHEN the user clicks the "Board" toggle
+- THEN the view MUST switch to the kanban board layout
+- AND the current filters (case, assignee, priority) MUST be preserved across the toggle
+- AND when switching back to "List", the filters MUST still be active
+
+#### Scenario: Drag task from Available to Active
+
+- GIVEN a task card "Controleer bouwtekeningen" in the "Available" column
+- WHEN the user drags the card to the "Active" column
+- THEN the system MUST update the task status to `active` via the OpenRegister API
+- AND the card MUST move to the "Active" column
+- AND the column counts MUST update (Available -1, Active +1)
+- AND the audit trail MUST record the status change
+
+#### Scenario: Drag task from Active to Completed
+
+- GIVEN a task card "Site visit uitvoeren" in the "Active" column
+- WHEN the user drags the card to the "Completed" column
+- THEN the system MUST update the task status to `completed`
+- AND `completedDate` MUST be set to the current timestamp
+- AND the card MUST show a completion checkmark
+
+#### Scenario: Prevent invalid drag (Completed to Active)
+
+- GIVEN a task card "Intake controle" in the "Completed" column
+- WHEN the user attempts to drag it to the "Active" column
+- THEN the system MUST reject the drop (invalid transition per CMMN lifecycle)
+- AND the card MUST snap back to the "Completed" column
+- AND a brief error message SHOULD inform the user that completed tasks cannot be reactivated
+
+#### Scenario: Filter kanban by case
+
+- GIVEN the user selects case filter "Case #2024-042" on the kanban board
+- THEN only tasks belonging to case #2024-042 MUST be shown in each column
+- AND the column counts MUST reflect the filtered totals
+
+#### Scenario: Filter kanban by assignee
+
+- GIVEN the user selects assignee filter "Jan de Vries"
+- THEN only tasks assigned to "jan.devries" MUST be shown across all columns
+
+#### Scenario: Kanban board with no tasks
+
+- GIVEN no tasks exist (or all are filtered out)
+- THEN the board MUST display empty columns with a helpful message
+- AND an "Add Task" button MUST be available
+
+---
+
+### REQ-TASK-008: Task Completion
+
+**Tier**: MVP
+
+When a task is completed, the system MUST automatically set the `completedDate` and enforce lifecycle rules.
+
+#### Scenario: Complete a task and record completion date
+
+- GIVEN a task "Locatiebezoek" with status `active` and no `completedDate`
+- WHEN the user marks it as completed at 2026-02-25T14:30:00Z
+- THEN the `status` MUST change to `completed`
+- AND the `completedDate` MUST be set to "2026-02-25T14:30:00Z"
+- AND the task MUST remain visible in the case timeline with a green checkmark
+
+#### Scenario: Attempt to complete an already-completed task
+
+- GIVEN a task "Intake controle" already has status `completed` and completedDate "2026-01-20T10:00:00Z"
+- WHEN the user attempts to complete it again
+- THEN the system MUST reject the operation (no-op or error)
+- AND the `completedDate` MUST remain "2026-01-20T10:00:00Z"
+
+#### Scenario: Task completion updates case progress
+
+- GIVEN case #2024-042 has 5 tasks, 2 of which are completed
+- WHEN the user completes a third task
+- THEN the case detail Tasks section MUST show updated progress "3/5"
+
+---
+
+### REQ-TASK-009: Task Checklist (Sub-Items)
+
+**Tier**: V1
+
+The system SHOULD support checklists within tasks for detailed work breakdown. Checklist items are lightweight items stored as part of the task object (not separate OpenRegister objects).
+
+#### Scenario: Add checklist items to a task
+
+- GIVEN a task "Beoordeel aanvraag" with status `active`
+- WHEN the user adds checklist items:
+ - "Controleer volledigheid formulier"
+ - "Verifieer bijlagen"
+ - "Check regelgeving"
+- THEN the task object MUST store these items as an ordered list
+- AND each item MUST have a `checked` boolean (default: false) and a `label` string
+
+#### Scenario: Check off a checklist item
+
+- GIVEN a task with 3 checklist items, all unchecked
+- WHEN the user checks "Controleer volledigheid formulier"
+- THEN that item's `checked` MUST be set to true
+- AND the task card SHOULD show checklist progress (e.g., "1/3")
+
+#### Scenario: Reorder checklist items
+
+- GIVEN a task with 3 checklist items
+- WHEN the user drags "Check regelgeving" to the first position
+- THEN the order MUST be updated in the stored list
+
+#### Scenario: Checklist completion does not auto-complete the task
+
+- GIVEN a task with 3 checklist items, all checked
+- THEN the task status MUST NOT automatically change to `completed`
+- AND the user MUST still explicitly complete the task
+
+---
+
+### REQ-TASK-010: Task Dependencies
+
+**Tier**: V1
+
+The system SHOULD support declaring dependencies between tasks ("blocked by" relationships). Dependencies are advisory: they provide visual indicators but do not strictly prevent work.
+
+#### Scenario: Declare a task dependency
+
+- GIVEN task A "Draft besluit" and task B "Review documenten" in case #2024-042
+- WHEN the user sets task A as "blocked by" task B
+- THEN task A MUST store a reference to task B's UUID as a dependency
+- AND task A's card MUST show a "blocked" indicator while task B is not completed
+
+#### Scenario: Dependency resolved when blocking task completes
+
+- GIVEN task A "Draft besluit" is blocked by task B "Review documenten"
+- WHEN task B is completed
+- THEN task A's "blocked" indicator MUST be removed
+- AND task A MUST remain in its current status (the indicator is visual only)
+
+#### Scenario: View dependency chain
+
+- GIVEN task A is blocked by task B, and task B is blocked by task C
+- WHEN the user views task A's dependencies
+- THEN the system SHOULD show the full dependency chain: A depends on B depends on C
+- AND the system SHOULD warn if the chain creates a circular dependency
+
+#### Scenario: Prevent circular dependencies
+
+- GIVEN task A is blocked by task B
+- WHEN the user attempts to set task B as "blocked by" task A
+- THEN the system MUST reject the circular dependency
+- AND the error message MUST indicate that a circular dependency was detected
+
+---
+
+### REQ-TASK-011: Task Templates per Case Type
+
+**Tier**: V1
+
+The system SHOULD support defining task templates on case types. When a case of that type is created, the user can choose to instantiate the template tasks.
+
+#### Scenario: Define task template on case type
+
+- GIVEN the admin is editing case type "Omgevingsvergunning"
+- WHEN the admin defines a task template with:
+ - "Intake controle" (priority: high, relative due: +3 days)
+ - "Locatiebezoek plannen" (priority: normal, relative due: +14 days)
+ - "Beoordeel aanvraag" (priority: high, relative due: +28 days)
+ - "Draft besluit" (priority: urgent, relative due: +42 days)
+ - "Verstuur resultaat" (priority: normal, relative due: +56 days)
+- THEN the template MUST be saved on the case type configuration
+
+#### Scenario: Apply task template on case creation
+
+- GIVEN case type "Omgevingsvergunning" has 5 template tasks
+- WHEN the user creates a new case of this type with start date "2026-03-01"
+- THEN the system SHOULD offer to create the template tasks
+- AND if accepted, 5 task objects MUST be created, each as an independent OpenRegister object
+- AND relative due dates MUST be calculated from the case start date (e.g., "Intake controle" due date = 2026-03-04)
+- AND all template tasks MUST be created with status `available`
+
+#### Scenario: Skip task template on case creation
+
+- GIVEN case type "Omgevingsvergunning" has 5 template tasks
+- WHEN the user creates a new case and declines the template
+- THEN no tasks MUST be created automatically
+- AND the user can add tasks manually later
+
+---
+
+### REQ-TASK-012: Automated Task Creation on Case Status Change
+
+**Tier**: Enterprise
+
+The system MAY support automatically creating tasks when a case transitions to a specific status.
+
+#### Scenario: Auto-create tasks on status change
+
+- GIVEN case type "Omgevingsvergunning" has a rule: "When status changes to Besluitvorming, create task 'Draft besluit'"
+- WHEN case #2024-042 transitions from "In behandeling" to "Besluitvorming"
+- THEN the system MUST automatically create a task "Draft besluit"
+- AND the task MUST be linked to case #2024-042
+- AND the task MUST inherit default values from the automation rule (assignee, priority, relative due date)
+
+#### Scenario: Auto-created task notification
+
+- GIVEN an automated task creation rule exists
+- WHEN the rule fires and creates a task assigned to "jan.devries"
+- THEN "jan.devries" SHOULD receive a notification that a task was auto-created
+- AND the notification MUST indicate it was system-generated
+
+---
+
+### REQ-TASK-013: Overdue Task Management
+
+**Tier**: MVP
+
+The system MUST provide clear visual indicators for overdue tasks and support filtering/sorting by overdue status.
+
+#### Scenario: Overdue task in My Work view
+
+- GIVEN "jan.devries" has an active task "Review documenten" with dueDate "2026-02-20"
+- AND today is 2026-02-25
+- THEN the task MUST appear in the "Overdue" section of Jan's My Work view
+- AND the overdue indicator MUST show "5 days overdue" in red
+
+#### Scenario: Multiple overdue tasks sorted by urgency
+
+- GIVEN "jan.devries" has overdue tasks:
+ - "Review documenten" (5 days overdue, priority: high)
+ - "Verzamel informatie" (2 days overdue, priority: normal)
+ - "Controleer bijlagen" (1 day overdue, priority: urgent)
+- WHEN Jan views his My Work
+- THEN overdue tasks MUST be sorted by priority first (urgent, high, normal), then by overdue duration (most overdue first within the same priority)
+- AND the resulting order MUST be: "Controleer bijlagen", "Review documenten", "Verzamel informatie"
+
+#### Scenario: Task becomes overdue
+
+- GIVEN a task "Beoordeel begroting" with dueDate "2026-02-25T17:00:00Z" and status `active`
+- WHEN the current time passes "2026-02-25T17:00:00Z"
+- THEN on the next view render, the task MUST display an overdue indicator
+- AND the task MUST move to the "Overdue" group in My Work
+
+#### Scenario: Terminated or disabled tasks are not shown as overdue
+
+- GIVEN a task "Verouderde controle" with dueDate in the past and status `terminated`
+- THEN the task MUST NOT be marked as overdue
+- AND the task MUST NOT appear in the overdue section of My Work
+
+---
+
+## Accessibility
+
+All task management interfaces MUST comply with WCAG AA:
+
+- Task cards MUST have sufficient color contrast for all text and indicators
+- Overdue/priority indicators MUST NOT rely solely on color (use icons and text labels)
+- Kanban drag-and-drop MUST have a keyboard-accessible alternative (e.g., dropdown to change status)
+- Task list MUST be navigable by keyboard (Tab to move between rows, Enter to open)
+- Screen readers MUST be able to identify task status, priority, and overdue state
+
+---
+
+## Performance
+
+- The task list MUST load within 2 seconds for up to 100 tasks
+- The kanban board MUST render within 2 seconds for up to 50 cards per column
+- Drag-and-drop status changes MUST provide optimistic UI updates (move the card immediately, then confirm with the API)
+- The My Work view MUST aggregate tasks and cases in a single page load (parallel API calls)
+
+---
+
+### Current Implementation Status
+
+**MVP substantially implemented. V1/Enterprise features not implemented.**
+
+**Implemented (with file paths):**
+- **Task schema**: Defined in `lib/Settings/procest_register.json` with properties: `title`, `description`, `status` (enum: available/active/completed/terminated/disabled), `assignee`, `case` (UUID ref), `dueDate`, `priority` (enum: low/normal/high/urgent), `completedDate`. Matches the spec data model exactly.
+- **Task lifecycle**: `src/utils/taskLifecycle.js` implements the full CMMN PlanItem lifecycle with:
+ - `TASK_STATUSES` constant object
+ - `TRANSITION_MAP`: available -> [active, terminated, disabled], active -> [completed, terminated], completed/terminated/disabled -> [] (terminal)
+ - `getAllowedTransitions(currentStatus)`, `validateTransition(from, to)`, `getStatusLabel(status)`, `getTransitionLabel(targetStatus)`, `isTerminalStatus(status)` functions
+ - Localized status labels (English, Dutch pending)
+- **Task helpers**: `src/utils/taskHelpers.js` provides utility functions for task display and overdue calculations.
+- **Task list view**: `src/views/tasks/TaskList.vue` -- paginated list of tasks with status, assignee, due date, and priority display (REQ-TASK-004).
+- **Task detail view**: `src/views/tasks/TaskDetail.vue` -- full task detail with editable fields.
+- **Task create dialog**: `src/views/tasks/TaskCreateDialog.vue` -- form for creating new tasks linked to a case.
+- **Task API service**: `src/services/taskApi.js` -- `fetchTasksForCases()` for CalDAV task integration.
+- **Task CRUD**: Via the shared object store (`src/store/modules/object.js`) for OpenRegister-based tasks.
+- **My Work integration**: `src/views/MyWork.vue` includes tasks in the unified view with overdue highlighting, priority indicators, grouped sections (REQ-TASK-013).
+- **Dashboard widgets**: `lib/Dashboard/MyTasksWidget.php` and `src/views/widgets/MyTasksWidget.vue` -- Nextcloud dashboard widget showing assigned tasks. `src/views/dashboard/MyWorkPreview.vue` shows task summary on app dashboard.
+- **Navigation**: `src/navigation/MainMenu.vue` includes "Tasks" menu item linked to `/tasks` route.
+- **Router**: `src/router/index.js` includes routes for `/tasks`, `/tasks/new`, and `/tasks/:id`.
+- **Overdue highlighting**: Implemented in `MyWork.vue` with red indicators and "X days overdue" text (REQ-TASK-005, REQ-TASK-013).
+- **Priority badges**: Priority indicators shown in My Work view for high and urgent priorities.
+
+**Architecture note:** Tasks have a dual implementation:
+1. OpenRegister `task` schema objects (used by the object store for CRUD)
+2. CalDAV tasks via `fetchTasksForCases()` in `src/services/taskApi.js` (used by My Work view)
+
+This duality means some views use OpenRegister tasks while others use CalDAV tasks. The spec assumes a single OpenRegister-based task system.
+
+**Not yet implemented:**
+- **REQ-TASK-007: Kanban board view (V1)**: No kanban/board view with columns per status. No drag-and-drop status transitions. No board/list toggle.
+- **REQ-TASK-009: Task checklists (V1)**: No checklist/sub-item support on tasks.
+- **REQ-TASK-010: Task dependencies (V1)**: No "blocked by" relationship support.
+- **REQ-TASK-011: Task templates per case type (V1)**: No task template configuration on case types. No auto-creation of template tasks on case creation.
+- **REQ-TASK-012: Automated task creation on status change (Enterprise)**: No automation rules for task creation.
+- **Task assignment notifications**: No Nextcloud notifications sent when tasks are assigned or reassigned.
+- **Task search by title**: Basic search may exist via the object store's `_search` parameter, but dedicated task search UI is not prominent.
+- **Keyboard navigation**: No explicit keyboard navigation support in task list or cards.
+- **Screen reader support**: No ARIA attributes for task status, priority, or overdue state.
+
+### Standards & References
+
+- **CMMN 1.1**: Task lifecycle states (Available, Active, Completed, Terminated, Disabled) follow the CMMN PlanItem lifecycle exactly. Transition rules match CMMN specification. Implemented in `src/utils/taskLifecycle.js`.
+- **Schema.org**: Tasks typed as `schema:Action` with `actionStatus` in `procest_register.json`.
+- **BPMN 2.0**: Task patterns for assignment and lifecycle management.
+- **ZGW APIs**: No direct ZGW equivalent for tasks (ZGW does not define a task resource), but tasks complement the ZGW Zaak lifecycle.
+- **WCAG 2.1 AA**: Spec requires color-independent indicators and keyboard accessibility. Partially implemented (text labels for overdue, but no keyboard nav).
+
+### Specificity Assessment
+
+- **MVP requirements are well-specified and mostly implemented.** The task CRUD, lifecycle, assignment, list view, and overdue management are clear and actionable.
+- **V1 features (kanban, checklists, dependencies, templates) are well-specified** with concrete scenarios but not yet implemented.
+- **Task architecture ambiguity**: The dual CalDAV/OpenRegister task system needs resolution. The spec assumes OpenRegister-only tasks.
+- **Open questions:**
+ - Should tasks migrate fully to OpenRegister, or should CalDAV integration be maintained for Nextcloud ecosystem compatibility?
+ - How should kanban drag-and-drop handle the keyboard-accessible alternative (dropdown vs. move buttons)?
+ - Should task templates be stored as JSON arrays on the case type object or as separate OpenRegister objects?
+ - How should task dependencies be stored (array of UUIDs on the task object, or separate relation objects)?
diff --git a/openspec/specs/zgw-autorisaties/spec.md b/openspec/specs/zgw-autorisaties/spec.md
new file mode 100644
index 00000000..3b99259d
--- /dev/null
+++ b/openspec/specs/zgw-autorisaties/spec.md
@@ -0,0 +1,57 @@
+# Spec: ZGW Autorisaties API (AC)
+
+## ZGW Standard References
+
+### Official Documentation
+- **Standard overview**: https://vng-realisatie.github.io/gemma-zaken/standaard/autorisaties/
+- **Developer guide**: https://vng-realisatie.github.io/gemma-zaken/ontwikkelaars/
+
+### OpenAPI Specifications
+- **OAS (gemma-zaken canonical)**: [api-specificatie/ac/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/ac/openapi.yaml)
+ - Raw: https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/ac/openapi.yaml
+- **Reference implementation OAS**: [src/openapi.yaml](https://github.com/VNG-Realisatie/autorisaties-api/blob/master/src/openapi.yaml)
+ - Raw: https://raw.githubusercontent.com/VNG-Realisatie/autorisaties-api/master/src/openapi.yaml
+
+### Source Documentation (Markdown)
+- **Standard page**: [docs/standaard/autorisaties/index.md](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/docs/standaard/autorisaties/index.md)
+- **Authorization spec**: [src/autorisaties.md](https://github.com/VNG-Realisatie/autorisaties-api/blob/master/src/autorisaties.md)
+- **Notification spec**: [src/notificaties.md](https://github.com/VNG-Realisatie/autorisaties-api/blob/master/src/notificaties.md)
+
+### Note on Versioning
+The AC in gemma-zaken does NOT have versioned subdirectories — the spec files sit directly in the `ac/` folder.
+
+## Requirements
+
+### Requirement: AC Resource Coverage
+The implementation MUST support:
+- **Applicatie** — API client registration with credentials and permissions
+ - Fields: uuid, clientIds, label, heeftAlleAutorisaties, autorisaties[]
+- **Autorisatie** — Permission grants (inline in Applicatie)
+ - Fields: component, scopes[], zaaktype, maxVertrouwelijkheidaanduiding
+
+### Requirement: JWT-ZGW Authentication
+- Validate incoming JWT tokens per the ZGW standard
+- JWT claims: `iss` (maps to clientId), `iat`, `client_id`, `user_id`, `user_representation`
+- Token signed with shared secret (HMAC) registered in the Applicatie
+
+### Requirement: Scope Enforcement
+| Scope | Meaning |
+|-------|---------|
+| `*.lezen` | Read access (GET) |
+| `*.aanmaken` | Create access (POST) |
+| `*.bijwerken` | Update access (PUT, PATCH) |
+| `*.verwijderen` | Delete access (DELETE) |
+
+Scopes are prefixed by component: `zaken.lezen`, `documenten.aanmaken`, etc.
+
+### Requirement: Superuser Mode
+Applicaties with `heeftAlleAutorisaties: true` bypass all scope checks.
+
+### Requirement: Confidentiality Enforcement
+`maxVertrouwelijkheidaanduiding` limits access to documents/cases at or below the specified confidentiality level. Levels (low to high): `openbaar`, `beperkt_openbaar`, `intern`, `zaakvertrouwelijk`, `vertrouwelijk`, `confidentieel`, `geheim`, `zeer_geheim`.
+
+### Requirement: Applicatie-Consumer Mapping
+ZGW Applicatie resources MUST map to OpenRegister's Consumer entity for credential storage and JWT validation.
+
+### Requirement: ZGW Pagination
+All list endpoints MUST return `{ count, next, previous, results }` format.
diff --git a/openspec/specs/zgw-newman/spec.md b/openspec/specs/zgw-newman/spec.md
new file mode 100644
index 00000000..a7d53c99
--- /dev/null
+++ b/openspec/specs/zgw-newman/spec.md
@@ -0,0 +1,51 @@
+# Spec: ZGW Newman Test Suite
+
+## ZGW Standard References
+
+### All ZGW API Specifications (tested by the suite)
+| Component | OAS Source | Raw URL |
+|-----------|-----------|---------|
+| ZRC (Zaken) | [zrc/current_version/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/zrc/current_version/openapi.yaml) | https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/zrc/current_version/openapi.yaml |
+| ZTC (Catalogi) | [ztc/current_version/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/ztc/current_version/openapi.yaml) | https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/ztc/current_version/openapi.yaml |
+| BRC (Besluiten) | [brc/current_version/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/brc/current_version/openapi.yaml) | https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/brc/current_version/openapi.yaml |
+| DRC (Documenten) | [drc/current_version/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/drc/current_version/openapi.yaml) | https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/drc/current_version/openapi.yaml |
+| NRC (Notificaties) | [nrc/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/nrc/openapi.yaml) | https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/nrc/openapi.yaml |
+| AC (Autorisaties) | [ac/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/ac/openapi.yaml) | https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/ac/openapi.yaml |
+
+### Official Documentation
+- **Standard overview**: https://vng-realisatie.github.io/gemma-zaken/standaard/
+- **Developer guide**: https://vng-realisatie.github.io/gemma-zaken/ontwikkelaars/
+- **Source repo**: https://github.com/VNG-Realisatie/gemma-zaken
+
+### Reference Implementation Repos
+- Zaken: https://github.com/VNG-Realisatie/zaken-api
+- Documenten: https://github.com/VNG-Realisatie/documenten-api
+- Catalogi: https://github.com/VNG-Realisatie/catalogi-api
+- Besluiten: https://github.com/VNG-Realisatie/besluiten-api
+- Notificaties: https://github.com/VNG-Realisatie/notificaties-api
+- Autorisaties: https://github.com/VNG-Realisatie/autorisaties-api
+
+## Requirements
+
+### Requirement: Test Collection Compatibility
+Newman MUST run the existing Postman collections from `procest/data/` without modification:
+- `ZGW OAS tests.postman_collection.json`
+- `ZGW business rules.postman_collection.json`
+
+### Requirement: Environment Variable Coverage
+The environment file MUST provide all variables referenced by both collections (67 for OAS, 120 for business rules). Dynamic variables set by pre-request scripts (e.g., `created_zaak_url`) are handled by the collections themselves.
+
+### Requirement: Sequential Execution
+OAS tests MUST run before business rules tests. Business rules depend on the data model being OAS-compliant.
+
+### Requirement: Selective Execution
+Support running:
+- All tests (default)
+- OAS tests only (`--oas-only`)
+- Business rules only (`--business-only`)
+- Specific API component (`--folder zrc`)
+
+### Requirement: Docker Compatibility
+The test runner MUST work both:
+- On the host (delegating execution to the Docker container)
+- Inside the container (running Newman directly)
diff --git a/openspec/specs/zgw-notificaties/spec.md b/openspec/specs/zgw-notificaties/spec.md
new file mode 100644
index 00000000..fe5b2d37
--- /dev/null
+++ b/openspec/specs/zgw-notificaties/spec.md
@@ -0,0 +1,55 @@
+# Spec: ZGW Notificaties API (NRC)
+
+## ZGW Standard References
+
+### Official Documentation
+- **Standard overview**: https://vng-realisatie.github.io/gemma-zaken/standaard/notificaties/
+- **Developer guide**: https://vng-realisatie.github.io/gemma-zaken/ontwikkelaars/
+
+### OpenAPI Specifications
+- **Provider OAS (gemma-zaken canonical)**: [api-specificatie/nrc/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/nrc/openapi.yaml)
+ - Raw: https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/nrc/openapi.yaml
+- **Consumer OAS**: [api-specificatie/nrc/consumer-api/openapi.yaml](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/api-specificatie/nrc/consumer-api/openapi.yaml)
+ - Raw: https://raw.githubusercontent.com/VNG-Realisatie/gemma-zaken/master/api-specificatie/nrc/consumer-api/openapi.yaml
+- **Reference implementation OAS**: [src/openapi.yaml](https://github.com/VNG-Realisatie/notificaties-api/blob/master/src/openapi.yaml)
+ - Raw: https://raw.githubusercontent.com/VNG-Realisatie/notificaties-api/master/src/openapi.yaml
+
+### Source Documentation (Markdown)
+- **Standard page**: [docs/standaard/notificaties/index.md](https://github.com/VNG-Realisatie/gemma-zaken/blob/master/docs/standaard/notificaties/index.md)
+- **Authorization spec**: [src/autorisaties.md](https://github.com/VNG-Realisatie/notificaties-api/blob/master/src/autorisaties.md)
+- **Notification spec**: [src/notificaties.md](https://github.com/VNG-Realisatie/notificaties-api/blob/master/src/notificaties.md)
+
+### Note on Versioning
+The NRC in gemma-zaken does NOT have versioned subdirectories — the spec files sit directly in the `nrc/` folder.
+
+## Requirements
+
+### Requirement: NRC Resource Coverage
+The implementation MUST support these resources:
+- **Kanaal** — Notification channels (one per resource type)
+- **Abonnement** — Subscriptions with callback URLs and filters
+- **Notificatie** — Published event notifications
+
+### Requirement: Notification Payload Format
+```json
+{
+ "kanaal": "zaken",
+ "hoofdObject": "https://host/api/zgw/zaken/v1/zaken/{uuid}",
+ "resource": "zaak",
+ "resourceUrl": "https://host/api/zgw/zaken/v1/zaken/{uuid}",
+ "actie": "create",
+ "aanmaakdatum": "2026-03-07T10:00:00Z",
+ "kenmerken": {}
+}
+```
+
+### Requirement: Callback Delivery
+- Notifications are delivered via HTTP POST to subscriber callback URLs
+- The subscriber's configured `auth` header is included in callbacks
+- Delivery failures MUST NOT block the original operation
+
+### Requirement: Default Channels
+Pre-register channels: `zaken`, `documenten`, `besluiten`, `catalogi`, `autorisaties`
+
+### Requirement: ZGW Pagination
+All list endpoints MUST return `{ count, next, previous, results }` format.
diff --git a/openspec/verify-l10n.js b/openspec/verify-l10n.js
new file mode 100644
index 00000000..ceb792c5
--- /dev/null
+++ b/openspec/verify-l10n.js
@@ -0,0 +1,155 @@
+#!/usr/bin/env node
+/**
+ * Verify l10n: JSON valid, key sync, placeholders, code coverage.
+ * Run from procest app root: node openspec/verify-l10n.js
+ */
+const fs = require('fs')
+const path = require('path')
+
+const L10N_DIR = path.join(__dirname, '..', 'l10n')
+const SRC_DIR = path.join(__dirname, '..', 'src')
+
+function loadJson(file) {
+ try {
+ const raw = fs.readFileSync(file, 'utf8')
+ const data = JSON.parse(raw)
+ return { data, error: null }
+ } catch (e) {
+ return { data: null, error: e.message }
+ }
+}
+
+function main() {
+ let failed = false
+
+ // 1. Load and validate JSON
+ const enPath = path.join(L10N_DIR, 'en.json')
+ const nlPath = path.join(L10N_DIR, 'nl.json')
+
+ const enResult = loadJson(enPath)
+ const nlResult = loadJson(nlPath)
+
+ if (enResult.error) {
+ console.error('FAIL: en.json parsing error:', enResult.error)
+ failed = true
+ } else {
+ console.log('OK: en.json valid JSON')
+ }
+
+ if (nlResult.error) {
+ console.error('FAIL: nl.json parsing error:', nlResult.error)
+ failed = true
+ } else {
+ console.log('OK: nl.json valid JSON')
+ }
+
+ if (failed) process.exit(1)
+
+ const enKeys = new Set(Object.keys(enResult.data.translations))
+ const nlKeys = new Set(Object.keys(nlResult.data.translations))
+
+ // 2. REQ-L10N-001: Identical key sets
+ const onlyInEn = [...enKeys].filter(k => !nlKeys.has(k))
+ const onlyInNl = [...nlKeys].filter(k => !enKeys.has(k))
+
+ if (onlyInEn.length > 0) {
+ console.error('FAIL: Keys in en.json but not nl.json:', onlyInEn.slice(0, 5).join(', '), onlyInEn.length > 5 ? `... (+${onlyInEn.length - 5} more)` : '')
+ failed = true
+ }
+ if (onlyInNl.length > 0) {
+ console.error('FAIL: Keys in nl.json but not en.json:', onlyInNl.slice(0, 5).join(', '), onlyInNl.length > 5 ? `... (+${onlyInNl.length - 5} more)` : '')
+ failed = true
+ }
+ if (onlyInEn.length === 0 && onlyInNl.length === 0) {
+ console.log('OK: en.json and nl.json have identical keys (' + enKeys.size + ' total)')
+ }
+
+ // 3. Placeholder preservation
+ const placeholderKeys = [...enKeys].filter(k => /\{[a-zA-Z]+\}/.test(enResult.data.translations[k]))
+ let placeholderOk = true
+ for (const key of placeholderKeys) {
+ const enVal = enResult.data.translations[key]
+ const nlVal = nlResult.data.translations[key]
+ const enPlaceholders = (enVal.match(/\{[a-zA-Z]+\}/g) || []).sort().join(',')
+ const nlPlaceholders = (nlVal.match(/\{[a-zA-Z]+\}/g) || []).sort().join(',')
+ if (enPlaceholders !== nlPlaceholders) {
+ console.error('FAIL: Placeholder mismatch for key:', key, '| en:', enPlaceholders, '| nl:', nlPlaceholders)
+ placeholderOk = false
+ failed = true
+ }
+ }
+ if (placeholderOk && placeholderKeys.length > 0) {
+ console.log('OK: Placeholders preserved in nl.json for', placeholderKeys.length, 'keys')
+ }
+
+ // 4. Extract keys from t('procest', ...) in src
+ function walkDir(dir, files = []) {
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
+ for (const e of entries) {
+ const full = path.join(dir, e.name)
+ if (e.isDirectory() && e.name !== 'node_modules') {
+ walkDir(full, files)
+ } else if (e.isFile() && (e.name.endsWith('.vue') || e.name.endsWith('.js'))) {
+ files.push(full)
+ }
+ }
+ return files
+ }
+ const srcFiles = walkDir(SRC_DIR)
+
+ function unescapeKey(s) {
+ return s.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
+ }
+
+ const usedKeys = new Set()
+ const tPatternSingle = /t\s*\(\s*['"]procest['"]\s*,\s*'((?:[^'\\]|\\.)*)'/g
+ const tPatternDouble = /t\s*\(\s*['"]procest['"]\s*,\s*"((?:[^"\\]|\\.)*)"/g
+ for (const file of srcFiles) {
+ try {
+ const content = fs.readFileSync(file, 'utf8')
+ let m
+ tPatternSingle.lastIndex = 0
+ while ((m = tPatternSingle.exec(content)) !== null) {
+ usedKeys.add(unescapeKey(m[1]))
+ }
+ tPatternDouble.lastIndex = 0
+ while ((m = tPatternDouble.exec(content)) !== null) {
+ usedKeys.add(unescapeKey(m[1]))
+ }
+ } catch (_) {}
+ }
+
+ const missingInL10n = [...usedKeys].filter(k => !enKeys.has(k))
+ if (missingInL10n.length > 0) {
+ console.error('FAIL: Keys used in code but missing from l10n:', missingInL10n.slice(0, 10).join(', '), missingInL10n.length > 10 ? `... (+${missingInL10n.length - 10} more)` : '')
+ failed = true
+ } else {
+ console.log('OK: All', usedKeys.size, 'keys used in code exist in l10n')
+ }
+
+ // Orphan check: keys in l10n not used in code (original 55 are allowed)
+ const originalKeys = new Set([
+ 'Procest', 'Dashboard', 'Cases', 'Case', 'Tasks', 'Task', 'New case', 'New task',
+ 'Title', 'Description', 'Status', 'Assignee', 'Priority', 'Due date', 'Created', 'Updated',
+ 'Closed', 'Save', 'Cancel', 'Delete', 'Edit', 'Search', 'Loading...', 'No cases found',
+ 'No tasks found', 'Are you sure you want to delete this?', 'Case created successfully',
+ 'Case updated successfully', 'Case deleted successfully', 'Task created successfully',
+ 'Task updated successfully', 'Settings', 'Register', 'Case schema', 'Task schema',
+ 'Status schema', 'Role schema', 'Result schema', 'Decision schema', 'Configuration saved',
+ 'Welcome to Procest', 'Manage your cases and tasks', 'open', 'in_progress', 'closed',
+ 'low', 'normal', 'high', 'urgent', 'Back to list', 'Previous', 'Next'
+ ])
+ const orphanKeys = [...enKeys].filter(k => !usedKeys.has(k) && !originalKeys.has(k))
+ if (orphanKeys.length > 0) {
+ console.warn('WARN: Keys in l10n not found in code (may be used dynamically):', orphanKeys.length)
+ }
+
+ console.log('')
+ if (failed) {
+ console.error('VERIFICATION FAILED')
+ process.exit(1)
+ }
+ console.log('VERIFICATION PASSED')
+}
+
+main()
diff --git a/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php
index 92740a93..4e21f8d3 100644
--- a/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php
+++ b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php
@@ -1,408 +1,408 @@
-method(name: $value)
- * - self::method(name: $value) / static::method(name: $value)
- * - parent::method(name: $value)
- * - new OurClass(name: $value) — classes from the same app namespace
- *
- * Allows positional arguments for external code:
- * - PHP built-in functions (strlen, array_map, sprintf, etc.)
- * - Nextcloud/third-party method calls ($variable->method() where $variable !== $this)
- * - Any call we cannot determine is "our code"
- *
- * @author Conduction
- * @package CustomSniffs
- */
-
-namespace CustomSniffs\Sniffs\Functions;
-
-use PHP_CodeSniffer\Sniffs\Sniff;
-use PHP_CodeSniffer\Files\File;
-
-/**
- * NamedParametersSniff — enforces named parameters for internal code.
- */
-class NamedParametersSniff implements Sniff
-{
-
-
- /**
- * Returns tokens this sniff listens for.
- *
- * @return array
- */
- public function register(): array
- {
- return [T_STRING];
-
- }//end register()
-
-
- /**
- * Process a T_STRING token — check if it's a function/method call to our code.
- *
- * @param File $phpcsFile The file being scanned.
- * @param int $stackPtr Position of the T_STRING token.
- *
- * @return void
- */
- public function process(File $phpcsFile, $stackPtr): void
- {
- $tokens = $phpcsFile->getTokens();
- $functionName = $tokens[$stackPtr]['content'];
-
- // Must be followed by ( to be a function/method call.
- $openParen = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
- if ($openParen === false || $tokens[$openParen]['code'] !== T_OPEN_PARENTHESIS) {
- return;
- }
-
- // Skip function/method definitions.
- if ($this->isFunctionDefinition(phpcsFile: $phpcsFile, stackPtr: $stackPtr) === true) {
- return;
- }
-
- // Only check calls to our own code.
- if ($this->isInternalCall(phpcsFile: $phpcsFile, stackPtr: $stackPtr) === false) {
- return;
- }
-
- // Get the closing parenthesis.
- if (isset($tokens[$openParen]['parenthesis_closer']) === false) {
- return;
- }
-
- $closeParen = $tokens[$openParen]['parenthesis_closer'];
-
- // Check if there are any arguments.
- $firstContent = $phpcsFile->findNext(
- types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
- start: ($openParen + 1),
- end: $closeParen,
- exclude: true
- );
- if ($firstContent === false) {
- return;
- }
-
- // Check if all arguments use named parameters.
- if ($this->hasUnnamedArguments(phpcsFile: $phpcsFile, openParen: $openParen, closeParen: $closeParen) === true) {
- $error = 'All arguments in calls to internal code must use named parameters: %s(paramName: $value)';
- $phpcsFile->addError($error, $stackPtr, 'RequireNamedParameters', [$functionName]);
- }
-
- }//end process()
-
-
- /**
- * Check if the T_STRING at $stackPtr is part of a function/method definition (not a call).
- *
- * @param File $phpcsFile The file being scanned.
- * @param int $stackPtr Position of the T_STRING token.
- *
- * @return bool True if this is a definition, false if it's a call.
- */
- private function isFunctionDefinition(File $phpcsFile, int $stackPtr): bool
- {
- $tokens = $phpcsFile->getTokens();
- $prev = ($stackPtr - 1);
- while ($prev >= 0) {
- $code = $tokens[$prev]['code'];
- if ($code === T_FUNCTION) {
- return true;
- }
-
- // Stop at statement/block boundaries.
- if ($code === T_SEMICOLON
- || $code === T_OPEN_CURLY_BRACKET
- || $code === T_CLOSE_CURLY_BRACKET
- ) {
- return false;
- }
-
- $prev--;
- }
-
- return false;
-
- }//end isFunctionDefinition()
-
-
- /**
- * Determine if the function/method call at $stackPtr is to our own code.
- *
- * Matches:
- * - $this->method()
- * - self::method() / static::method() / parent::method()
- * - new OurClass() where OurClass is from the same app namespace
- *
- * @param File $phpcsFile The file being scanned.
- * @param int $stackPtr Position of the T_STRING (function/method name).
- *
- * @return bool True if call is to internal code.
- */
- private function isInternalCall(File $phpcsFile, int $stackPtr): bool
- {
- $tokens = $phpcsFile->getTokens();
-
- $prev = $phpcsFile->findPrevious(
- types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
- start: ($stackPtr - 1),
- end: null,
- exclude: true
- );
- if ($prev === false) {
- return false;
- }
-
- $prevCode = $tokens[$prev]['code'];
-
- // Case 1: $this->method().
- if ($prevCode === T_OBJECT_OPERATOR) {
- $beforeArrow = $phpcsFile->findPrevious(
- types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
- start: ($prev - 1),
- end: null,
- exclude: true
- );
- return ($beforeArrow !== false
- && $tokens[$beforeArrow]['code'] === T_VARIABLE
- && $tokens[$beforeArrow]['content'] === '$this');
- }
-
- // Case 2: self::method() / static::method() / parent::method().
- if ($prevCode === T_DOUBLE_COLON) {
- $beforeColon = $phpcsFile->findPrevious(
- types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
- start: ($prev - 1),
- end: null,
- exclude: true
- );
- return ($beforeColon !== false
- && in_array($tokens[$beforeColon]['code'], [T_SELF, T_STATIC, T_PARENT], true) === true);
- }
-
- // Case 3: new OurClass().
- if ($prevCode === T_NEW) {
- return $this->isClassFromOurNamespace(phpcsFile: $phpcsFile, classNamePtr: $stackPtr);
- }
-
- return false;
-
- }//end isInternalCall()
-
-
- /**
- * Check if the class at $classNamePtr is from the same app namespace as the current file.
- *
- * Compares the class's use-import path against the file's OCA\AppName\ prefix.
- *
- * @param File $phpcsFile The file being scanned.
- * @param int $classNamePtr Position of the class name token.
- *
- * @return bool True if the class is from our app namespace.
- */
- private function isClassFromOurNamespace(File $phpcsFile, int $classNamePtr): bool
- {
- $tokens = $phpcsFile->getTokens();
- $className = $tokens[$classNamePtr]['content'];
-
- // Find the file's namespace declaration.
- $appPrefix = $this->getAppNamespacePrefix(phpcsFile: $phpcsFile);
- if ($appPrefix === null) {
- return false;
- }
-
- // Scan use-statements for an import matching this class name from our namespace.
- for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
- if ($tokens[$i]['code'] === T_USE) {
- $usePath = $this->getUseStatementPath(phpcsFile: $phpcsFile, usePtr: $i);
- if ($usePath === null) {
- continue;
- }
-
- // Extract the imported short name (last segment).
- $segments = explode(separator: '\\', string: $usePath);
- $importedName = end($segments);
-
- if ($importedName === $className
- && str_starts_with(haystack: $usePath, needle: $appPrefix) === true
- ) {
- return true;
- }
- }//end if
-
- // Stop scanning after the class declaration.
- if (in_array($tokens[$i]['code'], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true) === true) {
- break;
- }
- }//end for
-
- return false;
-
- }//end isClassFromOurNamespace()
-
-
- /**
- * Get the app namespace prefix (e.g., "OCA\MyDash") from the file's namespace declaration.
- *
- * @param File $phpcsFile The file being scanned.
- *
- * @return string|null The app prefix or null if not found.
- */
- private function getAppNamespacePrefix(File $phpcsFile): ?string
- {
- $tokens = $phpcsFile->getTokens();
- for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
- if ($tokens[$i]['code'] === T_NAMESPACE) {
- $namespace = '';
- $j = ($i + 1);
- while ($j < $phpcsFile->numTokens && $tokens[$j]['code'] !== T_SEMICOLON) {
- if ($tokens[$j]['code'] !== T_WHITESPACE) {
- $namespace .= $tokens[$j]['content'];
- }
-
- $j++;
- }
-
- // Extract first two segments: OCA\AppName.
- $parts = explode(separator: '\\', string: $namespace);
- if (count($parts) >= 2) {
- return $parts[0].'\\'.$parts[1];
- }
-
- return null;
- }//end if
- }//end for
-
- return null;
-
- }//end getAppNamespacePrefix()
-
-
- /**
- * Extract the full path from a use-statement starting at $usePtr.
- *
- * @param File $phpcsFile The file being scanned.
- * @param int $usePtr Position of the T_USE token.
- *
- * @return string|null The use path or null if parsing failed.
- */
- private function getUseStatementPath(File $phpcsFile, int $usePtr): ?string
- {
- $tokens = $phpcsFile->getTokens();
- $usePath = '';
- $j = ($usePtr + 1);
- while ($j < $phpcsFile->numTokens) {
- $code = $tokens[$j]['code'];
- if ($code === T_SEMICOLON || $code === T_OPEN_CURLY_BRACKET) {
- break;
- }
-
- // Stop at 'as' keyword (aliases) — use the path before it.
- if ($code === T_AS) {
- break;
- }
-
- if ($code !== T_WHITESPACE) {
- $usePath .= $tokens[$j]['content'];
- }
-
- $j++;
- }
-
- $usePath = trim(string: $usePath);
- return ($usePath !== '') ? $usePath : null;
-
- }//end getUseStatementPath()
-
-
- /**
- * Check if the arguments between $openParen and $closeParen contain any unnamed arguments.
- *
- * An argument is "named" if its first significant token is followed by T_COLON.
- * Handles nested parentheses, brackets, and braces correctly.
- *
- * @param File $phpcsFile The file being scanned.
- * @param int $openParen Position of the opening parenthesis.
- * @param int $closeParen Position of the closing parenthesis.
- *
- * @return bool True if any argument is positional (unnamed).
- */
- private function hasUnnamedArguments(File $phpcsFile, int $openParen, int $closeParen): bool
- {
- $tokens = $phpcsFile->getTokens();
- $parenDepth = 0;
- $bracketDepth = 0;
- $braceDepth = 0;
- $atArgumentStart = true;
-
- for ($i = ($openParen + 1); $i < $closeParen; $i++) {
- $code = $tokens[$i]['code'];
-
- // Track nesting depth.
- if ($code === T_OPEN_PARENTHESIS) {
- $parenDepth++;
- } elseif ($code === T_CLOSE_PARENTHESIS) {
- $parenDepth--;
- } elseif ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
- $bracketDepth++;
- } elseif ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
- $bracketDepth--;
- } elseif ($code === T_OPEN_CURLY_BRACKET) {
- $braceDepth++;
- } elseif ($code === T_CLOSE_CURLY_BRACKET) {
- $braceDepth--;
- }
-
- // Only examine tokens at the top level of the argument list.
- if ($parenDepth > 0 || $bracketDepth > 0 || $braceDepth > 0) {
- continue;
- }
-
- // Comma at top level → next argument starts.
- if ($code === T_COMMA) {
- $atArgumentStart = true;
- continue;
- }
-
- // Skip whitespace and comments at argument start.
- if ($atArgumentStart === true
- && in_array($code, [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true) === true
- ) {
- continue;
- }
-
- if ($atArgumentStart === true) {
- $atArgumentStart = false;
-
- // Skip spread operator (...$args).
- if ($code === T_ELLIPSIS) {
- continue;
- }
-
- // Check if this argument is named: token followed by ':'.
- $nextNonWs = $phpcsFile->findNext(
- types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
- start: ($i + 1),
- end: $closeParen,
- exclude: true
- );
-
- $isNamed = ($nextNonWs !== false && $tokens[$nextNonWs]['code'] === T_COLON);
-
- if ($isNamed === false) {
- return true;
- }
- }//end if
- }//end for
-
- return false;
-
- }//end hasUnnamedArguments()
-
-
-}//end class
+method(name: $value)
+ * - self::method(name: $value) / static::method(name: $value)
+ * - parent::method(name: $value)
+ * - new OurClass(name: $value) — classes from the same app namespace
+ *
+ * Allows positional arguments for external code:
+ * - PHP built-in functions (strlen, array_map, sprintf, etc.)
+ * - Nextcloud/third-party method calls ($variable->method() where $variable !== $this)
+ * - Any call we cannot determine is "our code"
+ *
+ * @author Conduction
+ * @package CustomSniffs
+ */
+
+namespace CustomSniffs\Sniffs\Functions;
+
+use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Files\File;
+
+/**
+ * NamedParametersSniff — enforces named parameters for internal code.
+ */
+class NamedParametersSniff implements Sniff
+{
+
+
+ /**
+ * Returns tokens this sniff listens for.
+ *
+ * @return array
+ */
+ public function register(): array
+ {
+ return [T_STRING];
+
+ }//end register()
+
+
+ /**
+ * Process a T_STRING token — check if it's a function/method call to our code.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr Position of the T_STRING token.
+ *
+ * @return void
+ */
+ public function process(File $phpcsFile, $stackPtr): void
+ {
+ $tokens = $phpcsFile->getTokens();
+ $functionName = $tokens[$stackPtr]['content'];
+
+ // Must be followed by ( to be a function/method call.
+ $openParen = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
+ if ($openParen === false || $tokens[$openParen]['code'] !== T_OPEN_PARENTHESIS) {
+ return;
+ }
+
+ // Skip function/method definitions.
+ if ($this->isFunctionDefinition(phpcsFile: $phpcsFile, stackPtr: $stackPtr) === true) {
+ return;
+ }
+
+ // Only check calls to our own code.
+ if ($this->isInternalCall(phpcsFile: $phpcsFile, stackPtr: $stackPtr) === false) {
+ return;
+ }
+
+ // Get the closing parenthesis.
+ if (isset($tokens[$openParen]['parenthesis_closer']) === false) {
+ return;
+ }
+
+ $closeParen = $tokens[$openParen]['parenthesis_closer'];
+
+ // Check if there are any arguments.
+ $firstContent = $phpcsFile->findNext(
+ types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
+ start: ($openParen + 1),
+ end: $closeParen,
+ exclude: true
+ );
+ if ($firstContent === false) {
+ return;
+ }
+
+ // Check if all arguments use named parameters.
+ if ($this->hasUnnamedArguments(phpcsFile: $phpcsFile, openParen: $openParen, closeParen: $closeParen) === true) {
+ $error = 'All arguments in calls to internal code must use named parameters: %s(paramName: $value)';
+ $phpcsFile->addError($error, $stackPtr, 'RequireNamedParameters', [$functionName]);
+ }
+
+ }//end process()
+
+
+ /**
+ * Check if the T_STRING at $stackPtr is part of a function/method definition (not a call).
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr Position of the T_STRING token.
+ *
+ * @return bool True if this is a definition, false if it's a call.
+ */
+ private function isFunctionDefinition(File $phpcsFile, int $stackPtr): bool
+ {
+ $tokens = $phpcsFile->getTokens();
+ $prev = ($stackPtr - 1);
+ while ($prev >= 0) {
+ $code = $tokens[$prev]['code'];
+ if ($code === T_FUNCTION) {
+ return true;
+ }
+
+ // Stop at statement/block boundaries.
+ if ($code === T_SEMICOLON
+ || $code === T_OPEN_CURLY_BRACKET
+ || $code === T_CLOSE_CURLY_BRACKET
+ ) {
+ return false;
+ }
+
+ $prev--;
+ }
+
+ return false;
+
+ }//end isFunctionDefinition()
+
+
+ /**
+ * Determine if the function/method call at $stackPtr is to our own code.
+ *
+ * Matches:
+ * - $this->method()
+ * - self::method() / static::method() / parent::method()
+ * - new OurClass() where OurClass is from the same app namespace
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr Position of the T_STRING (function/method name).
+ *
+ * @return bool True if call is to internal code.
+ */
+ private function isInternalCall(File $phpcsFile, int $stackPtr): bool
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $prev = $phpcsFile->findPrevious(
+ types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
+ start: ($stackPtr - 1),
+ end: null,
+ exclude: true
+ );
+ if ($prev === false) {
+ return false;
+ }
+
+ $prevCode = $tokens[$prev]['code'];
+
+ // Case 1: $this->method().
+ if ($prevCode === T_OBJECT_OPERATOR) {
+ $beforeArrow = $phpcsFile->findPrevious(
+ types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
+ start: ($prev - 1),
+ end: null,
+ exclude: true
+ );
+ return ($beforeArrow !== false
+ && $tokens[$beforeArrow]['code'] === T_VARIABLE
+ && $tokens[$beforeArrow]['content'] === '$this');
+ }
+
+ // Case 2: self::method() / static::method() / parent::method().
+ if ($prevCode === T_DOUBLE_COLON) {
+ $beforeColon = $phpcsFile->findPrevious(
+ types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
+ start: ($prev - 1),
+ end: null,
+ exclude: true
+ );
+ return ($beforeColon !== false
+ && in_array($tokens[$beforeColon]['code'], [T_SELF, T_STATIC, T_PARENT], true) === true);
+ }
+
+ // Case 3: new OurClass().
+ if ($prevCode === T_NEW) {
+ return $this->isClassFromOurNamespace(phpcsFile: $phpcsFile, classNamePtr: $stackPtr);
+ }
+
+ return false;
+
+ }//end isInternalCall()
+
+
+ /**
+ * Check if the class at $classNamePtr is from the same app namespace as the current file.
+ *
+ * Compares the class's use-import path against the file's OCA\AppName\ prefix.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $classNamePtr Position of the class name token.
+ *
+ * @return bool True if the class is from our app namespace.
+ */
+ private function isClassFromOurNamespace(File $phpcsFile, int $classNamePtr): bool
+ {
+ $tokens = $phpcsFile->getTokens();
+ $className = $tokens[$classNamePtr]['content'];
+
+ // Find the file's namespace declaration.
+ $appPrefix = $this->getAppNamespacePrefix(phpcsFile: $phpcsFile);
+ if ($appPrefix === null) {
+ return false;
+ }
+
+ // Scan use-statements for an import matching this class name from our namespace.
+ for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
+ if ($tokens[$i]['code'] === T_USE) {
+ $usePath = $this->getUseStatementPath(phpcsFile: $phpcsFile, usePtr: $i);
+ if ($usePath === null) {
+ continue;
+ }
+
+ // Extract the imported short name (last segment).
+ $segments = explode(separator: '\\', string: $usePath);
+ $importedName = end($segments);
+
+ if ($importedName === $className
+ && str_starts_with(haystack: $usePath, needle: $appPrefix) === true
+ ) {
+ return true;
+ }
+ }//end if
+
+ // Stop scanning after the class declaration.
+ if (in_array($tokens[$i]['code'], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true) === true) {
+ break;
+ }
+ }//end for
+
+ return false;
+
+ }//end isClassFromOurNamespace()
+
+
+ /**
+ * Get the app namespace prefix (e.g., "OCA\MyDash") from the file's namespace declaration.
+ *
+ * @param File $phpcsFile The file being scanned.
+ *
+ * @return string|null The app prefix or null if not found.
+ */
+ private function getAppNamespacePrefix(File $phpcsFile): ?string
+ {
+ $tokens = $phpcsFile->getTokens();
+ for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
+ if ($tokens[$i]['code'] === T_NAMESPACE) {
+ $namespace = '';
+ $j = ($i + 1);
+ while ($j < $phpcsFile->numTokens && $tokens[$j]['code'] !== T_SEMICOLON) {
+ if ($tokens[$j]['code'] !== T_WHITESPACE) {
+ $namespace .= $tokens[$j]['content'];
+ }
+
+ $j++;
+ }
+
+ // Extract first two segments: OCA\AppName.
+ $parts = explode(separator: '\\', string: $namespace);
+ if (count($parts) >= 2) {
+ return $parts[0].'\\'.$parts[1];
+ }
+
+ return null;
+ }//end if
+ }//end for
+
+ return null;
+
+ }//end getAppNamespacePrefix()
+
+
+ /**
+ * Extract the full path from a use-statement starting at $usePtr.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $usePtr Position of the T_USE token.
+ *
+ * @return string|null The use path or null if parsing failed.
+ */
+ private function getUseStatementPath(File $phpcsFile, int $usePtr): ?string
+ {
+ $tokens = $phpcsFile->getTokens();
+ $usePath = '';
+ $j = ($usePtr + 1);
+ while ($j < $phpcsFile->numTokens) {
+ $code = $tokens[$j]['code'];
+ if ($code === T_SEMICOLON || $code === T_OPEN_CURLY_BRACKET) {
+ break;
+ }
+
+ // Stop at 'as' keyword (aliases) — use the path before it.
+ if ($code === T_AS) {
+ break;
+ }
+
+ if ($code !== T_WHITESPACE) {
+ $usePath .= $tokens[$j]['content'];
+ }
+
+ $j++;
+ }
+
+ $usePath = trim(string: $usePath);
+ return ($usePath !== '') ? $usePath : null;
+
+ }//end getUseStatementPath()
+
+
+ /**
+ * Check if the arguments between $openParen and $closeParen contain any unnamed arguments.
+ *
+ * An argument is "named" if its first significant token is followed by T_COLON.
+ * Handles nested parentheses, brackets, and braces correctly.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $openParen Position of the opening parenthesis.
+ * @param int $closeParen Position of the closing parenthesis.
+ *
+ * @return bool True if any argument is positional (unnamed).
+ */
+ private function hasUnnamedArguments(File $phpcsFile, int $openParen, int $closeParen): bool
+ {
+ $tokens = $phpcsFile->getTokens();
+ $parenDepth = 0;
+ $bracketDepth = 0;
+ $braceDepth = 0;
+ $atArgumentStart = true;
+
+ for ($i = ($openParen + 1); $i < $closeParen; $i++) {
+ $code = $tokens[$i]['code'];
+
+ // Track nesting depth.
+ if ($code === T_OPEN_PARENTHESIS) {
+ $parenDepth++;
+ } elseif ($code === T_CLOSE_PARENTHESIS) {
+ $parenDepth--;
+ } elseif ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
+ $bracketDepth++;
+ } elseif ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
+ $bracketDepth--;
+ } elseif ($code === T_OPEN_CURLY_BRACKET) {
+ $braceDepth++;
+ } elseif ($code === T_CLOSE_CURLY_BRACKET) {
+ $braceDepth--;
+ }
+
+ // Only examine tokens at the top level of the argument list.
+ if ($parenDepth > 0 || $bracketDepth > 0 || $braceDepth > 0) {
+ continue;
+ }
+
+ // Comma at top level → next argument starts.
+ if ($code === T_COMMA) {
+ $atArgumentStart = true;
+ continue;
+ }
+
+ // Skip whitespace and comments at argument start.
+ if ($atArgumentStart === true
+ && in_array($code, [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true) === true
+ ) {
+ continue;
+ }
+
+ if ($atArgumentStart === true) {
+ $atArgumentStart = false;
+
+ // Skip spread operator (...$args).
+ if ($code === T_ELLIPSIS) {
+ continue;
+ }
+
+ // Check if this argument is named: token followed by ':'.
+ $nextNonWs = $phpcsFile->findNext(
+ types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
+ start: ($i + 1),
+ end: $closeParen,
+ exclude: true
+ );
+
+ $isNamed = ($nextNonWs !== false && $tokens[$nextNonWs]['code'] === T_COLON);
+
+ if ($isNamed === false) {
+ return true;
+ }
+ }//end if
+ }//end for
+
+ return false;
+
+ }//end hasUnnamedArguments()
+
+
+}//end class
diff --git a/phpcs.xml b/phpcs.xml
index 7beb59d9..1aaf54c6 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -1,216 +1,216 @@
-
-
- Coding standard for Procest, based on the Conduction/OpenRegister standard.
-
- lib
-
-
- */vendor/*
- */node_modules/*
- composer-setup.php
-
-
-
-
-
-
-
-
-
- error
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
-
-
-
-
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- error
-
-
-
-
- error
-
-
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
- error
-
-
-
+
+
+ Coding standard for Procest, based on the Conduction/OpenRegister standard.
+
+ lib
+
+
+ */vendor/*
+ */node_modules/*
+ composer-setup.php
+
+
+
+
+
+
+
+
+
+ error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ error
+
+
+
+
+ error
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ error
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
index 1d8676de..d00a09d5 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -28,3 +28,5 @@ parameters:
- '#Caught class GuzzleHttp\\#'
# Dynamic HTTP status codes from business rule validation results
- '#Parameter \$statusCode of class OCP\\AppFramework\\Http\\JSONResponse constructor expects#'
+ # Controller::$request from OCP\AppFramework\Controller
+ - '#Access to an undefined property OCA\\Procest\\Controller\\SettingsController::\$request#'
diff --git a/phpunit.xml b/phpunit.xml
index fd087a69..8f1690cf 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,27 +1,27 @@
-
-
-
-
- tests/Unit
-
-
-
-
- lib/
-
-
-
-
-
-
+
+
+
+
+ tests/Unit
+
+
+
+
+ lib/
+
+
+
+
+
+
diff --git a/project.md b/project.md
index 4ce25be0..adcf96fc 100644
--- a/project.md
+++ b/project.md
@@ -1,120 +1,120 @@
-# Procest — Case Management for Nextcloud
-
-## Overview
-
-Procest is a lightweight case management (zaakgericht werken) app for Nextcloud, built as a thin client on top of OpenRegister. It manages cases, tasks, statuses, roles, results, and decisions — the internal processing side of case management. Customer-facing concerns (clients, communication, intake) are handled by the companion app Pipelinq.
-
-## Architecture
-
-- **Type**: Nextcloud App (PHP backend + Vue 2 frontend)
-- **Data layer**: OpenRegister (all data stored as register objects)
-- **Pattern**: Thin client — Procest provides UI/UX, OpenRegister handles persistence
-- **License**: AGPL-3.0-or-later
-
-See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed architecture and data model decisions.
-
-## Standards
-
-**Principle: international standards for data storage, Dutch standards as API mapping layer.**
-
-| Layer | Standard | Purpose |
-|-------|----------|---------|
-| **Primary** | CMMN 1.1 + Schema.org | International case management model |
-| **Semantic** | Schema.org JSON-LD | Linked data interoperability |
-| **API mapping** | ZGW APIs (Zaken, Besluiten, Catalogi) | Dutch government compatibility |
-| **Supplementary** | BPMN 2.0, DMN | Process and decision modeling |
-| **Nextcloud** | Deck, Calendar, Contacts | Native reuse |
-
-## Tech Stack
-
-| Layer | Technology |
-|-------|-----------|
-| Backend | PHP 8.1+, Nextcloud App Framework |
-| Frontend | Vue 2.7, Pinia, @nextcloud/vue |
-| Data | OpenRegister (JSON object storage) |
-| Build | Webpack 5, @nextcloud/webpack-vue-config |
-| i18n | English, Dutch |
-
-## Data Model
-
-| Object | Description | CMMN / Schema.org | ZGW Mapping |
-|--------|-------------|-------------------|-------------|
-| Case | Formal process with lifecycle | CasePlanModel / `Project` | Zaak |
-| Task | Work item within a case | HumanTask / `Action` | — |
-| Status | Lifecycle phase | Milestone / `ActionStatusType` | Status |
-| Role | Participant relationship | — / `Role` | Rol |
-| Result | Case outcome | Case outcome / `Action.result` | Resultaat |
-| Decision | Formal decision | — / `ChooseAction` | Besluit |
-
-## Features
-
-### Implemented (MVP)
-
-| Feature | Description | Status |
-|---------|-------------|--------|
-| Case Types | Configurable case types with status workflows | Done |
-| Case Management | Create, view, edit cases with status timeline, deadlines, activity | Done |
-| Task Management | BPMN lifecycle tasks (available/active/completed) within cases | Done |
-| Dashboard | KPI cards, status chart, overdue panel, activity feed, my work preview | Done |
-| Unified Search Deep Links | Cases and tasks appear in Nextcloud search with links to Procest detail views | Done |
-
-### Planned
-
-Features derived from zaakafhandelapp analysis and feature counsel.
-
-| Feature | Description | Priority | Source |
-|---------|-------------|----------|--------|
-| OpenRegister Integration | Store all data in OpenRegister instead of local state | MUST | Architecture |
-| Werkvooraad (Work Queue) | Dashboard section showing unassigned cases needing a handler | SHOULD | ZAA Dashboard |
-| Snelle Start Sidebar | Quick-start sidebar with tabs: work instructions, your cases, your tasks | SHOULD | ZAA Dashboard |
-| Roles & Decisions | Participant roles on cases, formal decisions (besluiten) | SHOULD | Spec (roles-decisions) |
-| Citizen Portal ("Mijn Zaken") | Public case status tracker for citizens (legally required under Wmebv) | MUST | Feature Counsel |
-| CSV/Excel Export | Export on all list views for reporting and compliance | MUST | Feature Counsel |
-| ZGW API Compatibility | Read-only Zaken, Catalogi, Besluiten API endpoints | MUST | Feature Counsel |
-| Bulk Operations | Bulk reassign, status change, delete on list views | SHOULD | Feature Counsel |
-| Pre-built Case Type Templates | Omgevingsvergunning, Subsidieaanvraag, Klacht | SHOULD | Feature Counsel |
-| Email/SMS Notifications | External notification channels for case status changes | SHOULD | Feature Counsel |
-
-### Shared with OpenRegister
-
-These features are implemented at the OpenRegister level, benefiting all consumer apps:
-
-| Feature | Description |
-|---------|-------------|
-| Nextcloud Unified Search | Search provider with deep link registry (apps register URL patterns per schema) |
-| Audit Trail | Comprehensive audit logging with export capability |
-| Business Rules Engine | Server-side validation, status transitions, event hooks |
-
-### Boundary with Pipelinq
-
-Procest focuses on **internal case processing** (what happens after intake). Pipelinq handles the **customer-facing/CRM side** (who the case is about, communication with them).
-
-| Concern | Procest | Pipelinq |
-|---------|---------|----------|
-| Cases (Zaken) | Owns | Links to (as context for requests) |
-| Tasks (Taken) | Owns | — |
-| Roles (Rollen) | Owns | — |
-| Clients (Klanten) | References | Owns |
-| Contact Moments | — | Owns |
-| Messages (Berichten) | — | Owns |
-| Employees (Medewerkers) | Via Nextcloud Users | Via Nextcloud Users |
-| Search | Via OpenRegister unified search | Via OpenRegister unified search |
-
-## Key Directories
-
-```
-procest/
-├── appinfo/ # App manifest and routes
-├── lib/ # PHP backend (controllers, services, repair)
-├── src/ # Vue frontend source
-├── docs/ # Architecture and documentation
-├── openspec/ # OpenSpec specs and changes
-├── l10n/ # Translations
-└── templates/ # PHP templates
-```
-
-## Development
-
-- **Local URL**: http://localhost:8080/apps/procest/
-- **Requires**: OpenRegister app installed and enabled
-- **Docker**: Part of openregister/docker-compose.yml
+# Procest — Case Management for Nextcloud
+
+## Overview
+
+Procest is a lightweight case management (zaakgericht werken) app for Nextcloud, built as a thin client on top of OpenRegister. It manages cases, tasks, statuses, roles, results, and decisions — the internal processing side of case management. Customer-facing concerns (clients, communication, intake) are handled by the companion app Pipelinq.
+
+## Architecture
+
+- **Type**: Nextcloud App (PHP backend + Vue 2 frontend)
+- **Data layer**: OpenRegister (all data stored as register objects)
+- **Pattern**: Thin client — Procest provides UI/UX, OpenRegister handles persistence
+- **License**: AGPL-3.0-or-later
+
+See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed architecture and data model decisions.
+
+## Standards
+
+**Principle: international standards for data storage, Dutch standards as API mapping layer.**
+
+| Layer | Standard | Purpose |
+|-------|----------|---------|
+| **Primary** | CMMN 1.1 + Schema.org | International case management model |
+| **Semantic** | Schema.org JSON-LD | Linked data interoperability |
+| **API mapping** | ZGW APIs (Zaken, Besluiten, Catalogi) | Dutch government compatibility |
+| **Supplementary** | BPMN 2.0, DMN | Process and decision modeling |
+| **Nextcloud** | Deck, Calendar, Contacts | Native reuse |
+
+## Tech Stack
+
+| Layer | Technology |
+|-------|-----------|
+| Backend | PHP 8.1+, Nextcloud App Framework |
+| Frontend | Vue 2.7, Pinia, @nextcloud/vue |
+| Data | OpenRegister (JSON object storage) |
+| Build | Webpack 5, @nextcloud/webpack-vue-config |
+| i18n | English, Dutch |
+
+## Data Model
+
+| Object | Description | CMMN / Schema.org | ZGW Mapping |
+|--------|-------------|-------------------|-------------|
+| Case | Formal process with lifecycle | CasePlanModel / `Project` | Zaak |
+| Task | Work item within a case | HumanTask / `Action` | — |
+| Status | Lifecycle phase | Milestone / `ActionStatusType` | Status |
+| Role | Participant relationship | — / `Role` | Rol |
+| Result | Case outcome | Case outcome / `Action.result` | Resultaat |
+| Decision | Formal decision | — / `ChooseAction` | Besluit |
+
+## Features
+
+### Implemented (MVP)
+
+| Feature | Description | Status |
+|---------|-------------|--------|
+| Case Types | Configurable case types with status workflows | Done |
+| Case Management | Create, view, edit cases with status timeline, deadlines, activity | Done |
+| Task Management | BPMN lifecycle tasks (available/active/completed) within cases | Done |
+| Dashboard | KPI cards, status chart, overdue panel, activity feed, my work preview | Done |
+| Unified Search Deep Links | Cases and tasks appear in Nextcloud search with links to Procest detail views | Done |
+
+### Planned
+
+Features derived from zaakafhandelapp analysis and feature counsel.
+
+| Feature | Description | Priority | Source |
+|---------|-------------|----------|--------|
+| OpenRegister Integration | Store all data in OpenRegister instead of local state | MUST | Architecture |
+| Werkvooraad (Work Queue) | Dashboard section showing unassigned cases needing a handler | SHOULD | ZAA Dashboard |
+| Snelle Start Sidebar | Quick-start sidebar with tabs: work instructions, your cases, your tasks | SHOULD | ZAA Dashboard |
+| Roles & Decisions | Participant roles on cases, formal decisions (besluiten) | SHOULD | Spec (roles-decisions) |
+| Citizen Portal ("Mijn Zaken") | Public case status tracker for citizens (legally required under Wmebv) | MUST | Feature Counsel |
+| CSV/Excel Export | Export on all list views for reporting and compliance | MUST | Feature Counsel |
+| ZGW API Compatibility | Read-only Zaken, Catalogi, Besluiten API endpoints | MUST | Feature Counsel |
+| Bulk Operations | Bulk reassign, status change, delete on list views | SHOULD | Feature Counsel |
+| Pre-built Case Type Templates | Omgevingsvergunning, Subsidieaanvraag, Klacht | SHOULD | Feature Counsel |
+| Email/SMS Notifications | External notification channels for case status changes | SHOULD | Feature Counsel |
+
+### Shared with OpenRegister
+
+These features are implemented at the OpenRegister level, benefiting all consumer apps:
+
+| Feature | Description |
+|---------|-------------|
+| Nextcloud Unified Search | Search provider with deep link registry (apps register URL patterns per schema) |
+| Audit Trail | Comprehensive audit logging with export capability |
+| Business Rules Engine | Server-side validation, status transitions, event hooks |
+
+### Boundary with Pipelinq
+
+Procest focuses on **internal case processing** (what happens after intake). Pipelinq handles the **customer-facing/CRM side** (who the case is about, communication with them).
+
+| Concern | Procest | Pipelinq |
+|---------|---------|----------|
+| Cases (Zaken) | Owns | Links to (as context for requests) |
+| Tasks (Taken) | Owns | — |
+| Roles (Rollen) | Owns | — |
+| Clients (Klanten) | References | Owns |
+| Contact Moments | — | Owns |
+| Messages (Berichten) | — | Owns |
+| Employees (Medewerkers) | Via Nextcloud Users | Via Nextcloud Users |
+| Search | Via OpenRegister unified search | Via OpenRegister unified search |
+
+## Key Directories
+
+```
+procest/
+├── appinfo/ # App manifest and routes
+├── lib/ # PHP backend (controllers, services, repair)
+├── src/ # Vue frontend source
+├── docs/ # Architecture and documentation
+├── openspec/ # OpenSpec specs and changes
+├── l10n/ # Translations
+└── templates/ # PHP templates
+```
+
+## Development
+
+- **Local URL**: http://localhost:8080/apps/procest/
+- **Requires**: OpenRegister app installed and enabled
+- **Docker**: Part of openregister/docker-compose.yml
diff --git a/src/App.vue b/src/App.vue
index 314da369..5319818e 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -6,7 +6,10 @@
:name="t('procest', 'OpenRegister is required')"
:description="t('procest', 'Procest needs the OpenRegister app to store and manage data. Please install OpenRegister from the app store to get started.')">
-
+ h(App),
-})
+loadTranslations('procest', () => {
+ // Create Vue instance to activate Pinia context, then initialize stores.
+ const app = new Vue({
+ pinia,
+ router,
+ render: h => h(App),
+ })
-// Mount immediately so the App renders (NC32 needs #content to be taken over).
-app.$mount('#content')
+ // Mount immediately so the App renders (NC32 needs #content to be taken over).
+ app.$mount('#content')
-// Initialize stores in parallel — the useListView retry logic will wait
-// for registerObjectType to complete.
-initializeStores()
+ // Initialize stores in parallel — the useListView retry logic will wait
+ // for registerObjectType to complete.
+ initializeStores()
+})
diff --git a/src/navigation/MainMenu.vue b/src/navigation/MainMenu.vue
index 56f6cab6..bb70d989 100644
--- a/src/navigation/MainMenu.vue
+++ b/src/navigation/MainMenu.vue
@@ -39,49 +39,37 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+ {{ t('procest', 'My Work') }}
+ ({{ totalCount }})
+
+
+
+
+
` | Rendered as literal text, no execution |
+| Zaken `#/cases` — Add dialog | Omschrijving | `` | Rendered as literal text, no execution |
+| Admin Settings | Register | `` | Accepted by API; JSON-encoded in response (`\u003Cscript\u003E`); not rendered in form (settings 404 bug prevents reload) |
+
+Vue's reactive data binding handles input values as data, not HTML — `v-model` and `{{ }}` escape content by default. No `alert()` fired. No `` and ``) visible as plain text in the New Case form, form-level validation error shown
+- `security-new-case-xss-display.png` — Same form after attempted submission, showing "Zaaktype is verplicht" validation, XSS payload still rendered as literal text
+- `security-admin.png` — Admin settings page (`/index.php/settings/admin/procest`) showing configuration fields, ZGW mapping table, and case type management
+
+### Prior Run (2026-03-12)
+- `security-xss-cases-form.png` — XSS payloads rendered as plain text in Zaken Add dialog
+- `security-tasks-xss-empty-form.png` — Tasks Create dialog (empty form)
+- `security-admin-settings-full.png` — Admin settings page full view
+- `security-login.png`, `security-xss-input.png`, `security-tasks-dialog.png`, `security-admin-settings.png`, `security-network-requests.png`
diff --git a/test-results/ux-results.md b/test-results/ux-results.md
new file mode 100644
index 00000000..f0818b53
--- /dev/null
+++ b/test-results/ux-results.md
@@ -0,0 +1,302 @@
+# procest — UX Test Results
+
+**Date:** 2026-03-13
+**Perspective:** UX
+**Environment:** http://nextcloud.local
+**Browser:** browser-3 (headless)
+**Login:** admin
+
+> Experimental agentic testing — results should be verified manually for critical findings.
+
+## Summary
+
+| Status | Count |
+|--------|-------|
+| PASS | 9 |
+| PARTIAL | 7 |
+| FAIL | 8 |
+| CANNOT_TEST | 2 |
+
+---
+
+## Results by Feature
+
+### Dashboard
+
+#### Empty State / Blank Screen
+- **Status**: FAIL
+- **Tested**: Dashboard renders a completely blank white content area when app is unconfigured
+- **Screenshot**: ux-dashboard.png
+- **Console errors**: 8 errors — `Failed to fetch settings: Not Found (404)`, `Object type "case" is not registered`, `Object type "caseType" is not registered`, `Object type "statusType" is not registered`, `Object type "task" is not registered`
+- **Notes**: The dashboard main content area is blank white — no empty state message, no loading indicator, no error feedback to the user. There is no onboarding prompt or guidance directing the user to admin settings. A user encountering this for the first time has no idea what to do. This is a critical UX failure for unconfigured deployments.
+
+#### KPI Cards / Quick Actions
+- **Status**: CANNOT_TEST
+- **Tested**: Could not be tested — app is unconfigured and all data fetching fails silently with no visible feedback
+- **Screenshot**: ux-dashboard.png
+- **Notes**: No KPI cards or quick actions rendered at all. Could not assess labels or layout.
+
+#### Navigation Sidebar
+- **Status**: PASS
+- **Tested**: Sidebar items (Dashboard, Mijn werk, Zaken, Taken, Documentatie, Instellingen) are all present and visible
+- **Screenshot**: ux-dashboard.png
+- **Console errors**: None for sidebar
+- **Notes**: Navigation labels are clear Dutch. Icons accompany all items. "Instellingen" is visually separated at the bottom. Sidebar collapse toggle is present.
+
+---
+
+### Case List View (Zaken — `#/cases`)
+
+#### Empty State
+- **Status**: PARTIAL
+- **Tested**: Navigated to `#/cases` — shows "No items found" with a search/empty icon
+- **Screenshot**: ux-case-list.png
+- **Console errors**: Settings 404, caseType/statusType not registered
+- **Notes**: Empty state has a visual icon and text but: (1) the text is in English ("No items found") instead of Dutch, (2) it does not distinguish between "no data exists" and "API call failed". A more helpful message would be "Geen zaken gevonden" with guidance to create the first case, or "Zaken konden niet worden geladen" with an explanation when an API error occurs.
+
+#### View Toggle (Cards/Table)
+- **Status**: PASS
+- **Tested**: Cards/Table toggle visible in toolbar, Table selected by default
+- **Screenshot**: ux-case-list.png
+- **Notes**: Toggle is clearly positioned. Labels "Cards" / "Table" are English but visually understandable.
+
+#### Create Action
+- **Status**: PASS
+- **Tested**: "+ Add Item" button is present and prominent in the toolbar; clicking it opens the new case modal
+- **Screenshot**: ux-case-list.png, ux-new-case-form.png
+- **Notes**: Button is discoverable. However the label "Add Item" is generic English — contextual Dutch label "Nieuwe zaak" would be clearer.
+
+#### Search / Filter Controls
+- **Status**: FAIL
+- **Tested**: No search bar or filter controls visible on the case list
+- **Screenshot**: ux-case-list.png
+- **Notes**: The case list has no visible search or filter controls. An "Actions" menu button exists but testing its contents was not possible. In a case management system this is a significant missing UX element.
+
+---
+
+### New Case Form (Nieuwe zaak modal)
+
+#### Modal Quality
+- **Status**: PASS
+- **Tested**: Modal opens with title "Nieuwe zaak", has close button (✕), Cancel and Submit buttons
+- **Screenshot**: ux-new-case-form.png
+- **Console errors**: caseType collection error (empty dropdown)
+- **Notes**: Modal has "Annuleren" (Cancel) and "Zaak aanmaken" (Create case) — descriptive and Dutch. Background dims. Modal is well-proportioned. Close button uses raw "✕" character rather than a proper icon with aria-label — minor accessibility concern.
+
+#### Required Field Indicators
+- **Status**: PASS
+- **Tested**: "Zaaktype *" and "Titel *" are marked with asterisks. "Omschrijving" has placeholder "Optionele omschrijving..."
+- **Screenshot**: ux-new-case-form.png
+- **Notes**: Required field indicators are clear and consistent.
+
+#### Zaaktype Selector (empty)
+- **Status**: PARTIAL
+- **Tested**: Combobox shows "Selecteer een zaaktype..." but lists no options because caseTypes cannot be fetched
+- **Screenshot**: ux-new-case-form.png
+- **Console errors**: `Object type "caseType" is not registered`
+- **Notes**: The dropdown is empty with no explanation to the user. A message such as "Geen zaaktypen beschikbaar — configureer de app in beheerdersinstellingen" would help users understand why they cannot proceed.
+
+#### Form Validation
+- **Status**: PARTIAL
+- **Tested**: Submitted empty form — validation messages appear: "Zaaktype is verplicht" and "Titel is verplicht"
+- **Screenshot**: ux-new-case-validation.png
+- **Console errors**: None
+- **Notes**: Validation messages are in Dutch and appear inline next to each field — good pattern. However, the error text appears in a very low-contrast light pink/red color against a white background — potential WCAG AA compliance failure. The "Titel" field shows a red border and error icon clearly, but the "Zaaktype" error text below the dropdown is pale pink and barely readable.
+
+---
+
+### Case Detail View (`#/cases/:id`)
+
+#### Page Header / Case Identity
+- **Status**: PARTIAL
+- **Tested**: Navigated to `#/cases/test-case-id` — page renders with header "Zaak" (no case name)
+- **Screenshot**: ux-case-detail.png
+- **Console errors**: role/roleType/task/result collections — all "not registered"
+- **Notes**: The page header shows only the generic word "Zaak" with no case title, ID, or identifier. A non-existent case ID silently renders an empty, editable form rather than a "not found" error. The user could believe they are editing a real case when they are on a ghost/empty record.
+
+#### Status Bar
+- **Status**: FAIL
+- **Tested**: A grey bar appears below the header showing only "—" (dash)
+- **Screenshot**: ux-case-detail.png
+- **Console errors**: statusType not registered
+- **Notes**: The status progression bar renders as a plain grey band with a single "—" marker. No status labels, no timeline, no current status indicator. No user-facing explanation of why this area is empty. The status panel is a key navigational element in case management and should either show content or a clear "not configured" message.
+
+#### Case Info Section
+- **Status**: PARTIAL
+- **Tested**: "Zaakinformatie" section is visible with Titel, Omschrijving, Zaaktype, Identificatie, Prioriteit, Vertrouwelijkheid, Behandelaar, Startdatum fields
+- **Screenshot**: ux-case-detail.png
+- **Notes**: Section heading is clear Dutch. Fields are well-labelled. Read-only fields (Zaaktype, Identificatie, Vertrouwelijkheid, Startdatum) show "—" instead of a value, which is acceptable for an unconfigured system. The "Behandelaar" field has a helpful placeholder "Behandelaar toewijzen...".
+
+#### Delete Button Styling
+- **Status**: FAIL
+- **Tested**: "Verwijderen" button renders in light pink/salmon color next to "Opslaan"
+- **Screenshot**: ux-case-detail.png
+- **Notes**: The delete button uses a light pink background that is visually too similar to normal secondary action styling. It should use a clearly destructive red style, or be separated from the "Opslaan" button (e.g. moved to a danger zone section), to prevent accidental deletion.
+
+#### Panels (Participants, Tasks, Activity)
+- **Status**: PARTIAL
+- **Tested**: "Deelnemers (0)", "Taken (0/0)", "Activiteit" panels visible with empty states
+- **Screenshot**: ux-case-detail.png
+- **Notes**: Empty states within panels are present: "Geen deelnemers toegewezen", "Nog geen taken", "Nog geen activiteit" — these are correctly in Dutch. Action buttons "Deelnemer toevoegen" and "Behandelaar toewijzen" both appear in the Deelnemers section — the relationship between the two is unclear (is a Behandelaar a type of Deelnemer?). No deadline or processing deadline panel is visible on this view.
+
+#### Back Navigation
+- **Status**: PASS
+- **Tested**: "Terug naar lijst" button is present top-left
+- **Screenshot**: ux-case-detail.png
+- **Notes**: Clear navigation affordance back to the cases list. No dead ends.
+
+---
+
+### Task List View (Taken — `#/tasks`)
+
+#### Empty State
+- **Status**: PARTIAL
+- **Tested**: Shows "No items found" with icon
+- **Screenshot**: ux-task-list.png
+- **Notes**: Same issues as case list: English "No items found", no contextual guidance for tasks specifically.
+
+#### Page Title
+- **Status**: FAIL
+- **Tested**: No visible page heading on the task list view
+- **Screenshot**: ux-task-list.png
+- **Notes**: The task list has no page heading to orient the user, unlike "Mijn werk" which has a prominent "Mijn werk (0)" heading. The toolbar floats at the top with no title.
+
+---
+
+### My Work View (Mijn werk — `#/my-work`)
+
+#### Empty State
+- **Status**: PASS
+- **Tested**: Shows "Geen items aan u toegewezen" with illustrative icon and explanatory subtitle
+- **Screenshot**: ux-my-work.png
+- **Console errors**: Case object type not registered (warning), handled gracefully
+- **Notes**: This is the best empty state in the app — contextual icon, clear Dutch heading, explanatory subtitle "Zaken en taken die aan u zijn toegewezen verschijnen hier". This pattern should be replicated across all list views.
+
+#### Filter Tabs
+- **Status**: PASS
+- **Tested**: "Alles (0)", "Zaken (0)", "Taken (0)" tabs and "Toon voltooide" checkbox all visible
+- **Screenshot**: ux-my-work.png
+- **Notes**: Tab labels include item counts (helpful). "Toon voltooide" checkbox is clearly labelled. Layout is clean.
+
+#### Page Heading
+- **Status**: PASS
+- **Tested**: "Mijn werk (0)" heading is prominent and includes count
+- **Screenshot**: ux-my-work.png
+- **Notes**: Clear, well-positioned heading. The parenthetical count is helpful UX.
+
+#### Temporal Grouping (Overdue / This Week / Upcoming)
+- **Status**: CANNOT_TEST
+- **Tested**: No work items exist, so no temporal groups are shown
+- **Notes**: Could not assess overdue highlighting, temporal section headers, or visual distinction between urgency levels.
+
+---
+
+### Admin Settings (`/index.php/settings/admin/procest`)
+
+#### Configuration Section
+- **Status**: PARTIAL
+- **Tested**: Page loads with Register + 8 schema fields, all empty. External documentation link (?) present.
+- **Screenshot**: ux-admin-settings.png
+- **Console errors**: Settings 404, ZGW mappings 404
+- **Notes**: Field labels are clear Dutch names. "Opslaan" button is prominent. However all fields use the field label as the placeholder (e.g. placeholder="Register") — this is redundant and provides no guidance on what value to enter. The documentation link target (procest.app) may not exist. No error message shown when settings fail to load (fields just appear blank without explanation).
+
+#### Zaaktypebeheer Section — Empty State
+- **Status**: PASS
+- **Tested**: Section renders with "No items found" empty state and "Add Item" / "Actions" toolbar
+- **Screenshot**: ux-admin-settings.png
+- **Notes**: Structure is clear. "Add Item" correctly opens the new case type inline form.
+
+#### Case Type Create Form
+- **Status**: PASS
+- **Tested**: "Add Item" opens inline "Nieuw zaaktype" form with "Algemeen" and "Statussen" tabs
+- **Screenshot**: ux-case-type-detail.png
+- **Console errors**: None for form rendering
+- **Notes**: Form has "Terug naar lijst" back button, clear heading "Nieuw zaaktype", and "Opslaan" button. Two-tab interface is intuitive. "Verwerkingsdeadline" field has a helpful example placeholder "bijv. P56D (56 dagen)" — good UX pattern.
+
+#### Case Type Form Validation
+- **Status**: PASS
+- **Tested**: Submitting empty form shows "Los de validatiefouten op" summary + per-field inline errors
+- **Screenshot**: ux-case-type-validation.png
+- **Notes**: Validation summary message at top + red borders + inline error messages per field is the best validation pattern in the app. All messages are in Dutch.
+
+#### ZGW API Mapping Section
+- **Status**: PARTIAL
+- **Tested**: Table with 11 ZGW resource rows, all "Not configured", with "Bewerken" and "Reset" actions
+- **Screenshot**: ux-admin-settings.png
+- **Console errors**: ZGW mappings API 404
+- **Notes**: Table structure is readable. However column headers ("ZGW Resource", "Status", "Actions") and cell values ("Not configured") are in English while the rest of the page is Dutch — a language inconsistency. "Reset" button label is also English.
+
+---
+
+### Navigation and General UX
+
+#### "Documentatie" Dead Link
+- **Status**: FAIL
+- **Tested**: Sidebar "Documentatie" item links to "#" — clicking takes user nowhere useful
+- **Screenshot**: ux-dashboard.png
+- **Notes**: The Documentatie nav item is a placeholder/dead link. Clicking it does not navigate anywhere. Either a documentation page should be implemented, or this item should be hidden/disabled with a "Binnenkort beschikbaar" indicator until it is ready.
+
+#### Language Consistency (Dutch/English mix)
+- **Status**: FAIL
+- **Tested**: Multiple pages inspected
+- **Notes**: Significant mixing of Dutch and English throughout the app — unacceptable for a Dutch government-oriented app:
+ - "No items found" empty states (all list views) — should be Dutch
+ - "Add Item", "Actions" toolbar buttons — should be Dutch
+ - "Cards" / "Table" view toggle labels — should be Dutch
+ - ZGW API Mapping section heading, column headers, "Not configured", "Reset" — should be Dutch
+ - Admin sidebar group headings "Personal" / "Administration" — Nextcloud core (out of app scope)
+
+#### API Error Feedback to Users
+- **Status**: FAIL
+- **Tested**: Settings API 404 causes dashboard to be completely blank with no user-facing error
+- **Screenshot**: ux-dashboard.png
+- **Console errors**: 8 errors visible only in browser console
+- **Notes**: When the settings API fails (404), the app silently fails — users see a blank screen with no error message, no retry option, and no guidance. This is a critical UX failure. Errors are only visible in the browser developer console. A clear error banner or empty state with a link to admin settings should be shown.
+
+#### Loading Indicators
+- **Status**: PARTIAL
+- **Tested**: In-app settings page (`#/settings`) shows "Loading..." spinners during data fetching
+- **Screenshot**: ux-settings-in-app.png
+- **Notes**: The in-app settings view does show "Loading..." spinners while fetching data — good. However these spinners persist indefinitely when API calls fail (no timeout, no error transition). On other pages (dashboard, case list) there are no loading indicators at all — data failures are silent.
+
+---
+
+## UX Issues Summary
+
+| # | Issue | Severity | Page(s) |
+|---|-------|----------|---------|
+| 1 | Dashboard is completely blank when unconfigured — no empty state, error, or onboarding guidance | HIGH | Dashboard |
+| 2 | All API errors are silent — no user-facing feedback when calls fail, only browser console | HIGH | All pages |
+| 3 | "Documentatie" sidebar link is a dead placeholder link ("#") | HIGH | All pages (sidebar) |
+| 4 | Mixed Dutch/English language — empty states, toolbar buttons, ZGW section all in English | HIGH | All pages |
+| 5 | Case detail view silently renders an empty editable form for non-existent IDs — no "not found" error | HIGH | Case detail |
+| 6 | Validation error text contrast is too low (light pink on white) — WCAG AA risk | MEDIUM | New case modal |
+| 7 | "Verwijderen" (Delete) button styling insufficiently distinct from normal buttons — accidental deletion risk | MEDIUM | Case detail |
+| 8 | Status bar in case detail shows only "—" with no explanation when status types are unavailable | MEDIUM | Case detail |
+| 9 | Admin settings config fields use field label as placeholder — no value format guidance | MEDIUM | Admin settings |
+| 10 | Zaaktype dropdown has no options and no explanation when unconfigured | MEDIUM | New case modal |
+| 11 | "Deelnemer toevoegen" vs "Behandelaar toewijzen" dual buttons on case detail — relationship unclear | MEDIUM | Case detail |
+| 12 | Loading spinners persist indefinitely when API fails — no timeout or error transition | LOW | In-app settings |
+| 13 | Case list and task list views have no page heading — disorienting vs "Mijn werk" which does | LOW | Zaken, Taken |
+| 14 | "Add Item" button label is generic English — should use contextual Dutch labels | LOW | Case list, Task list, Admin |
+| 15 | Case type form has 8+ required fields — onboarding may feel very heavy for new admins | LOW | Admin settings |
+
+---
+
+## Console Errors Summary
+
+- **Pages checked**: 7 (Dashboard, Case list, Task list, My Work, Case detail, Admin settings, In-app settings)
+- **Pages with errors**: 6 (all except My Work — had 1 warning only)
+- **Unique errors**:
+ 1. `Failed to fetch settings: Not Found` — `/apps/procest/api/settings` returns 404 on every page load (root cause of most failures)
+ 2. `Object type "case" is not registered in the store` — requires settings to be configured first
+ 3. `Object type "caseType" is not registered in the store`
+ 4. `Object type "statusType" is not registered in the store`
+ 5. `Object type "task" is not registered in the store`
+ 6. `Object type "role" is not registered in the store`
+ 7. `Object type "roleType" is not registered in the store`
+ 8. `Object type "result" is not registered in the store`
+ 9. `Error fetching ZGW mappings` — `/apps/procest/api/zgw-mappings` returns 404
+ 10. `Refused to apply style from profiler/css/profiler-toolbar.css` (MIME type mismatch — dev profiler app issue, not procest-specific)
+
+**Root cause assessment**: The app is not configured — no OpenRegister register or schema IDs have been set in admin settings. The 404 on `/apps/procest/api/settings` suggests the PHP backend route may be missing or broken. All downstream object type registrations depend on this settings fetch succeeding, so all data operations fail across the entire app. This is the single highest-priority issue to resolve.
diff --git a/tests/unit/Controller/SettingsControllerTest.php b/tests/unit/Controller/SettingsControllerTest.php
new file mode 100644
index 00000000..b74a655d
--- /dev/null
+++ b/tests/unit/Controller/SettingsControllerTest.php
@@ -0,0 +1,120 @@
+
+ * @copyright 2024 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @version GIT:
+ *
+ * @link https://procest.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Procest\Tests\Unit\Controller;
+
+use OCA\Procest\Controller\SettingsController;
+use OCA\Procest\Service\SettingsService;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests for SettingsController.
+ */
+class SettingsControllerTest extends TestCase
+{
+
+ /**
+ * The controller under test.
+ *
+ * @var SettingsController
+ */
+ private SettingsController $controller;
+
+ /**
+ * Mock IRequest.
+ *
+ * @var IRequest&MockObject
+ */
+ private IRequest&MockObject $request;
+
+ /**
+ * Mock SettingsService.
+ *
+ * @var SettingsService&MockObject
+ */
+ private SettingsService&MockObject $settingsService;
+
+ /**
+ * Set up test fixtures.
+ *
+ * @return void
+ */
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->settingsService = $this->createMock(SettingsService::class);
+
+ $this->controller = new SettingsController(
+ request: $this->request,
+ settingsService: $this->settingsService,
+ );
+
+ }//end setUp()
+
+ /**
+ * Test that index() returns a JSONResponse with success and config keys.
+ *
+ * @return void
+ */
+ public function testIndexReturnsJsonResponseWithExpectedKeys(): void
+ {
+ $this->settingsService->expects($this->once())
+ ->method('getSettings')
+ ->willReturn(['openRegisterUrl' => 'http://localhost']);
+
+ $result = $this->controller->index();
+
+ self::assertInstanceOf(JSONResponse::class, $result);
+ self::assertTrue($result->getData()['success']);
+ self::assertArrayHasKey('config', $result->getData());
+
+ }//end testIndexReturnsJsonResponseWithExpectedKeys()
+
+ /**
+ * Test that create() calls updateSettings with request params and returns success.
+ *
+ * @return void
+ */
+ public function testCreateCallsUpdateSettingsAndReturnsSuccess(): void
+ {
+ $params = ['openRegisterUrl' => 'http://new-url'];
+
+ $this->request->expects($this->once())
+ ->method('getParams')
+ ->willReturn($params);
+
+ $this->settingsService->expects($this->once())
+ ->method('updateSettings')
+ ->with($params)
+ ->willReturn($params);
+
+ $result = $this->controller->create();
+
+ self::assertInstanceOf(JSONResponse::class, $result);
+ self::assertTrue($result->getData()['success']);
+ self::assertArrayHasKey('config', $result->getData());
+
+ }//end testCreateCallsUpdateSettingsAndReturnsSuccess()
+
+}//end class