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: 2 additions & 2 deletions openspec/changes/zaaktype-configuratie/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
status: proposed
created: "2026-03-20"
schema: spec-driven
created: 2026-03-20
6 changes: 6 additions & 0 deletions openspec/changes/zaaktype-configuratie/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Zaaktype Configuratie Design

## Components
1. **DurationPicker.vue** - User-friendly ISO 8601 duration picker with presets
2. **CaseTypeDetail.vue** - Enhanced with publish validation, active case warnings
3. **GeneralTab.vue** - Enhanced with DurationPicker for deadline fields
21 changes: 8 additions & 13 deletions openspec/changes/zaaktype-configuratie/proposal.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
## Why
# Zaaktype Configuratie Implementation

Zaaktype Configuratie Specification is not yet implemented in Procest. This change proposes adding this capability based on the detailed spec in `specs/zaaktype-configuratie/spec.md`.
## Summary
Implement V1 configuration UI improvements: duration picker component, publish validation with Dutch error messages, active case count warnings, and inline validation feedback.

**Feature tier**: V1 (basic CRUD UI, status diagram editor, document type config, property definition config, role type config, result type config), V2 (visual flow designer, import/export, ZTC sync, versioning, test mode)

## What Changes

See `specs/zaaktype-configuratie/spec.md` for full requirements and scenarios.

## Impact

- **Code**: New frontend views and/or backend services
- **Dependencies**: OpenRegister (data storage), Nextcloud platform APIs
- **Testing**: Unit tests for new services, integration tests for API endpoints
## Scope
- REQ-ZTC-01: Publish validation with Dutch error messages
- REQ-ZTC-02: Status diagram visual enhancement (color indicators)
- REQ-ZTC-11: Tab item counts, inline validation
- REQ-ZTC-12: Duration picker component for processing deadline
6 changes: 6 additions & 0 deletions openspec/changes/zaaktype-configuratie/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tasks

- [x] TASK-ZC-01: Create DurationPicker.vue component
- [x] TASK-ZC-02: Integrate DurationPicker into GeneralTab.vue
- [x] TASK-ZC-03: Add active case count warning to CaseTypeDetail.vue
- [x] TASK-ZC-04: Improve publish validation with Dutch error messages
37 changes: 31 additions & 6 deletions src/views/settings/CaseTypeDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,27 @@
</div>
</div>

<!-- Active case warning -->
<div v-if="activeCaseCount > 0 && !isCreate" class="case-type-detail__warning">
<p>{{ t('procest', 'There are {count} active cases of this type. Changes will only apply to new cases.', { count: activeCaseCount }) }}</p>
</div>

<!-- Publish errors -->
<div v-if="publishErrors.length > 0" class="case-type-detail__publish-errors">
<p><strong>{{ t('procest', 'Cannot publish:') }}</strong></p>
<ul>
<li v-for="(err, i) in publishErrors" :key="i">
{{ err }}
<li v-for="(err, i) in publishErrors" :key="i">
{{ err }}
</li>
</ul>
</div>

<!-- Save feedback -->
<p v-if="saveError" class="case-type-detail__error">
{{ saveError }}
<p v-if="saveError" class="case-type-detail__error">
{{ saveError }}
</p>
<p v-if="saveSuccess" class="case-type-detail__success">
{{ t('procest', 'Saved successfully') }}
<p v-if="saveSuccess" class="case-type-detail__success">
{{ t('procest', 'Saved successfully') }}
</p>

