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
33 changes: 24 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"extends @nextcloud/browserslist-config"
],
"dependencies": {
"@conduction/nextcloud-vue": "^1.0.0-beta.29",
"@conduction/nextcloud-vue": "^1.0.0-beta.38",
"@nextcloud/auth": "^2.6.0",
"@nextcloud/axios": "~2.5.2",
"@nextcloud/dialogs": "^3.2.0",
Expand Down
10 changes: 10 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:manifest="manifest"
:custom-components="customComponents"
:page-types="pageTypes"
:formatters="formatters"
app-id="procest"
:translate="translateForApp"
:permissions="permissions" />
Expand Down Expand Up @@ -47,6 +48,15 @@ export default {
type: Object,
default: () => ({}),
},
/**
* Cell-formatter registry — forwarded to CnAppRoot as `cnFormatters`.
* Resolves `pages[].config.columns[].formatter` ids on index/logs
* pages (see src/services/formatters.js).
*/
formatters: {
type: Object,
default: () => ({}),
},
},

data() {
Expand Down
46 changes: 44 additions & 2 deletions src/customComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import WerkvoorraadView from './views/Werkvoorraad.vue'
// CaseMapView removed — superseded by manifest `type: 'map'` CnMapPage
// (see openspec/changes/case-map-overview/design.md).
import DoorlooptijdView from './views/DoorlooptijdDashboard.vue'
import VoorstellenView from './views/voorstellen/VoorstelList.vue'
// VoorstellenView removed — the Voorstellen list page is now a declarative
// `type:"index"` on the `voorstel` schema (formatter columns + status badge,
// see src/manifest.json + src/services/formatters.js).
import VoorstelDetailView from './views/voorstellen/VoorstelDetail.vue'
import AdminRootView from './views/settings/AdminRoot.vue'
import PublicCaseView from './views/public/PublicCaseView.vue'
Expand Down Expand Up @@ -51,6 +53,44 @@ import CaseDocumentsTab from './components/tabs/CaseDocumentsTab.vue'
// MAY reference it by string name. See openspec/changes/map-component/.
import MapComponent from './components/map/MapComponent.vue'

/**
* Row-action handler for the Voorstellen index: POST a parafering-reminder
* notification for the step the voorstel is currently waiting on. Registered
* below as a "function" entry so the manifest action
* `{ id: "reminder", handler: "voorstelReminder" }` can dispatch to it —
* CnIndexPage calls a function-typed `customComponents[handler]` with
* `{ actionId, item }` on row-action click. (Replaces the bespoke
* `sendReminder()` that lived in the deleted VoorstelList.vue.)
*
* @param {object} ctx Dispatch context.
* @param {string} ctx.actionId The action id (`"reminder"`).
* @param {object} ctx.item The voorstel row.
* @return {Promise<void>}
*/
async function voorstelReminder({ actionId, item }) {
const steps = (() => {
const snap = item && item.routeSnapshot
if (!snap) return []
try { return typeof snap === 'string' ? JSON.parse(snap) : snap } catch { return [] }
})()
const current = steps.find((s) => s.order === item.currentStep)
const actor = current ? (current.label || current.actor || '-') : '-'
try {
await fetch('/apps/procest/api/notifications/parafering-reminder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
requesttoken: window.OC?.requestToken,
'OCS-APIREQUEST': 'true',
},
body: JSON.stringify({ voorstelId: item.id, actor, onderwerp: item.onderwerp }),
})
} catch (error) {
// eslint-disable-next-line no-console
console.error('[procest] parafering reminder failed', error)
}
}

