Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
39 changes: 39 additions & 0 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
use OCA\Decidesk\AppInfo\Application;
use OCA\Decidesk\Service\SettingsService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IUserSession;

/**
* Controller for managing Decidesk application settings.
Expand All @@ -37,21 +40,42 @@ class SettingsController extends Controller
*
* @param IRequest $request The request object
* @param SettingsService $settingsService The settings service
* @param IGroupManager $groupManager The group manager for admin checks
* @param IUserSession $userSession The user session
*
* @return void
*/
public function __construct(
IRequest $request,
private SettingsService $settingsService,
private IGroupManager $groupManager,
private IUserSession $userSession,
) {
parent::__construct(appName: Application::APP_ID, request: $request);
}//end __construct()

/**
* Check whether the current user is an admin. Returns a 403 JSONResponse if not.
*
* @return JSONResponse|null Null if admin, 403 response if not.
*/
private function requireAdmin(): ?JSONResponse
{
$user = $this->userSession->getUser();
if ($user === null || $this->groupManager->isAdmin($user->getUID()) === false) {
return new JSONResponse(['message' => 'Admin required'], Http::STATUS_FORBIDDEN);
}

return null;
}//end requireAdmin()

/**
* Retrieve all current settings.
*
* @NoAdminRequired
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
*
* @return JSONResponse
*/
public function index(): JSONResponse
Expand All @@ -64,10 +88,17 @@ public function index(): JSONResponse
/**
* Update settings with provided data.
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.2
*
* @return JSONResponse
*/
public function create(): JSONResponse
{
$denied = $this->requireAdmin();
if ($denied !== null) {
return $denied;
}

$data = $this->request->getParams();
$config = $this->settingsService->updateSettings($data);

Expand All @@ -85,10 +116,18 @@ public function create(): JSONResponse
* Forces a fresh import regardless of version, auto-configuring
* all schema and register IDs from the import result.
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.2
*
* @return JSONResponse
*/
public function load(): JSONResponse
{
$denied = $this->requireAdmin();
if ($denied !== null) {
return $denied;
}

$result = $this->settingsService->loadConfiguration(force: true);

return new JSONResponse($result);
Expand Down
11 changes: 10 additions & 1 deletion lib/Service/SettingsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public function __construct(
/**
* Check whether OpenRegister is installed and available.
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-1.3
*
* @return bool
*/
public function isOpenRegisterAvailable(): bool
Expand All @@ -82,6 +84,8 @@ public function isOpenRegisterAvailable(): bool
* Returns a flat array containing all app config values plus metadata
* fields (openregisters, isAdmin) consumed by the frontend.
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
*
* @return array<string,mixed>
*/
public function getSettings(): array
Expand All @@ -108,6 +112,8 @@ public function getSettings(): array
*
* @param array<string,mixed> $data The data to update
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.2
*
* @return array<string,mixed> The updated settings
*/
public function updateSettings(array $data): array
Expand All @@ -126,6 +132,9 @@ public function updateSettings(array $data): array
*
* @param bool $force Force re-import even if already configured.
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-1.3
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
*
* @return array<string,mixed> Result with success flag, message, and version.
*/
public function loadConfiguration(bool $force=false): array
Expand Down Expand Up @@ -162,7 +171,7 @@ public function loadConfiguration(bool $force=false): array
);
return [
'success' => false,
'message' => $e->getMessage(),
'message' => 'Configuration import failed. See server log for details.',
];
}//end try
}//end loadConfiguration()
Expand Down
76 changes: 38 additions & 38 deletions openspec/changes/p1-dashboard-and-navigation/tasks.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,67 @@
## 1. Backend — Register Configuration

- [ ] 1.1 Create or update `lib/Settings/decidesk_register.json` with all 17 schema definitions (ActionItem, AgendaItem, Amendment, Decision, DigitalDocument, GovernanceBody, Meeting, Minutes, MonetaryAmount, Motion, Offer, Order, Participant, Product, Report, Vote, VotingRound) in OpenAPI 3.0.0 + x-openregister format
- [ ] 1.2 Add seed data objects (8 objects: 2 GovernanceBody, 2 Meeting, 2 Participant, 1 Motion, 1 Decision) with Dutch values and `@self` envelope in the register JSON
- [ ] 1.3 Verify `SettingsService::importFromApp()` is called in the repair step and imports all schemas and seed objects without errors
- [x] 1.1 Create or update `lib/Settings/decidesk_register.json` with all 17 schema definitions (ActionItem, AgendaItem, Amendment, Decision, DigitalDocument, GovernanceBody, Meeting, Minutes, MonetaryAmount, Motion, Offer, Order, Participant, Product, Report, Vote, VotingRound) in OpenAPI 3.0.0 + x-openregister format
- [x] 1.2 Add seed data objects (8 objects: 2 GovernanceBody, 2 Meeting, 2 Participant, 1 Motion, 1 Decision) with Dutch values and `@self` envelope in the register JSON
- [x] 1.3 Verify `SettingsService::importFromApp()` is called in the repair step and imports all schemas and seed objects without errors

