Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/Repair/RenameCollidingSchemaSlugs.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class RenameCollidingSchemaSlugs implements IRepairStep {
*/
private const RENAMES = [
'supplierInvoice' => ['to' => 'caseSupplierInvoice', 'with' => 'shillinq'],
// The case task. `task` was claimed by three apps and they share only
// `description`, `priority` and `status`. planninq's project task is the
// largest and keeps the bare slug; pipelinq took crmTask.
'task' => ['to' => 'caseTask', 'with' => 'planninq, pipelinq'],
];

/**
Expand Down
4 changes: 2 additions & 2 deletions lib/Service/DemoCaseloadGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function __construct(
/**
* The register and schema ids the demo caseload reads and writes.
*
* @return array{register: string, case: string, task: string, caseType: string, statusType: string} The ids.
* @return array{register: string, case: string, caseTask: string, caseType: string, statusType: string} The ids.
*
* @throws RuntimeException When the app is not configured against a register yet.
*
Expand All @@ -75,7 +75,7 @@ public function schemaIds(): array {
$ids = [
'register' => $this->config(key: 'register'),
'case' => $this->config(key: 'case_schema'),
'task' => $this->config(key: 'task_schema'),
'caseTask' => $this->config(key: 'task_schema'),
'caseType' => $this->config(key: 'case_type_schema'),
'statusType' => $this->config(key: 'status_type_schema'),
];
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/DemoCaseloadReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public function buckets(?DateTimeImmutable $now = null): array {
$tasks = $this->gateway->findMany(
objectService: $objectService,
registerId: $ids['register'],
schemaId: $ids['task'],
schemaId: $ids['caseTask'],
filters: []
);

Expand Down
2 changes: 1 addition & 1 deletion lib/Service/DemoCaseloadSeedDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ private function createTasks(
$id = $this->gateway->create(
objectService: $objectService,
registerId: $ids['register'],
schemaId: $ids['task'],
schemaId: $ids['caseTask'],
data: $this->taskPayload(taskSeed: $taskSeed, caseId: $caseId, now: $now)
);

Expand Down
6 changes: 3 additions & 3 deletions lib/Service/KpiAggregationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ private function countCases(array $ids, array $filters): int {
* @return int The count.
*/
private function countTasks(array $ids, array $filters): int {
return $this->countObjects(ids: $ids, schema: $ids['task'], filters: $filters);
return $this->countObjects(ids: $ids, schema: $ids['caseTask'], filters: $filters);
}//end countTasks()

/**
Expand Down Expand Up @@ -450,13 +450,13 @@ private function toArray(mixed $object): array {
/**
* The register and schema ids these metrics read.
*
* @return array{register: string, case: string, task: string}|null The ids, or null when unconfigured.
* @return array{register: string, case: string, caseTask: string}|null The ids, or null when unconfigured.
*/
private function ids(): ?array {
$ids = [
'register' => $this->appConfig->getValueString(Application::APP_ID, 'register', ''),
'case' => $this->appConfig->getValueString(Application::APP_ID, 'case_schema', ''),
'task' => $this->appConfig->getValueString(Application::APP_ID, 'task_schema', ''),
'caseTask' => $this->appConfig->getValueString(Application::APP_ID, 'task_schema', ''),
];

if (in_array('', $ids, true) === true) {
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/Settings/SchemaSlugMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class SchemaSlugMap {
public const SLUG_TO_CONFIG_KEY = [
'catalog' => 'catalogus_schema',
'case' => 'case_schema',
'task' => 'task_schema',
'caseTask' => 'task_schema',
'status' => 'status_schema',
'statusRecord' => 'status_record_schema',
'role' => 'role_schema',
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/Settings/SchemaSlugResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public function __construct(
* and dropping the fallback would blank all three.
*
* @param object $schemaMapper The OpenRegister SchemaMapper.
* @param string $slug The schema slug, e.g. 'task'.
* @param string $slug The schema slug, e.g. 'caseTask'.
*
* @return object|null The live schema, or null when the slug does not resolve.
*
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/Support/JsonEncodedStringProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class JsonEncodedStringProperties {
'notificationChannel' => ['filters'],
'objection' => ['attachments'],
'resultType' => ['sourceDateArchiveProcedure'],
'task' => ['checklist'],
'caseTask' => ['checklist'],
'workflowTemplate' => ['nodePositions', 'steps', 'transitions'],
];

Expand Down
10 changes: 5 additions & 5 deletions lib/Settings/dossiq_mock_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -13112,8 +13112,8 @@
}
}
},
"task": {
"slug": "task",
"caseTask": {
"slug": "caseTask",
"x-openregister-notifications": {
"taskAssigned": {
"trigger": {
Expand Down Expand Up @@ -22676,7 +22676,7 @@
{
"@self": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"slug": "task-voorbeeld-title-1-1",
"id": "fab148e2-e14d-45ca-a0c2-58073d5e59b3"
},
Expand All @@ -22695,7 +22695,7 @@
{
"@self": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"slug": "task-voorbeeld-title-2-2",
"id": "10d3503b-1077-45db-8516-ae4719f1a40f"
},
Expand All @@ -22714,7 +22714,7 @@
{
"@self": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"slug": "task-voorbeeld-title-3-3",
"id": "0d4e9a65-535b-4ef8-bd93-453713b8fa78"
},
Expand Down
6 changes: 3 additions & 3 deletions lib/Settings/dossiq_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -1848,8 +1848,8 @@
},
"searchable": true
},
"task": {
"slug": "task",
"caseTask": {
"slug": "caseTask",
"x-openregister-notifications": {
"taskAssigned": {
"trigger": {
Expand Down Expand Up @@ -9230,7 +9230,7 @@
"documentType",
"decisionType",
"case",
"task",
"caseTask",
"role",
"result",
"statusRecord",
Expand Down
35 changes: 35 additions & 0 deletions openspec/changes/namespace-the-case-task/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Namespace the case task

## Why

`task` was claimed by three apps: planninq, pipelinq and this one. They share
`description`, `priority` and `status` — what any task-shaped record carries,
and nothing that identifies the record.

planninq's project task is the largest and keeps the bare slug. pipelinq's
became `crmTask`; this is the case task.

## What the suite found that the grep did not

The rename reached the descriptors and the store calls on the first pass, and
the suite still failed six times. Twice for the same reason, in two different
places:

`KpiAggregationService::ids()` and `DemoCaseloadGateway::schemaIds()` each build
a local array keyed `'task'` and hand it round as `$ids['task']`. Renaming the
READERS without the BUILDERS left `$ids['caseTask']` resolving to null, which
surfaced as a `TypeError` on a nullable argument three frames away.

Then PHPStan caught the array shapes that still declared `task: string` after
the builders moved. A grep for the slug would never have found either: the key
is not the slug, it just happened to be spelled like it.

## The decoys

`itemType` in `WorkQueueService`, `type` in `CaseReassignmentService`,
`BulkReassignModal`, `taskApi` and `dashboardHelpers` — all row and item type
labels for mixed lists, not schemas.

`tests/e2e/ci-seed.sh` names the slug in its required-schema list and moved with
it. That list is checked after the import and exits before Playwright, so a miss
there reports every spec as not run.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Case management

## ADDED Requirements

### Requirement: The case task is namespaced (REQ-CM-070)

The case task schema SHALL be `caseTask` and SHALL NOT be `task`. planninq's
project task keeps the bare slug; pipelinq uses `crmTask`.

The three claiming schemas share `description`, `priority` and `status` alone,
so all three are renamed apart rather than folded onto one owner.

Every local schema-id map keyed by the slug SHALL move with it, including
`KpiAggregationService::ids()` and `DemoCaseloadGateway::schemaIds()`, together
with their declared array shapes. A reader renamed without its builder resolves
to null and fails several frames away, where the cause is no longer visible.

`tests/e2e/ci-seed.sh` SHALL name the new slug in its required-schema list.

The rename SHALL NOT touch `task` where it is a row or item type label:
`WorkQueueService`'s `itemType`, or the `type` key in
`CaseReassignmentService`, `BulkReassignModal`, `taskApi` and
`dashboardHelpers`.

#### Scenario: The KPI counts still resolve their schema

- **WHEN** the dashboard KPIs are computed
- **THEN** the task count resolves a schema id rather than null.

#### Scenario: The demo caseload still seeds its tasks

- **WHEN** the demo caseload is seeded
- **THEN** each task is created against a resolved schema id.
16 changes: 16 additions & 0 deletions openspec/changes/namespace-the-case-task/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Tasks

- [x] 1.1 Move the key, slug and register-list entry, and the manifest references
**files**: lib/Settings/dossiq_register.json, lib/Settings/dossiq_mock_register.json, src/manifest.json
- [x] 2.1 Move the slug in the config-key map and the JSON-encoded property map
**files**: lib/Service/Settings/SchemaSlugMap.php, lib/Service/Support/JsonEncodedStringProperties.php
- [x] 2.2 Move the schema-id map key in BOTH builders and their readers, and the declared array shapes
**files**: lib/Service/KpiAggregationService.php, lib/Service/DemoCaseloadGateway.php, lib/Service/DemoCaseloadReport.php, lib/Service/DemoCaseloadSeedDataService.php
- [x] 2.3 Follow the slug through the frontend object-type registration and store calls
**files**: src/store/store.js, src/store/modules/advice.js, src/store/modules/inspection.js, src/store/modules/enforcement.js, src/store/modules/workflow.js, src/views/widgets/TaskRemindersWidget.vue, src/views/widgets/MyTasksWidget.vue, src/components/flow/TaskWaitingCaseSection.vue, src/components/tabs/CaseTasksTab.vue
- [x] 3.1 Add the rename to the colliding-slug map
**files**: lib/Repair/RenameCollidingSchemaSlugs.php
- [x] 4.1 Move the slug in the e2e seed's required-schema list
**files**: tests/e2e/ci-seed.sh
- [x] 4.2 Repoint the test stubs, leaving item and row type labels alone
**files**: tests/Unit/, tests/vitest/
2 changes: 1 addition & 1 deletion src/components/flow/TaskWaitingCaseSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default {

try {
this.task =
(await this.objectStore.fetchObject('task', taskId)) || null
(await this.objectStore.fetchObject('caseTask', taskId)) || null
} catch {
// An unreadable task renders nothing, same as a non-flow task.
this.task = null
Expand Down
2 changes: 1 addition & 1 deletion src/components/tabs/CaseTasksTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default {
}
this.loading = true
try {
const results = await this.objectStore.fetchCollection('task', {
const results = await this.objectStore.fetchCollection('caseTask', {
case: this.resolvedCaseId,
_limit: 50,
})
Expand Down
16 changes: 8 additions & 8 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@
"content": {
"source": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"filter": {
"assignee": "@me",
"isTerminalStatus": false
Expand Down Expand Up @@ -817,7 +817,7 @@
"content": {
"source": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"filter": {
"assignee": "@me",
"isTerminalStatus": false,
Expand Down Expand Up @@ -1329,7 +1329,7 @@
"icon": "ClipboardCheckOutline",
"content": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"filter": {
"case": "@objectId"
},
Expand Down Expand Up @@ -2124,7 +2124,7 @@
"title": "Tasks",
"config": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"allowSavedViews": true,
"showViewAction": false,
"actions": [
Expand Down Expand Up @@ -2158,7 +2158,7 @@
"title": "New task",
"config": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"_note": "CREATE-FORM archetype — the task creation form (route /tasks/new, usually pre-seeded with ?caseId=). A not-yet-persisted task has no id, so no children, history, aggregates or content leaves apply: it is just the Data form. No audit sidebar tab either — there is no object to have a history. All richness lives on TaskDetail once the task exists.",
"documentationUrl": "https://procest.conduction.nl",
"widgets": [
Expand Down Expand Up @@ -2197,7 +2197,7 @@
"title": "Task",
"config": {
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"_note": "ACTION-ITEM archetype — a work item on a case (task.case) with assignee/dueDate/priority/status and an inline checklist[]. The schema declares NO comms linkedTypes and has NO child schemas pointing at it, so it stays deliberately lean: the Data widget carries the who/when/status and the checklist, and lifecycleActions drive the status machine (available → active → completed / terminated). No files/notes/contacts/calendar leaves — a task carries none of that content; those live on the parent case. Audit history (status transitions) is a sidebar tab.",
"documentationUrl": "https://procest.conduction.nl",
"widgets": [
Expand Down Expand Up @@ -4053,7 +4053,7 @@
"source": {
"kind": "objectCount",
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"groupBy": [
"status"
]
Expand All @@ -4067,7 +4067,7 @@
"source": {
"kind": "objectCount",
"register": "dossiq",
"schema": "task",
"schema": "caseTask",
"filter": {
"deadline": {
"lt": "now"
Expand Down
4 changes: 2 additions & 2 deletions src/store/modules/advice.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const useAdviceStore = defineStore('advice', {

// Create task for the adviseur if internal
if (requestData.type === 'intern' && requestData.advisor) {
await objectStore.saveObject('task', {
await objectStore.saveObject('caseTask', {
case: requestData.case,
title: `Advies uitbrengen: ${requestData.subject || 'Adviesaanvraag'}`,
description: requestData.questions || '',
Expand Down Expand Up @@ -205,7 +205,7 @@ export const useAdviceStore = defineStore('advice', {
})

// Create task for behandelaar
await objectStore.saveObject('task', {
await objectStore.saveObject('caseTask', {
case: request.case,
title: `Advies verlopen: ${request.subject || request.advisor}`,
description: `Advies van ${request.advisor} is verlopen. Beoordeel of procedure kan doorgaan zonder dit advies.`,
Expand Down
2 changes: 1 addition & 1 deletion src/store/modules/enforcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export const useEnforcementStore = defineStore('enforcement', {
async createBegunstigingTask(caseId, action) {
try {
const objectStore = useObjectStore()
return await objectStore.saveObject('task', {
return await objectStore.saveObject('caseTask', {
case: caseId,
title: 'Hercontrole uitvoeren',
description: `Begunstigingstermijn van ${action.compliance_period} dagen is verlopen. Voer hercontrole uit voor ${action.intervention}.`,
Expand Down
2 changes: 1 addition & 1 deletion src/store/modules/inspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ export const useInspectionStore = defineStore('inspection', {
async createFollowUpTask(caseId, failedCount, reportId) {
try {
const objectStore = useObjectStore()
return await objectStore.saveObject('task', {
return await objectStore.saveObject('caseTask', {
case: caseId,
title: `Opvolging vereist: ${failedCount} afwijkingen geconstateerd`,
description: `Inspectierapport bevat ${failedCount} niet-conforme punten. Beoordeel de afwijkingen en plan opvolging.`,
Expand Down
2 changes: 1 addition & 1 deletion src/store/modules/workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ export const useWorkflowStore = defineStore('workflow', {
*/
async dispatchCreateTaskAction(action, caseData) {
const objectStore = useObjectStore()
await objectStore.saveObject('task', {
await objectStore.saveObject('caseTask', {
title: action.title || t('dossiq', 'New task'),
description: action.description || '',
case: caseData.id,
Expand Down
2 changes: 1 addition & 1 deletion src/store/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function initializeStores() {
}
if (config.register && config.task_schema) {
objectStore.registerObjectType(
'task',
'caseTask',
config.task_schema,
config.register,
)
Expand Down
2 changes: 1 addition & 1 deletion src/views/widgets/MyTasksWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default {
// 32 tasks while `assignee=rbac-editor` returned the 2 that match. So
// this widget was fetching EVERY user's tasks and filtering only by
// status, which is not what a widget called "My Tasks" may show.
const results = await this.objectStore.fetchCollection('task', {
const results = await this.objectStore.fetchCollection('caseTask', {
assignee: currentUser,
isTerminalStatus: false,
_limit: 7,
Expand Down
Loading
Loading