Skip to content

feat: adopt OpenRegister declarative annotations (lifecycle / aggregations / calculations / notifications)#141

Open
rubenvdlinde wants to merge 25 commits intodevelopmentfrom
feature/declarative-annotation-pilot
Open

feat: adopt OpenRegister declarative annotations (lifecycle / aggregations / calculations / notifications)#141
rubenvdlinde wants to merge 25 commits intodevelopmentfrom
feature/declarative-annotation-pilot

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Summary

Pilot adoption of OpenRegister's four declarative schema annotations against decidesk's Meeting + ActionItem schemas. Replaces ~533 lines of bespoke transition machinery wholesale with the platform path.

Companion PR: ConductionNL/openregister#1357 (the platform side).

What landed

Schema Annotation(s) Replaces
Meeting x-openregister-lifecycle (6 transitions, 5 guarded) + x-openregister-notifications (transition open/close → admin) MeetingService::transition, MeetingController::lifecycle, /api/meetings/{id}/lifecycle route, two unit tests
ActionItem x-openregister-aggregations (6 metrics) + x-openregister-calculations (daysLate, isOverdue) (analytics service kept for now; full migration in a follow-up)

Net change

  • −1,139 lines removed (MeetingService, MeetingController, MeetingControllerTest, MeetingServiceTest, hardcoded JS transition map mirror)
  • +245 lines added (MeetingTransitionGuard, schema annotations, frontend API switch)

Frontend

MeetingLifecycle.vue no longer hardcodes the transition table. It calls:

  • GET /apps/openregister/api/objects/{id}/available-actions to populate buttons
  • POST /apps/openregister/api/objects/{id}/transition to apply

The schema is now the single source of truth — frontend cannot drift.

Guard wiring

MeetingTransitionGuard implements OCA\OpenRegister\Lifecycle\LifecycleGuardInterface and delegates to existing WorkflowService (domain rules, chair gates) + QuorumService (open quorum check). The schema's requires field uses the guard's FQCN so OR's LifecycleGuardRegistry autowires it from the server container.

Calculations + aggregations interaction

isOverdue is materialised on save by the calculation evaluator and immediately becomes filterable by aggregations (totalOverdue: count where isOverdue=true). avgDaysLate averages the materialised daysLate field. This proves the annotations compose.

Verified end-to-end in the dev container

  • draft → scheduled → opened → closed transitions all succeed
  • invalid transitions return clean 422 from TransitionController
  • pause in operations domain denied by guard with structured 422
  • daysLate + isOverdue materialise correctly on create + update
  • avgDaysLate reads the materialised field; totalOverdue counts via the materialised flag
  • meetingOpened / meetingClosed fire to admin via INotificationManager; schedule action correctly does NOT fire either (action filter)

Test plan

  • Wait for OR PR #1357 to land on development
  • Bump OR dependency in CI / ensure latest dev container picks up the new platform code
  • Run occ maintenance:repair to re-import the schemas (versions bumped)
  • Smoke test: create a Meeting, transition through schedule → open → close; check admin gets two notifications
  • Smoke test: create ActionItem with completedAt + dueDate; verify daysLate + isOverdue are populated
  • Smoke test: GET /api/objects/aggregations/decidesk/action-item/{name} for each declared aggregation

Note on PR #138

PR #138 (Tier 2 manifest pilot) is on a different branch but shares the same commits via the same author/timeline. After this PR merges to development, PR #138 will only show its original manifest-pilot scope.

Pilot of the manifest renderer landing in @conduction/nextcloud-vue
PR #89. Routes the Decisions and DecisionDetail pages through
CnPageRenderer instead of mounting their view components directly;
every other route is unchanged so the blast radius stays minimal.

Changes:
- src/manifest.json: declares Decisions and DecisionDetail as
  type:"custom" pages mapped to registry component names. The
  manifest also carries `dependencies: ["openregister"]`, anticipating
  the eventual move to Tier 4 with CnDependencyMissing.