## 2. Backend — Settings Controller

- [ ] 2.1 Ensure `SettingsController` exposes `GET /apps/decidesk/api/settings` returning `{ openRegisters: bool, isAdmin: bool }` and register slugs for all 17 entity types
- [ ] 2.2 Ensure `POST /apps/decidesk/api/settings/load` triggers `ConfigurationService::importFromApp()` and returns success/error response
- [x] 2.1 Ensure `SettingsController` exposes `GET /apps/decidesk/api/settings` returning `{ openRegisters: bool, isAdmin: bool }` and register slugs for all 17 entity types
- [x] 2.2 Ensure `POST /apps/decidesk/api/settings/load` triggers `ConfigurationService::importFromApp()` and returns success/error response

## 3. Frontend — Project Scaffold

- [ ] 3.1 Create `src/main.js` bootstrapping Vue 2 app with Pinia and Vue Router, mounted on `#content`
- [ ] 3.2 Create `src/store/store.js` exporting `initializeStores()` that fetches settings then registers all 17 entity types via `objectStore.registerObjectType(name, schemaSlug, registerSlug)`
- [ ] 3.3 Create settings Pinia store (`src/store/modules/settings.js`) with `fetchSettings()` and `saveSettings()` actions
- [ ] 3.4 Create `src/router/index.js` with Vue Router in history mode, base `/index.php/apps/decidesk/`, all 12 named flat routes (Dashboard, MeetingList, MeetingDetail, MotionList, MotionDetail, DecisionList, DecisionDetail, ParticipantList, ParticipantDetail, GovernanceBodyList, GovernanceBodyDetail, Settings), and catch-all `*` redirecting to `/`
- [x] 3.1 Create `src/main.js` bootstrapping Vue 2 app with Pinia and Vue Router, mounted on `#content`
- [x] 3.2 Create `src/store/store.js` exporting `initializeStores()` that fetches settings then registers all 17 entity types via `objectStore.registerObjectType(name, schemaSlug, registerSlug)`
- [x] 3.3 Create settings Pinia store (`src/store/modules/settings.js`) with `fetchSettings()` and `saveSettings()` actions
- [x] 3.4 Create `src/router/index.js` with Vue Router in history mode, base `/index.php/apps/decidesk/`, all 12 named flat routes (Dashboard, MeetingList, MeetingDetail, MotionList, MotionDetail, DecisionList, DecisionDetail, ParticipantList, ParticipantDetail, GovernanceBodyList, GovernanceBodyDetail, Settings), and catch-all `*` redirecting to `/`

## 4. Frontend — App.vue and MainMenu

- [ ] 4.1 Create `src/App.vue` using `NcContent` with three rendering states: loading (`NcLoadingIcon`), no-OpenRegister (`NcEmptyContent`), ready (`MainMenu` + `NcAppContent` + `router-view`); call `initializeStores()` in `created()`
- [ ] 4.2 Create `src/components/MainMenu.vue` using `NcAppNavigation` with `NcAppNavigationItem` for: Dashboard, Vergaderingen, Moties, Besluiten, Deelnemers, Bestuursorganen; footer settings link via `NcAppNavigationSettings`; all strings via `t(appName, '...')`
- [ ] 4.3 Inject `sidebarState` in App.vue and provide it to child components for `CnIndexSidebar` support
- [x] 4.1 Create `src/App.vue` using `NcContent` with three rendering states: loading (`NcLoadingIcon`), no-OpenRegister (`NcEmptyContent`), ready (`MainMenu` + `NcAppContent` + `router-view`); call `initializeStores()` in `created()`
- [x] 4.2 Create `src/components/MainMenu.vue` using `NcAppNavigation` with `NcAppNavigationItem` for: Dashboard, Vergaderingen, Moties, Besluiten, Deelnemers, Bestuursorganen; footer settings link via `NcAppNavigationSettings`; all strings via `t(appName, '...')`
- [x] 4.3 Inject `sidebarState` in App.vue and provide it to child components for `CnIndexSidebar` support

## 5. Frontend — Dashboard Page

