Skip to content
Draft
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
3 changes: 3 additions & 0 deletions openspec/changes/case-management/design.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Case Management Design

## Status
pr-created (PR #266)

## Architecture
All case data stored as OpenRegister objects. Frontend uses shared object store (`createObjectStore`) with `_filters` and `_search` parameters for filtering and search.

Expand Down
237 changes: 237 additions & 0 deletions openspec/changes/case-management/hydra.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
{
"spec_slug": "case-management",
"app": "procest",
"repo": "ConductionNL/procest",
"issue": 180,
"depends_on": [],
"schema_version": 2,
"cycles": [
{
"cycle": 1,
"trigger": "build:queued",
"started_at": "2026-04-18T18:02:59Z",
"ended_at": "2026-04-20T21:34:55Z",
"outcome": "aborted",
"outcome_reason": "rebuild:queued \u2014 human wiped prior cycle",
"pattern_tags": [],
"stages": [
{
"stage": "build",
"persona": "Al Gorithm",
"model": "haiku",
"container": "hydra-builder",
"started_at": "2026-04-18T18:02:59Z",
"ended_at": "2026-04-18T18:03:00Z",
"turns_used": 72,
"turns_budget": 200,
"cost_usd": 0.812,
"checks_run": [],
"checks_skipped": [],
"findings": [
{
"id": "b-builder-build-0",
"severity": "WARNING",
"gate": null,
"rule": "builder output flagged failure language",
"status": "open",
"note": "## Summary\n\nI've successfully completed the implementation of the case-management feature for the Procest repository. Here's what was accomplished:\n\n### Completed Tasks\n\n**TASK-CM-01 & CM-02: CaseList Enhancements** \u2705\n- Added filter controls (priority, handler, overdue status dropdowns)\n- Implemented client-side search across case title, description, and identifier\n- Created filter UI with clear filters button\n- Filters update dynamically via Vue computed properties\n\n**TASK-CM-03 & CM-04: Compon",
"autofixable": false
}
],
"decisions": [],
"verdict": "fail"
},
{
"stage": "pre-review-quality",
"persona": "orchestrator",
"container": "hydra-quality-runner",
"started_at": "2026-04-18T18:02:59Z",
"ended_at": "2026-04-18T18:03:00Z",
"exit_code": 0,
"checks_run": [
"phpcs",
"phpmd",
"psalm",
"phpstan",
"phpmetrics",
"composer-audit",
"spdx-headers",
"forbidden-patterns",
"eslint",
"stylelint",
"npm-audit",
"phpunit"
],
"checks_skipped": [
"php-lint",
"publiccode",
"gitleaks",
"trivy",
"newman"
],
"gates": {
"phpcs": {
"pass": true,
"failures": 0
},
"phpmd": {
"pass": true,
"failures": 0
},
"psalm": {
"pass": true,
"failures": 0
},
"phpstan": {
"pass": true,
"failures": 0
},
"phpmetrics": {
"pass": true,
"failures": 0
},
"composer-audit": {
"pass": true,
"failures": 0
},
"spdx-headers": {
"pass": true,
"failures": 0
},
"forbidden-patterns": {
"pass": true,
"failures": 0
},
"eslint": {
"pass": true,
"failures": 0
},
"stylelint": {
"pass": true,
"failures": 0
},
"npm-audit": {
"pass": true,
"failures": 0
},
"phpunit": {
"pass": true,
"failures": 0
}
},
"findings": [],
"verdict": "pass"
}
]
},
{
"cycle": 2,
"trigger": "build:queued",
"started_at": "2026-04-20T21:41:47Z",
"ended_at": null,
"outcome": "in-flight",
"outcome_reason": null,
"pattern_tags": [
"browser-test-nc-setup-failed"
],
"stages": [
{
"stage": "build",
"persona": "Al Gorithm",
"model": "haiku",
"container": "hydra-builder",
"started_at": "2026-04-20T21:35:37Z",
"ended_at": "2026-04-20T21:41:43Z",
"exit_code": 0,
"turns_used": 171,
"turns_budget": 40,
"checks_run": [],
"checks_skipped": [],
"findings": [],
"decisions": [],
"verdict": "pass"
},
{
"stage": "pre-review-quality",
"persona": "orchestrator",
"container": "hydra-quality-runner",
"started_at": "2026-04-21T04:45:04Z",
"ended_at": "2026-04-21T04:45:04Z",
"exit_code": 1,
"checks_run": [
"php -l",
"composer check:strict (phpcs)",
"composer check:strict (phpmd)",
"composer check:strict (psalm)",
"composer check:strict (phpstan)",
"phpmetrics",
"composer audit",
"spdx-headers",
"forbidden-patterns",
"npm run lint (eslint)",
"npm run lint (stylelint)",
"npm audit"
],
"checks_skipped": [
"publiccode",
"stub-scan",
"gitleaks",
"trivy",
"composer test:unit (phpunit)",
"newman"
],
"gates": {
"php-lint": "pass",
"phpcs": "fail",
"phpmd": "pass",
"psalm": "pass",
"phpstan": "pass",
"phpmetrics": "pass",
"composer-audit": "pass",
"spdx-headers": "pass",
"publiccode": "skip",
"forbidden-patterns": "pass",
"eslint": "fail",
"stylelint": "fail",
"npm-audit": "pass",
"stub-scan": "skip",
"gitleaks": "skip",
"trivy": "skip",
"phpunit": "skip",
"newman": "skip"
},
"findings": [
{
"id": "prq-phpcs",
"severity": "WARNING",
"gate": "phpcs",
"rule": "composer check:strict (phpcs) failing",
"status": "open",
"note": "...\nThe repository at \"/server/apps/app\" does not have the correct ownership and git refuses to use it:\n\nfatal: detected dubious ownership in repository at '/server/apps/app'\nTo add an exception for this directory, call:\n\ngit config --global --add safe.directory /server/apps/app\n\nComposer could not detect the root package (conductionnl/procest) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version\n\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m",
"autofixable": true
},
{
"id": "prq-eslint",
"severity": "WARNING",
"gate": "eslint",
"rule": "npm run lint (eslint) failing",
"status": "open",
"note": "...\n\n> procest@0.1.0 lint\n> eslint src\n\nsh: 1: eslint: not found",
"autofixable": true
},
{
"id": "prq-stylelint",
"severity": "WARNING",
"gate": "stylelint",
"rule": "npm run lint (stylelint) failing",
"status": "open",
"note": "...\n\n> procest@0.1.0 stylelint\n> stylelint src/**/*.vue src/**/*.scss src/**/*.css\n\nsh: 1: stylelint: not found",
"autofixable": true
}
],
"verdict": "fail"
}
]
}
]
}
94 changes: 94 additions & 0 deletions openspec/changes/case-management/plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"change": "case-management",
"project": "procest",
"repo": "ConductionNL/procest",
"created": "2026-04-20",
"tracking_issue": 180,
"tasks": [
{
"id": "TASK-CM-01",
"title": "Add filter controls to CaseList.vue (priority, handler, overdue)",
"priority": "MVP",
"status": "done",
"spec_ref": "openspec/changes/case-management/tasks.md#TASK-CM-01",
"files_affected": ["src/views/cases/CaseList.vue"],
"acceptance_criteria": [
"Filter dropdowns for priority (low, normal, high, urgent) and handler text input implemented",
"Overdue status indicator displayed via CSS styling based on caseValidation helper",
"Filters applied client-side via Vue reactive properties",
"Clear filters button available"
]
},
{
"id": "TASK-CM-02",
"title": "Add search functionality to CaseList.vue",
"priority": "MVP",
"status": "done",
"spec_ref": "openspec/changes/case-management/tasks.md#TASK-CM-02",
"files_affected": ["src/views/cases/CaseList.vue"],
"acceptance_criteria": [
"Search input field integrated into case list controls",
"Search works across title, description, and identifier fields",
"Results update dynamically as user types"
]
},
{
"id": "TASK-CM-03",
"title": "Create CustomPropertiesPanel.vue component",
"priority": "MVP",
"status": "done",
"spec_ref": "openspec/changes/case-management/tasks.md#TASK-CM-03",
"files_affected": ["src/views/cases/components/CustomPropertiesPanel.vue"],
"acceptance_criteria": [
"Component displays custom properties from property definitions by case type",
"Shows count of filled properties",
"Edit capability for properties",
"Loading state handled",
"Empty state message when no properties defined"
]
},
{
"id": "TASK-CM-04",
"title": "Create DocumentChecklist.vue component",
"priority": "MVP",
"status": "done",
"spec_ref": "openspec/changes/case-management/tasks.md#TASK-CM-04",
"files_affected": ["src/views/cases/components/DocumentChecklist.vue"],
"acceptance_criteria": [
"Component displays required documents for case type",
"Shows present/total count",
"Visual indicators for completed vs missing documents",
"Upload capability for missing documents",
"Loading state and empty state handled"
]
},
{
"id": "TASK-CM-05",
"title": "Integrate new panels into CaseDetail.vue",
"priority": "MVP",
"status": "done",
"spec_ref": "openspec/changes/case-management/tasks.md#TASK-CM-05",
"files_affected": ["src/views/cases/CaseDetail.vue"],
"acceptance_criteria": [
"CustomPropertiesPanel integrated as new detail card",
"DocumentChecklist integrated as new detail card",
"Both panels receive caseId and caseTypeId props",
"Proper import statements and component registration"
]
},
{
"id": "TASK-CM-06",
"title": "Enhance caseValidation.js error messages",
"priority": "MVP",
"status": "done",
"spec_ref": "openspec/changes/case-management/tasks.md#TASK-CM-06",
"files_affected": ["src/utils/caseValidation.js"],
"acceptance_criteria": [
"getCaseTypeUnusableReason() enhanced with date-specific messages",
"Messages include days until/since validity window boundaries",
"Administrative guidance provided in error messages",
"Backward compatibility maintained"
]
}
]
}
15 changes: 12 additions & 3 deletions src/utils/caseValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function isCaseTypeUsable(caseType) {

/**
* Get a specific unusable reason for a case type.
* Enhanced with detailed validity window error messages.
*
* @param {object} caseType Case type object
* @return {string|null} Reason why the case type cannot be used, or null if usable
Expand All @@ -41,7 +42,7 @@ export function getCaseTypeUnusableReason(caseType) {
if (!caseType) return t('procest', 'Case type not found')

if (caseType.isDraft === true || caseType.isDraft === 'true') {
return t('procest', 'Cannot create a case with a draft case type. The case type must be published first.')
return t('procest', 'Cannot create a case: this case type is still in draft status. The case type must be published before cases can be created. Contact your administrator.')
}

const today = new Date()
Expand All @@ -52,7 +53,11 @@ export function getCaseTypeUnusableReason(caseType) {
validFrom.setHours(0, 0, 0, 0)
if (validFrom > today) {
const dateStr = caseType.validFrom.split('T')[0]
return t('procest', 'Cannot create a case with a case type that is not yet valid. The case type is valid from {date}.', { date: dateStr })
const daysUntil = Math.ceil((validFrom - today) / (1000 * 60 * 60 * 24))
return t('procest', 'Cannot create a case: this case type is not yet valid. It becomes available on {date} ({days} days from now). Check back later or contact your administrator if this is incorrect.', {
date: dateStr,
days: daysUntil,
})
}
}

Expand All @@ -61,7 +66,11 @@ export function getCaseTypeUnusableReason(caseType) {
validUntil.setHours(0, 0, 0, 0)
if (validUntil < today) {
const dateStr = caseType.validUntil.split('T')[0]
return t('procest', 'Cannot create a case with an expired case type. The case type was valid until {date}.', { date: dateStr })
const daysAgo = Math.ceil((today - validUntil) / (1000 * 60 * 60 * 24))
return t('procest', 'Cannot create a case: this case type has expired. It was valid until {date} ({days} days ago). If you need to use this case type again, contact your administrator.', {
date: dateStr,
days: daysAgo,
})
}
}

Expand Down
Loading