- src/customComponents.js: registry mapping DecisionsView /
  DecisionDetailView to the existing Decisions.vue / DecisionDetail.vue
  components.
- src/App.vue: setup() calls useAppManifest('decidesk',
  bundledManifest); provide() exposes cnManifest, cnCustomComponents,
  cnTranslate so CnPageRenderer can inject them. The visible shell
  (NcContent → MainMenu → router-view, the OpenRegister-installed
  gate, and the loading state) is untouched.
- src/router/index.js: Decisions and DecisionDetail routes now mount
  CnPageRenderer; route names match the manifest's pages[].id so the
  renderer matches by $route.name === page.id.
- docs/manifest-pilot.md: rationale, what changed, why type:"custom",
  how to run in dev with the local nextcloud-vue alias, and what the
  pilot does NOT cover.

Why type:"custom" for routes that look like list/detail pages:
the renderer's built-in type:"index"/"detail" paths only forward
page.config as props to the corresponding Cn*Page; they don't run
useListView / useDetailView. Decidesk's existing views need those
composables AND a #create-dialog slot override, so wrapping them in
the registry keeps full app-side control while routing through the
manifest. A future renderer iteration can add a config shape that
auto-wires the composable; until then, the registry is the right
tool for views with composable-driven data loading.

Dev setup until @conduction/nextcloud-vue ships a beta with PR #89:
  cd ../nextcloud-vue && git checkout feature/json-manifest-renderer
  cd ../decidesk && USE_LOCAL_LIB=1 npm run dev

webpack.config.js already aliases @conduction/nextcloud-vue to
../nextcloud-vue/src when USE_LOCAL_LIB=1.
Two compounding bugs that together meant every install thought it had
configured Decidesk while no register was ever created in OpenRegister:

1. lib/Service/SettingsService.php — loadConfiguration() called the
   OpenRegister importer with only `appId` + `force`, but the importer
   signature is importFromApp(appId, data, version, force). PHP named
   args silently dropped `data` and `version`, so the importer ran
   against an empty configuration and returned an empty result. Now the
   service reads lib/Settings/decidesk_register.json, parses it, picks
   up the version from `info.version`, and passes all four arguments.

2. lib/Settings/decidesk_register.json — only `components.schemas`
   was declared; no `components.registers` block. The OpenRegister
   importer creates registers from `data.components.registers[*]` —
   without it, schemas were imported but no register entity bound them
   together, so `/api/objects?register=decidesk&schema=decision`
   returned 404 ("Register not found: 'decidesk'"). Added a `decidesk`
   register entry referencing all 17 schema slugs (governance-body,
   meeting, participant, agenda-item, motion, amendment, voting-round,
   vote, decision, action-item, minutes, digital-document,
   monetary-amount, offer, order, product, report) following the same
   shape used by procest / opencatalogi / pipelinq.

Bumped info.version from 0.1.0 to 0.2.0 so existing installs that
already imported v0.1.0 (schemas-only) pick up the new register on
the next `occ maintenance:repair`.

Verified locally: `occ maintenance:repair --include-expensive` now
reports "Decidesk configuration imported successfully (version: 0.2.0)"
and `/apps/openregister/api/registers?_search=decidesk` returns the
new register with all 17 schemas attached.
Aligns decidesk with the @conduction/nextcloud-vue stack so the new
manifest renderer's useListView API path actually works against
decidesk's existing views.