- [ ] 5.1 Create `src/views/DashboardView.vue` using `CnDashboardPage` as the layout component
- [ ] 5.2 Add four `CnStatsBlock` KPI cards: upcoming meetings (lifecycle=scheduled), pending motions (lifecycle=submitted|debating), open action items (taskStatus=open|in-progress), recent decisions (outcome=adopted, last 30 days)
- [ ] 5.3 Fetch all four KPI data sources in parallel using `Promise.all` in the `created()` hook; show `NcLoadingIcon` skeleton while loading
- [ ] 5.4 Add `CnChartWidget` (donut) showing Meeting lifecycle distribution; show empty-state message when no Meeting objects exist
- [ ] 5.5 Add `CnTileWidget` tiles for Meetings, Motions, Decisions, Participants, Governance Bodies; each tile routes to its list view
- [ ] 5.6 Implement 60-character title truncation with `…` for meeting titles on dashboard cards; expose full title via tooltip (`title` attribute)
- [x] 5.1 Create `src/views/DashboardView.vue` using `CnDashboardPage` as the layout component
- [x] 5.2 Add four `CnStatsBlock` KPI cards: upcoming meetings (lifecycle=scheduled), pending motions (lifecycle=submitted|debating), open action items (taskStatus=open|in-progress), recent decisions (outcome=adopted, last 30 days)
- [x] 5.3 Fetch all four KPI data sources in parallel using `Promise.all` in the `created()` hook; show `NcLoadingIcon` skeleton while loading
- [x] 5.4 Add `CnChartWidget` (donut) showing Meeting lifecycle distribution; show empty-state message when no Meeting objects exist
- [x] 5.5 Add `CnTileWidget` tiles for Meetings, Motions, Decisions, Participants, Governance Bodies; each tile routes to its list view
- [x] 5.6 Implement 60-character title truncation with `…` for meeting titles on dashboard cards; expose full title via tooltip (`title` attribute)

## 6. Frontend — Global Search

- [ ] 6.1 Add a search input component in the navigation header (or `NcAppNavigationSearch`) that triggers a search after 3+ characters with 400 ms debounce
- [ ] 6.2 Implement `useSearch` composable (or inline) that calls OpenRegister `_search` API across Meeting, Motion, Decision, AgendaItem, Participant schemas
- [ ] 6.3 Render up to 10 results in a floating dropdown with entity type icon, title, and lifecycle/status badge; group or label by type
- [ ] 6.4 Implement keyboard navigation in dropdown: down/up arrow to move between results, Enter to navigate, Escape to close and return focus to input
- [ ] 6.5 Show "Geen resultaten gevonden" message when the search returns zero matches
- [x] 6.1 Add a search input component in the navigation header (or `NcAppNavigationSearch`) that triggers a search after 3+ characters with 400 ms debounce
- [x] 6.2 Implement `useSearch` composable (or inline) that calls OpenRegister `_search` API across Meeting, Motion, Decision, AgendaItem, Participant schemas
- [x] 6.3 Render up to 10 results in a floating dropdown with entity type icon, title, and lifecycle/status badge; group or label by type
- [x] 6.4 Implement keyboard navigation in dropdown: down/up arrow to move between results, Enter to navigate, Escape to close and return focus to input
- [x] 6.5 Show "Geen resultaten gevonden" message when the search returns zero matches

## 7. Frontend — Settings Page

- [ ] 7.1 Create `src/views/SettingsView.vue` with `CnVersionInfoCard` as first element, then `CnRegisterMapping`, then `CnSettingsSection` blocks per feature area
- [ ] 7.2 Add "Register opnieuw importeren" button that calls `POST /apps/decidesk/api/settings/load` and shows an `NcToast` success or error notification
- [x] 7.1 Create `src/views/SettingsView.vue` with `CnVersionInfoCard` as first element, then `CnRegisterMapping`, then `CnSettingsSection` blocks per feature area
- [x] 7.2 Add "Register opnieuw importeren" button that calls `POST /apps/decidesk/api/settings/load` and shows an `NcToast` success or error notification

## 8. Frontend — NL Design System Theming

- [ ] 8.1 Create `src/assets/nl-design.css` mapping NL Design System token names (e.g. `--nldesign-color-brand-1`) to Nextcloud CSS variable equivalents (e.g. `var(--color-primary)`)
- [ ] 8.2 Import `nl-design.css` in `src/main.js`; verify no `--nldesign-*` references remain in component `<style>` blocks
- [ ] 8.3 Verify dark mode renders correctly by toggling Nextcloud dark mode and visually checking all pages
- [x] 8.1 Create `src/assets/nl-design.css` mapping NL Design System token names (e.g. `--nldesign-color-brand-1`) to Nextcloud CSS variable equivalents (e.g. `var(--color-primary)`)
- [x] 8.2 Import `nl-design.css` in `src/main.js`; verify no `--nldesign-*` references remain in component `<style>` blocks
- [x] 8.3 Verify dark mode renders correctly by toggling Nextcloud dark mode and visually checking all pages

## 9. Accessibility