<NcLoadingIcon v-if="loadingDetail" />
Expand Down Expand Up @@ -151,6 +156,7 @@ export default {
validationErrors: {},
publishErrors: [],
statusTypes: [],
activeCaseCount: 0,
}
},
computed: {
Expand Down Expand Up @@ -182,6 +188,16 @@ export default {
if (data) {
this.form = { ...EMPTY_FORM, ...data }
}
// Count active cases of this type
try {
const cases = await this.objectStore.fetchCollection('case', {
'_filters[caseType]': this.caseTypeId,
_limit: 1,
})
this.activeCaseCount = cases?.length || 0
} catch (e) {
this.activeCaseCount = 0
}
this.loadingDetail = false
},

Expand Down Expand Up @@ -280,6 +296,15 @@ export default {
gap: 8px;
}

.case-type-detail__warning {
background: var(--color-warning-light, rgba(var(--color-warning-rgb), 0.1));
border: 1px solid var(--color-warning);
border-radius: var(--border-radius);
padding: 12px;
margin-bottom: 16px;
color: var(--color-warning-text);
}

.case-type-detail__publish-errors {
background: var(--color-error-light, rgba(var(--color-error-rgb), 0.1));
border: 1px solid var(--color-error);
Expand Down
148 changes: 148 additions & 0 deletions src/views/settings/components/DurationPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<template>
<div class="duration-picker">
<div class="duration-picker__input-row">
<NcTextField
:value="daysInput"
:label="t('procest', 'Days')"
type="number"
class="duration-picker__field"
@update:value="onDaysChange" />
<span class="duration-picker__iso">
{{ displayValue || t('procest', 'Enter days') }}
</span>
</div>

<div class="duration-picker__presets">
<button
v-for="preset in presets"
:key="preset.value"
class="duration-picker__preset"
:class="{ 'duration-picker__preset--active': value === preset.value }"
type="button"
@click="selectPreset(preset)">
{{ preset.label }}
</button>
</div>
</div>
</template>

<script>
import { NcTextField } from '@nextcloud/vue'
import { parseDuration, formatDuration, isValidDuration } from '../../../utils/durationHelpers.js'

export default {
name: 'DurationPicker',
components: {
NcTextField,
},
props: {
value: {
type: String,
default: '',
},
presetType: {
type: String,
default: 'deadline',
validator: v => ['deadline', 'extension'].includes(v),
},
},
computed: {
daysInput() {
if (!this.value) return ''
const parsed = parseDuration(this.value)
if (!parsed) return ''
let totalDays = 0
if (parsed.years) totalDays += parsed.years * 365
if (parsed.months) totalDays += parsed.months * 30
if (parsed.weeks) totalDays += parsed.weeks * 7
if (parsed.days) totalDays += parsed.days
return String(totalDays)
},
displayValue() {
if (!this.value || !isValidDuration(this.value)) return ''
const days = parseInt(this.daysInput, 10)
if (!days) return this.value
const weeks = Math.floor(days / 7)
const remainder = days % 7
if (remainder === 0 && weeks > 0) {
return `${this.value} (${weeks} ${t('procest', 'weeks')})`
}
return `${this.value} (${days} ${t('procest', 'days')})`
},
presets() {
if (this.presetType === 'extension') {
return [
{ label: t('procest', '2 weeks'), value: 'P14D' },
{ label: t('procest', '4 weeks'), value: 'P28D' },
{ label: t('procest', '6 weeks'), value: 'P42D' },
]
}
return [
{ label: t('procest', '6 weeks'), value: 'P42D' },
{ label: t('procest', '8 weeks'), value: 'P56D' },
{ label: t('procest', '13 weeks'), value: 'P91D' },
{ label: t('procest', '26 weeks'), value: 'P182D' },
]
},
},
methods: {
onDaysChange(val) {
const days = parseInt(val, 10)
if (!days || days < 0) {
this.$emit('input', '')
return
}
this.$emit('input', `P${days}D`)
},
selectPreset(preset) {
this.$emit('input', preset.value)
},
},
}
</script>

<style scoped>
.duration-picker__input-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}

.duration-picker__field {
max-width: 120px;
}

.duration-picker__iso {
font-size: 13px;
color: var(--color-text-maxcontrast);
font-family: monospace;
}

.duration-picker__presets {
display: flex;
gap: 6px;
flex-wrap: wrap;
}

.duration-picker__preset {
padding: 4px 10px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-pill);
background: var(--color-main-background);
cursor: pointer;
font-size: 12px;
transition: all 0.15s ease;
}

.duration-picker__preset:hover {
background: var(--color-background-hover);
}

.duration-picker__preset--active {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary);
font-weight: 500;
}
</style>
29 changes: 14 additions & 15 deletions src/views/settings/tabs/GeneralTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,21 @@
<!-- Processing Deadline -->
<div class="form-group">
<label class="required">{{ t('procest', 'Processing deadline') }}</label>
<NcTextField
<DurationPicker
:value="form.processingDeadline"
:placeholder="t('procest', 'e.g., P56D (56 days)')"
:error="!!errors.processingDeadline"
:helper-text="errors.processingDeadline || deadlinePreview"
@update:value="v => $emit('update', 'processingDeadline', v)" />
preset-type="deadline"
@input="v => $emit('update', 'processingDeadline', v)" />
<span v-if="errors.processingDeadline" class="field-error">{{ errors.processingDeadline }}</span>
</div>

<!-- Service Target -->
<div class="form-group">
<label>{{ t('procest', 'Service target') }}</label>
<NcTextField
<DurationPicker
:value="form.serviceTarget"
:placeholder="t('procest', 'e.g., P42D (42 days)')"
:error="!!errors.serviceTarget"
:helper-text="errors.serviceTarget || serviceTargetPreview"
@update:value="v => $emit('update', 'serviceTarget', v)" />
preset-type="deadline"
@input="v => $emit('update', 'serviceTarget', v)" />
<span v-if="errors.serviceTarget" class="field-error">{{ errors.serviceTarget }}</span>
</div>

<!-- Extension Allowed -->
Expand All @@ -109,12 +107,11 @@
<!-- Extension Period (conditional) -->
<div v-if="form.extensionAllowed" class="form-group">
<label class="required">{{ t('procest', 'Extension period') }}</label>
<NcTextField
<DurationPicker
:value="form.extensionPeriod"
:placeholder="t('procest', 'e.g., P28D (28 days)')"
:error="!!errors.extensionPeriod"
:helper-text="errors.extensionPeriod || extensionPreview"
@update:value="v => $emit('update', 'extensionPeriod', v)" />
preset-type="extension"
@input="v => $emit('update', 'extensionPeriod', v)" />
<span v-if="errors.extensionPeriod" class="field-error">{{ errors.extensionPeriod }}</span>
</div>

<!-- Confidentiality -->
Expand Down Expand Up @@ -199,13 +196,15 @@
import { NcTextField, NcSelect, NcCheckboxRadioSwitch } from '@nextcloud/vue'
import { formatDuration } from '../../../utils/durationHelpers.js'
import { getOriginOptions, getConfidentialityOptions } from '../../../utils/caseTypeValidation.js'
import DurationPicker from '../components/DurationPicker.vue'

export default {
name: 'GeneralTab',
components: {
NcTextField,
NcSelect,
NcCheckboxRadioSwitch,
DurationPicker,
},
props: {
form: {
Expand Down
Loading