ADR-022 ("apps consume OpenRegister abstractions over local
duplication"):

- src/store/store.js: imports useObjectStore from
  @conduction/nextcloud-vue (the canonical Pinia store) instead of
  the local fork. The configure() + registerObjectType() calls in
  initializeStores() use the same shape so no other call sites needed
  changes there.
- src/store/modules/object.js: deleted. It was a 90-line fork of
  the canonical store missing fetchSchema / fetchCollection /
  pagination / facets / errors — exactly what useListView's new
  one-arg API expects.
- 7 view/component files: rename the canonical .fetchObjects(...)
  callsite to .fetchCollection(...) — the canonical store uses the
  plural-collection name; both return the array so the rest of the
  call site is unchanged.

Manifest renderer bootstrap (per @conduction/nextcloud-vue's
CLAUDE.md required-bootstrap section):

- src/main.js: adds registerIcons({}) + registerTranslations() before
  Vue mount so library-rendered strings translate to the user's NC
  locale and CnIcon resolves icon names. Wraps loadTranslations() in
  .catch().then(mount) so a missing per-locale l10n/<lang>.json
  doesn't block app boot — pre-existing behaviour silently dropped
  the mount callback on a 404 in the locale file.

Dev-environment unblockers (USE_LOCAL_LIB=1):

- webpack.config.js: adds a vue-loader-compatible scss rule so
  CnCard.vue's `<style lang="scss">` compiles in dev (sass-loader
  was already in node_modules but not registered). Also sets
  output.publicPath = 'auto' so dynamically-loaded chunks resolve
  via document.currentScript.src instead of a hardcoded
  /apps/<appId>/js/ that doesn't match the bind-mounted custom_apps
  path.

Verified end-to-end via Playwright: navigating to /decisions now
mounts CnPageRenderer → Decisions.vue → CnIndexPage with the empty
state, "Add Decision" → CnSchemaFormDialog opens with all 17 fields
auto-rendered from the schema. No JS console errors.
Three pilot-soak findings, all making decidesk match procest / pipelinq
visually rather than diverging.

Dashboard (`src/views/Dashboard.vue`):
- Replaces the hand-rolled `<div class="decidesk-dashboard">` shell with
  `<CnDashboardPage>` so the page picks up the same outer padding /
  margins used by every other Conduction app dashboard. Earlier the
  custom CSS (`padding: 8px 4px 24px`) drifted from the library default
  and rendered noticeably tighter on the left.
- Wires the `#header-actions` slot with the standard quick-action set
  (New Decision / New Action Item / New Minutes / Refresh) — the slot
  was simply absent in the hand-rolled markup, so the top-right action
  area was empty.
- Reuses the existing KPI counts via custom widgets in the
  CnDashboardPage grid layout instead of an inline `CnKpiGrid`.
- Refresh button is wired to a `loadCounts()` method so refreshing the
  KPIs doesn't require a full page reload.

Main menu (`src/navigation/MainMenu.vue`):
- Dashboard nav icon: `Home` → `ViewDashboard` (the matrix-of-dots
  glyph). This is the icon every other Conduction app uses for its
  Dashboard entry and matches Nextcloud core's own dashboard.
- Settings entry now lives inside `<NcAppNavigationSettings>` (the
  canonical footer slot) instead of a bare `NcAppNavigationItem`.
  Visually identical for the user but matches the NC convention so
  future settings entries can stack inside the same group without
  another wrapper change.

Verified end-to-end via Playwright at /apps/decidesk/: the four header
action buttons render in the top right, the title block has the same
left margin as procest's dashboard, and the Settings entry occupies
the bottom-left footer with the cog icon.
Two divergences from the canonical pattern that caused decidesk's
dashboard widgets to look subtly off:

1. Empty widget titles. WIDGET_DEFS had `title: ''` for every entry;
   procest / pipelinq pass real translated strings. Even when
   `showTitle: false` hides the header bar, the title is read by
   CnWidgetWrapper for its aria attributes and content-area class
   selectors. Now resolved through a computed widgetDefs() so titles
   re-translate when the locale changes.
2. Double card chrome. Bottom-row "Notulen" / "Besluiten" widgets
   wrapped a `CnConfigurationCard` inside a (borderless) custom
   widget slot, so the visible card was the inner CnConfigurationCard
   while CnWidgetWrapper sat empty around it. Procest puts content
   directly in the slot and lets CnWidgetWrapper provide the card
   chrome via `showTitle: true` (the default). Now the bottom row
   drops `showTitle: false` from its layout entries — CnWidgetWrapper
   renders a single card with a proper title bar — and the slot
   contents are plain content (the link), not another card.