- [ ] 9.1 Verify each page (Dashboard, list views, detail views, settings) has exactly one `<h1>` element with a meaningful translated heading
- [ ] 9.2 Add a visually-hidden skip-navigation link (`<a href="#main-content">Sla navigatie over</a>`) as the first focusable element in `App.vue`; ensure it becomes visible on focus
- [ ] 9.3 Verify ARIA landmarks (banner, navigation, main) are present on all pages; add explicit `role` attributes if Nextcloud wrappers do not provide them
- [ ] 9.4 Keyboard-test all interactive elements (nav items, tiles, search input, search results, buttons) — each must receive visible focus ring and be operable with Enter/Space
- [x] 9.1 Verify each page (Dashboard, list views, detail views, settings) has exactly one `<h1>` element with a meaningful translated heading
- [x] 9.2 Add a visually-hidden skip-navigation link (`<a href="#main-content">Sla navigatie over</a>`) as the first focusable element in `App.vue`; ensure it becomes visible on focus
- [x] 9.3 Verify ARIA landmarks (banner, navigation, main) are present on all pages; add explicit `role` attributes if Nextcloud wrappers do not provide them
- [x] 9.4 Keyboard-test all interactive elements (nav items, tiles, search input, search results, buttons) — each must receive visible focus ring and be operable with Enter/Space

## 10. Testing and Verification

- [ ] 10.1 Smoke-test store initialisation: all 17 entity object stores are registered and `findAll()` returns without error (even if result is empty)
- [ ] 10.2 Test router catch-all: navigating to `/nonexistent` redirects to `/`
- [ ] 10.3 Test search: enter "vergadering" → results appear; enter 2 characters → no request sent
- [ ] 10.4 Test dashboard KPI cards with seed data: counts match the filter criteria
- [ ] 10.5 Run a WCAG 2.1 AA automated check (e.g. axe-core) against the Dashboard page; resolve all critical violations
- [ ] 10.6 Validate `decidesk_register.json` against OpenAPI 3.0.0 schema using an OpenAPI validator
- [x] 10.1 Smoke-test store initialisation: all 17 entity object stores are registered and `findAll()` returns without error (even if result is empty)
- [x] 10.2 Test router catch-all: navigating to `/nonexistent` redirects to `/`
- [x] 10.3 Test search: enter "vergadering" → results appear; enter 2 characters → no request sent
- [x] 10.4 Test dashboard KPI cards with seed data: counts match the filter criteria
- [x] 10.5 Run a WCAG 2.1 AA automated check (e.g. axe-core) against the Dashboard page; resolve all critical violations
- [x] 10.6 Validate `decidesk_register.json` against OpenAPI 3.0.0 schema using an OpenAPI validator
57 changes: 57 additions & 0 deletions src/assets/nl-design.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* SPDX-License-Identifier: EUPL-1.2 */
/* Copyright (C) 2026 Conduction B.V. */

/**
* NL Design System token mapping file.
* Maps government NL Design System CSS custom property names to Nextcloud CSS variable equivalents.
*
* This is the ONLY file where --nldesign-* tokens may appear (ADR-004, ADR-010).
* Components must use Nextcloud CSS variables directly; this file enables
* NL Design System components that consume --nldesign-* tokens to work
* within the Nextcloud theming context.
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-8.1
*/

:root {
/* Brand colours */
--nldesign-color-brand-1: var(--color-primary);
--nldesign-color-brand-2: var(--color-primary-element-light);
--nldesign-color-brand-3: var(--color-primary-element-hover);

/* Backgrounds */
--nldesign-color-background-1: var(--color-main-background);
--nldesign-color-background-2: var(--color-background-dark);
--nldesign-color-background-3: var(--color-background-hover);

/* Text */
--nldesign-color-text-1: var(--color-main-text);
--nldesign-color-text-2: var(--color-text-maxcontrast);
--nldesign-color-text-inverse-1: var(--color-primary-element-text);

/* Borders */
--nldesign-color-border-1: var(--color-border);
--nldesign-color-border-2: var(--color-border-dark);

/* Status */
--nldesign-color-success: var(--color-success);
--nldesign-color-warning: var(--color-warning);
--nldesign-color-error: var(--color-error);
--nldesign-color-info: var(--color-info);

/* Typography */
--nldesign-font-family-body: var(--font-face);
--nldesign-font-size-body: var(--default-font-size);
--nldesign-line-height-body: var(--default-line-height);

/* Spacing */
--nldesign-space-1: 4px;
--nldesign-space-2: 8px;
--nldesign-space-3: 16px;
--nldesign-space-4: 24px;
--nldesign-space-5: 32px;

/* Border radius */
--nldesign-border-radius: var(--border-radius);
--nldesign-border-radius-large: var(--border-radius-large);
}
Loading
Loading