export default {
// --- Genuine exceptions: no abstract analogue. ---
MyWorkView, // bespoke 4-tab filter UI mixing case + task entities
Expand All @@ -62,9 +102,11 @@ export default {
AdminRootView, // multi-tab admin root (lib settings-custom-slot gap)

// --- Migration cost: deferred to a follow-up. ---
VoorstellenView, // status-tabs filter tied to parafeerroute lifecycle
VoorstelDetailView, // parafeerroute multi-step approver flow

// --- Row-action handlers (function entries — dispatched by manifest `handler` id). ---
voorstelReminder, // Voorstellen index → POST a parafering reminder

// --- Anonymous-public routes (no auth, no main menu). ---
PublicCaseView,
PublicAppointmentPage,
Expand Down
3 changes: 3 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import App from './App.vue'
import bundledManifest from './manifest.json'
import customComponents from './customComponents.js'
import mapFormatters from './services/mapFormatters.js'
import formatters from './services/formatters.js'

// Library CSS — must be explicit import (webpack tree-shakes side-effect imports from aliased packages)
import '@conduction/nextcloud-vue/css/index.css'
Expand Down Expand Up @@ -104,6 +105,7 @@ tryLoadTranslations()
const pageTypesProp = { ...defaultPageTypes }
const customComponentsProp = { ...customComponents }
const mapFormattersProp = { ...mapFormatters }
const formattersProp = { ...formatters }

// Expose the map formatter registry as a Vue global so `CnMapPage`
// (and any future map-type pages) can resolve named formatters from
Expand All @@ -120,6 +122,7 @@ new Vue({
customComponents: customComponentsProp,
pageTypes: pageTypesProp,
mapFormatters: mapFormattersProp,
formatters: formattersProp,
},
}),
}).$mount('#content')
20 changes: 18 additions & 2 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,25 @@
{
"id": "Voorstellen",
"route": "/voorstellen",
"type": "custom",
"type": "index",
"title": "Voorstellen",
"component": "VoorstellenView"
"config": {
"register": "procest",
"schema": "voorstel",
"columns": [
{ "key": "onderwerp", "label": "Onderwerp" },
{ "key": "type", "label": "Type", "formatter": "voorstelType" },
{ "key": "status", "label": "Status", "formatter": "voorstelStatus", "widget": "badge" },
{ "key": "currentStep", "label": "Stap", "formatter": "voorstelStepProgress", "sortable": false },
{ "key": "routeSnapshot", "label": "Wacht op", "formatter": "voorstelWaitingActor", "sortable": false },
{ "key": "@self.updated", "label": "Dagen in stap", "formatter": "voorstelDaysInStep", "align": "right" },
{ "key": "steller", "label": "Steller" }
],
"actions": [
{ "id": "reminder", "label": "Herinnering sturen", "icon": "BellRing", "handler": "voorstelReminder" }
],
"sidebar": { "enabled": true, "showMetadata": true }
}
},
{
"id": "VoorstelDetail",
Expand Down
125 changes: 125 additions & 0 deletions src/services/formatters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: EUPL-1.2
// Copyright (C) 2026 Conduction B.V.
//
// Cell-formatter registry for procest's manifest-driven index pages.
//
// Each entry is `(value, row, property) => string|number` — pure data
// shaping, referenced by id from `pages[].config.columns[].formatter`
// in src/manifest.json (resolved by CnDataTable / CnCellRenderer via the
// `formatters` prop passed to CnAppRoot, see @conduction/nextcloud-vue →
// docs/migrating-to-manifest.md "Column formatters"). Keep this file to
// pure functions — the Vue layer stays the library's abstract
// CnIndexPage / CnDataTable; only the app-specific per-row logic lives
// here. (`mapFormatters.js` is the separate registry for `type:"map"`
// marker formatting.)

import { translate as t } from '@nextcloud/l10n'

const VOORSTEL_STATUS_LABELS = {
concept: 'Concept',
in_parafering: 'In parafering',
ter_accordering: 'Ter accordering',
geaccordeerd: 'Geaccordeerd',
aangeboden: 'Aangeboden',
besloten: 'Besloten',
gearchiveerd: 'Gearchiveerd',
teruggestuurd: 'Teruggestuurd',
}

const VOORSTEL_TYPE_LABELS = {
dt_advies: 'DT-advies',
collegeadvies: 'Collegeadvies',
raadsvoorstel: 'Raadsvoorstel',
}

/**
* Parse a voorstel's parafeerroute snapshot into a step array.
*
* @param {object} row The voorstel object.
* @return {Array<object>} The steps (possibly empty).
*/
function voorstelSteps(row) {
const snap = row && row.routeSnapshot
if (!snap) return []
try {
return typeof snap === 'string' ? JSON.parse(snap) : snap
} catch {
return []
}
}

/**
* Metadata `updated` timestamp for a row, tolerating both `@self.updated`
* (OpenRegister metadata envelope) and the older `_self.updated` /
* `updatedAt` shapes.
*
* @param {object} row The object.
* @return {string|undefined} ISO timestamp, or undefined.
*/
function rowUpdated(row) {
if (!row) return undefined
return (row['@self'] && row['@self'].updated)
|| (row._self && row._self.updated)
|| row.updatedAt
|| undefined
}

export default {
/**
* Human label for a voorstel `type` enum value.
*
* @param {string} value The raw `type`.
* @return {string}
*/
voorstelType: (value) => t('procest', VOORSTEL_TYPE_LABELS[value] || value || '-'),

/**
* Human label for a voorstel `status` enum value (also rendered as a
* `widget: "badge"` pill).
*
* @param {string} value The raw `status`.
* @return {string}
*/
voorstelStatus: (value) => t('procest', VOORSTEL_STATUS_LABELS[value] || value || '-'),

/**
* `currentStep / totalSteps` progress for a voorstel's parafeerroute.
*
* @param {*} value Unused (the column key is `currentStep`).
* @param {object} row The voorstel object.
* @return {string}
*/
voorstelStepProgress: (value, row) => {
const steps = voorstelSteps(row)
if (!steps.length || !row || !row.currentStep) return '-'
return `${row.currentStep}/${steps.length}`
},

/**
* Label / actor of the step the voorstel is currently waiting on.
*
* @param {*} value Unused.
* @param {object} row The voorstel object.
* @return {string}
*/
voorstelWaitingActor: (value, row) => {
const steps = voorstelSteps(row)
if (!steps.length || !row || !row.currentStep) return '-'
const current = steps.find((s) => s.order === row.currentStep)
return current ? (current.label || current.actor || '-') : '-'
},

/**
* Number of days the voorstel has been in its current step.
*
* @param {*} value Unused (the column key is `@self.updated`).
* @param {object} row The voorstel object.
* @return {string}
*/
voorstelDaysInStep: (value, row) => {
const updated = rowUpdated(row)
if (!updated) return '-'
const days = Math.floor((Date.now() - new Date(updated).getTime()) / 86400000)
return `${days}d`
},
}
Loading
Loading