KPI row continues to render with `showTitle: false` (borderless
CnWidgetWrapper, just the CnStatsBlock visible) — same as procest's
KPI row.
Migrates decidesk from Tier 2 (just /decisions through CnPageRenderer)
to Tier 4 of the manifest-renderer adoption ladder: the app shell,
side navigation, and every route are now driven by `src/manifest.json`.

Manifest expansion (`src/manifest.json` v0.2.0):
- `menu[]` grew from empty to 6 entries (Dashboard, Minutes, Decisions,
  Action items, Motions, Meetings) — CnAppNav reads this and renders
  the side panel.
- `pages[]` grew from 2 to 20, covering every route the old hand-rolled
  router carried (Dashboard, GovernanceBodies + detail, Meetings +
  detail, LiveMeeting, Participants + detail, AgendaItems + detail,
  Motions + detail, AmendmentDetail, Minutes + detail, Decisions +
  detail, ActionItems + detail, Settings).
- Every page is `type: "custom"` for now — they wrap existing app
  views (which all use the useListView / useDetailView composables and
  ship slot-overridden dialogs). Once nextcloud-vue#90 lands the
  auto-wire for `type: "index"` / `type: "detail"`, most of these
  collapse to declarative entries with no registry component.

`src/customComponents.js`:
- 20 entries mapping registry names to view imports.
- Each wrapped in `defineAsyncComponent` so views still chunk-split
  into separate bundles (preserves the lazy-load behaviour the old
  `() => import(...)` router config gave).

`src/router/index.js`:
- Routes are now generated from `manifest.pages.map(...)`. Every entry
  becomes `{ name: page.id, path: page.route, component: CnPageRenderer,
  props: page.route.includes(':') }`. A new route is one manifest line.
- Catch-all `path: '*'` redirect to `/` preserved.

`src/App.vue`:
- Replaces the hand-rolled NcContent + MainMenu + NcAppContent + custom
  OpenRegister-installed gate with a single `<CnAppRoot>` element.
- `useAppManifest('decidesk', bundledManifest)` provides the manifest
  + isLoading state.
- `customComponents` and `translate` (closure over @nextcloud/l10n's
  `translate('decidesk', key)`) are passed in.
- `manifest.dependencies: ["openregister"]` drives CnAppRoot's
  dependency-check phase, replacing the old `useSettingsStore.hasOpenRegisters`
  gate.

`src/main.js`:
- `initializeStores()` moved out of App.vue's `created()` hook into
  the bootstrap chain (after `loadTranslations`, before mount). Stores
  are ready by the time CnAppRoot renders.

`src/navigation/MainMenu.vue`:
- Deleted. CnAppNav (driven by manifest.menu[]) replaces it. Apps that
  need a hand-rolled menu can use CnAppRoot's `#menu` slot to override.

Verified end-to-end via Playwright after the matching nextcloud-vue
fixes in feature/json-manifest-renderer:
- Side nav: 6 manifest items render with the right icons
- Dashboard: header actions + KPIs + Notulen/Besluiten widgets render
- Decisions list (manifest-driven): CnIndexPage renders the empty state
- Minutes list: same, navigates correctly
- Layout: content area fills the full width once CnAppRoot wraps the
  router-view in NcAppContent
- No more "Required apps are missing" false positive (useAppStatus now
  checks OC.appswebroots, not capabilities)

Out of scope for this commit (filed as follow-ups):
- Documentation external link in the menu — the manifest schema has
  no `href` field for non-route items. Drop for now.
- Settings entry in NcAppNavigationSettings (the footer group) — the
  schema has no concept of menu sections. Drop for now.
- Auto-wire for type:"index" / type:"detail" pages — see
  ConductionNL/nextcloud-vue#90.
Both placed in section "settings" so CnAppNav renders them inside the
NcAppNavigationSettings collapsible footer group:
- Documentation: external href, opens conduction.nl in a new tab
- SettingsMenu: routes to the existing Settings page

Bumps manifest version to 0.3.0.
- Documentation drops the section: "settings" so it renders as the
  last item of the main list (always visible) instead of being hidden
  behind the NcAppNavigationSettings popover. Settings stays in the
  settings section.
- icon-dashboard is not a built-in Nextcloud icon class, so the entry
  rendered without an icon. Switch to icon-category-dashboard.
- Bump app version to 0.1.2 for cache busting after the JS change.
Documentation belongs visually next to Settings at the bottom of the
navigation, not floating in the middle of the main list. CnAppNav
now renders section:settings items directly in the footer slot, so
moving Documentation to that section anchors it to the bottom right
above Settings — same UX as procest's MainMenu without the popover.

Bumps app version to 0.1.3 for cache busting.
Decidesk shipped four bespoke per-schema Pinia stores
(meetings/minutes/decisions/actionItems) that hand-rolled CRUD
against /apps/openregister/api/objects (or in meetings' case,
/apps/decidesk/api/meetings). The canonical useObjectStore from
@conduction/nextcloud-vue already covers every method they
implemented (fetchCollection, fetchObject, saveObject, deleteObject,
deleteObjects, pagination, search, errors, caching).

A code audit shows the duplication was almost entirely dead:
- minutes.js / actionItems.js: 0 callers anywhere
- meetings.js: 1 view used 3 filter setters; the 5 CRUD actions and
  the lifecycle action were never imported
- decisions.js: 1 view used `publishDecision`; the CRUD actions were
  never imported

Changes:
- Delete the four dead store modules (-840 lines).
- Drop dead exports from store/store.js.
- In initializeStores(), register every object type the views
  actually reference (9 total: minutes, decision, action-item,
  meeting, agenda-item, motion, amendment, governance-body,
  participant). The previous list of 3 left useListView calls for
  the other 6 throwing "type not registered" silently — surfaced by
  the Playwright smoke test against /apps/decidesk/meetings.
- Add `setActivePinia(pinia)` in pinia.js. main.js calls
  initializeStores() *before* app.$mount(), at which point Pinia is
  not yet bound to the Vue app. Without setActivePinia, useStore()
  returned a detached store missing its `_s` plugin chain and every
  registerObjectType() call was a silent no-op (caught by the
  catch() in main.js's bootstrap chain). Surfaced by an empty
  objectTypeRegistry at runtime even though the source said
  otherwise.
- Meetings.vue: drop meetingStore import + dead .setSearchQuery /
  .setFilters / .resetFilters / .page = 1 calls. Local searchQuery
  and selectedLifecycle data() props remain to bind to the toolbar
  inputs.
- DecisionDetail.vue: inline the single POST to
  /apps/decidesk/api/decisions/{id}/publish (formerly
  decisionStore.publishDecision). Server still enforces admin /
  outcome / isPublished guards.
- Bump app version to 0.1.7.

Smoke test (browser-6): /apps/decidesk/{,minutes,decisions,
action-items,motions,agenda-items,participants,governance-bodies,
meetings,settings} — all 0 console errors, 9/9 types registered.
The MeetingController CRUD endpoints (index/create/show/update/destroy)
were thin pass-throughs to OpenRegister's ObjectService — every
MeetingService method just called createFromArray/find/updateFromArray/
deleteFromId with register='decidesk', schema='meeting' and a log
line. The frontend never used them: views go through useObjectStore
→ /apps/openregister/api/objects?register=decidesk&schema=meeting
directly, which is the canonical path per ADR-022.

Deleted:
- MeetingController::index, create, show, update, destroy
  (kept lifecycle — that one wraps real workflow logic in
   MeetingService::transition)
- MeetingService::create, read, update, delete (pass-throughs)
- The 5 matching routes in appinfo/routes.php
- The container DI on MeetingController + the matching test mock

Net: -260 PHP lines + -5 routes; the CRUD path is unchanged from the
UI's point of view since it was already hitting openregister.

Pre-existing fixes encountered:
- SettingsService.php:196 PHPCS concat-with-spaces
- Application.php AnalyticsController DI was missing userSession +
  groupManager (Psalm TooFewArguments — service registration drifted
  from the constructor signature).

Verification:
- composer phpcs / psalm: clean
- phpunit Meeting* tests: 17 pass, 4 skipped
- Playwright /apps/decidesk/meetings: 0 console errors
- Direct hits on the deleted endpoints return 405 (POST/PUT/DELETE);
  GET falls through to the SPA catch-all
…on stack

Pilot of OpenRegister's declarative lifecycle annotation against the Meeting schema.

Backend:
- Annotate Meeting schema with x-openregister-lifecycle (field, initial, final, transitions)
  under configuration.x-openregister-lifecycle so OR's existing schema persistence carries it.
- Add MeetingTransitionGuard implementing OCA\OpenRegister\Lifecycle\LifecycleGuardInterface;
  delegates to existing WorkflowService (domain rules + chair gates) and QuorumService
  (open-meeting quorum check). Registered under DI tag `decidesk.meeting.transitionGuard`
  matching the schema's `requires` field.
- Remove MeetingService, MeetingController, /api/meetings/{id}/lifecycle route, and
  associated unit tests — replaced wholesale by OR's POST /api/objects/{id}/transition
  + GET /api/objects/{id}/available-actions.

Frontend:
- MeetingLifecycle.vue: drop hardcoded transition map mirroring the backend (the warning
  comment said it would silently diverge — now removed at the source). Load actions from
  GET /apps/openregister/api/objects/{id}/available-actions; apply via POST
  /apps/openregister/api/objects/{id}/transition. Labels and badge colours stay client-side.

Net deletion: ~533 lines of meeting-specific transition machinery + tests, replaced with
~125 lines of guard + schema annotation. Lifecycle shape is now single-source-of-truth in
the schema; the frontend cannot drift.
Live-pilot follow-ups:
- Bump register version to 0.3.2 + Meeting schema version to 0.2.2 so the
  next maintenance:repair re-imports the lifecycle annotation.
- Switch the `requires` field from the local DI tag string
  `decidesk.meeting.transitionGuard` to the guard's FQCN
  `OCA\Decidesk\Lifecycle\MeetingTransitionGuard`. OpenRegister resolves
  guards via the server container so it can autowire across apps; FQCNs
  are the only stable cross-app identifier.
The guard is referenced by FQCN in the schema's `requires` field;
OpenRegister's LifecycleGuardRegistry resolves FQCNs via the server
container, which autowires constructor dependencies. The local
registerService block was redundant.
Declares totalCompleted, byStatus, completedThisMonth, totalOpen.
Verified end-to-end against OpenRegister's
GET /api/objects/aggregations/{register}/{schema}/{name}.

ActionItemAnalyticsService is left in place for now: avgDaysToClose
needs computed fields, getCompletionRates needs cross-schema rollup,
getMyItems needs user-scoped buckets. Migration to fully declarative
analytics ships after the calculations-annotation change lands.
- daysLate (materialise:true): diffDays(completedAt, dueDate)
- isOverdue (materialise:true): and(ne(taskStatus, "completed"), lt(dueDate, $now))
- New aggregations leaning on the materialised fields:
  - totalOverdue: count where isOverdue=true
  - avgDaysLate: avg(daysLate) where taskStatus=completed

Verified end-to-end via OR's CalculationOnSaveListener + AggregationRunner.
Two transition-triggered notifications: meetingOpened (action=open) and
meetingClosed (action=close), both delivered to admin via the Nextcloud
INotificationManager. Subject template uses {{title}} interpolation.

Verified end-to-end via OR's AnnotationNotificationDispatcher +
AnnotationNotifier. Trigger action filter correctly rejects unrelated
transitions (e.g. schedule fires no notification).
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ ca37f67

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-29 08:55 UTC

Download the full PDF report from the workflow artifacts.

…ansition stack

Three more schemas migrate off hand-written state machines onto the
declarative platform path that landed in PR #141:

| Schema    | Annotation actions                                |
|-----------|---------------------------------------------------|
| Motion    | debate / vote / adopt / reject / withdraw         |
| Amendment | debate / vote / adopt / reject                    |
| Minutes   | submit / approve / sign / publish                 |

### Deletes

- MotionService::transitionLifecycle + MOTION_TRANSITIONS + AMENDMENT_TRANSITIONS constants
- MotionService::transitionLifecycle related tests
- MotionController::transition + MotionController::amendmentTransition
- MinutesGenerationService::transition + LIFECYCLE_TRANSITIONS + MissingObjectException import
- MinutesController::transition (admin-gated bespoke endpoint)
- /api/motions/{id}/transition + /api/amendments/{id}/transition + /api/minutes/{minutesId}/transition routes
- MotionService dependency on VotingService

### Replaces with

- VotingService now resolves OR's TransitionEngine from the container
  and calls .transition($motionId, 'vote'/'adopt'/'reject') directly.
- MinutesTransitionListener subscribes to ObjectTransitionedEvent and
  patches Minutes-specific bookkeeping (approvedAt on `approve`,
  signedBy on `approve`/`sign`) — preserves the side-effects that the
  old MinutesGenerationService::transition had inline.
- MinutesDetail.vue: drop the hardcoded transition map; load actions
  from /apps/openregister/api/objects/{id}/available-actions and apply
  via /apps/openregister/api/objects/{id}/transition.

### Verified end-to-end

- Motion: submitted→debating→voting→rejected
- Amendment: submitted→debating→voting→adopted
- Minutes: draft→review→approved→signed→published; approvedAt set on
  approve, signedBy=[admin] accumulates across approve+sign
@rubenvdlinde
Copy link
Copy Markdown
Contributor Author

Update — Motion / Amendment / Minutes lifecycle adoption

Pushed 70af1f4 extending the pilot to three more schemas:

Schema Annotation actions
Motion debate / vote / adopt / reject / withdraw
Amendment debate / vote / adopt / reject
Minutes submit / approve / sign / publish

Deletes: MotionService::transitionLifecycle, MotionController::transition, MotionController::amendmentTransition, MinutesGenerationService::transition, MinutesController::transition, three routes, and the MOTION_TRANSITIONS / AMENDMENT_TRANSITIONS / LIFECYCLE_TRANSITIONS constant tables.

Replaces with:

  • VotingService now resolves OR's TransitionEngine from the container directly and calls .transition($motionId, 'vote'/'adopt'/'reject') — no MotionService dependency.
  • MinutesTransitionListener subscribes to ObjectTransitionedEvent and patches Minutes-specific bookkeeping (approvedAt on approve, signedBy on approve/sign).
  • MinutesDetail.vue drops the hardcoded transition map; loads actions from /apps/openregister/api/objects/{id}/available-actions and applies via /apps/openregister/api/objects/{id}/transition.

Verified end-to-end in the dev container: motion submitted→debating→voting→rejected; amendment submitted→debating→voting→adopted; minutes draft→review→approved→signed→published with side-effect listener correctly populating approvedAt/signedBy=['admin'].

Net additional change: −430 / +255 lines.

@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ f2c0b99

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-29 09:09 UTC

Download the full PDF report from the workflow artifacts.

Surfaces meetings as read-only events in Nextcloud Calendar via
OpenRegister's RegisterCalendarProvider. dtstart=scheduledDate,
dtend=endDate, titleTemplate={{title}}, color=#0082c9.

The provider scopes calendars by the user's active organisation; in
the dev container admin's active org doesn't match the test schemas'
org, so the smoke test surfaces 0 calendars. Once admin's active org
matches the schemas' org (or schemas have NULL org), Meeting will
appear as a calendar in the Calendar app.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 68ac284

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-29 09:14 UTC

Download the full PDF report from the workflow artifacts.

New calculation:

  daysOpen = diffDays($now, @self.created)

Materialises on every save thanks to OR's CalculationOnSaveListener
injecting @self metadata at evaluation time (PR
ConductionNL/openregister#1357).

This is the canonical "time since creation" metric that previously
required a hand-written analytics query — the listener does it once,
materialises the integer, and aggregations can target it directly:

  avgDaysOpen: { metric: "avg", field: "daysOpen", filter: { taskStatus: { in: ["open","in-progress","overdue"] } } }
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 057fa67

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-29 11:59 UTC

Download the full PDF report from the workflow artifacts.

statusBadge is a materialise:false calculation: 'taskStatus' + ' (' +
(isOverdue ? 'overdue' : 'on track') + ')'.

Evaluated at response-render time only when caller passes
_extend=calculations. Demonstrates the virtual-calculation path that
landed in OR PR #1357.

Companion: ConductionNL/openregister#1357
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ c066db1

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-29 12:23 UTC

Download the full PDF report from the workflow artifacts.

…larative aggregations

Replaces the in-PHP iteration over every ActionItem (~50 lines per
metric) with 4 calls to OR's AggregationRunner.

### Schema additions
- daysToClose calculation (materialise:true): diffDays(completedAt, @self.created)
- daysToClose property declaration (integer, populated by the calc listener)
- avgDaysToClose aggregation (avg of daysToClose where taskStatus=completed)

### Service refactor
- getSummary delegates to a thin private aggregate() helper that calls
  OCA\OpenRegister\Service\Aggregation\AggregationRunner directly.
- Each metric (totalOpen, totalOverdue, completedThisMonth,
  avgDaysToClose) is now declared on the schema and computed by the
  platform — the service no longer iterates objects in PHP.

### Live verification

  ActionItemAnalyticsService::getSummary("2026-01-01","2026-12-31")
  → {"totalOpen":10,"totalOverdue":3,"completedThisMonth":4,"avgDaysToClose":0}

avgDaysToClose is 0 today because newly-imported ActionItems don't have
daysToClose materialised yet — once OR's
\`occ openregister:rematerialise-calculations decidesk action-item\`
command lands (class is shipped in OR PR #1357 but the <commands>
block is gated on an upstream DI fix), existing items will pick up
the materialised value on the next bulk pass.

Companion: ConductionNL/openregister#1357
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 4b9c025

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-29 12:24 UTC

Download the full PDF report from the workflow artifacts.

…t-webhook notifications

- Meeting.meetingReminderDaily — scheduled notification (intervalSec=86400, filter: lifecycle=scheduled). Picked up by openregister's ScheduledNotificationJob (verified live: "fired meetingReminderDaily on schema 657").
- Meeting.meetingClosed — adds webhook channel with persistent: true. NotificationsAnnotationInstaller auto-creates a managed Webhook entity (verified live: or-notif-meeting-meetingClosed provisioned with url + ObjectTransitionedEvent subscription).
- ActionItem.tooManyOverdue — threshold notification on the totalOverdue aggregation (op: gt, value: 10). AggregationThresholdListener fires once per below->above transition.

These three rules implement notif-v2 pilot tasks 9.1 (scheduled), 9.2 (threshold), 9.3 (persistent-webhook).
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 58154cf

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-29 15:14 UTC

Download the full PDF report from the workflow artifacts.

Formal administrative decisions (besluiten under ZGW BRC + Wet open
overheid) are decision-management domain logic — that belongs in
Decidesk, not the OpenRegister foundation. OpenRegister provides the
underlying storage / authorization / audit primitives the spec builds
on, but the lifecycle workflow (concept -> definitief -> ingetrokken),
zaak-besluit linking, BesluitType catalog, and Woo publication path
are all decidesk-domain concerns.

Spec content unchanged from the original. Top-of-proposal note added
to record the relocation date.

Companion change in openregister will remove the misplaced spec.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 0ef8114

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 416/416
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 12:12 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant