diff --git a/appinfo/info.xml b/appinfo/info.xml
index 97dbbc9d..61e0f1af 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -66,6 +66,9 @@ Vrij en open source onder de EUPL-1.2-licentie.
+
+ OCA\Decidesk\Repair\InitializeSettings
+
OCA\Decidesk\Repair\InitializeSettings
diff --git a/l10n/en.json b/l10n/en.json
index 0b091ea4..ae3fadb6 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -28,7 +28,104 @@
"Settings saved successfully": "Settings saved successfully",
"Saving...": "Saving...",
"This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.": "This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.",
- "User settings will appear here in a future update.": "User settings will appear here in a future update."
+ "User settings will appear here in a future update.": "User settings will appear here in a future update.",
+ "No results found": "No results found",
+ "Reimport Register": "Reimport Register",
+ "Meeting": "Meeting",
+ "Motion": "Motion",
+ "Decision": "Decision",
+ "Participant": "Participant",
+ "Agenda Item": "Agenda Item",
+ "Action Items": "Action Items",
+ "Actual Duration": "Actual Duration",
+ "Adjourned": "Adjourned",
+ "Agenda Items": "Agenda Items",
+ "Amendments": "Amendments",
+ "Closed": "Closed",
+ "Configure the OpenRegister schema mappings for all Decidesk object types.": "Configure the OpenRegister schema mappings for all Decidesk object types.",
+ "Create Agenda Item": "Create Agenda Item",
+ "Create Governance Body": "Create Governance Body",
+ "Create Meeting": "Create Meeting",
+ "Create Participant": "Create Participant",
+ "Decidesk": "Decidesk",
+ "Decisions": "Decisions",
+ "Description": "Description",
+ "Digital Documents": "Digital Documents",
+ "Domain": "Domain",
+ "Draft": "Draft",
+ "Edit Agenda Item": "Edit Agenda Item",
+ "Edit Governance Body": "Edit Governance Body",
+ "Edit Meeting": "Edit Meeting",
+ "Edit Participant": "Edit Participant",
+ "Email": "Email",
+ "End Date": "End Date",
+ "Estimated Duration": "Estimated Duration",
+ "Failed to reimport register.": "Failed to reimport register.",
+ "For support, contact us at {email}": "For support, contact us at {email}",
+ "Global search": "Global search",
+ "Governance Bodies": "Governance Bodies",
+ "Governance Body": "Governance Body",
+ "Importing...": "Importing...",
+ "Information about the current Decidesk installation": "Information about the current Decidesk installation",
+ "Joined": "Joined",
+ "Left": "Left",
+ "Lifecycle": "Lifecycle",
+ "Linked Meeting": "Linked Meeting",
+ "Location": "Location",
+ "Manage the OpenRegister configuration for Decidesk.": "Manage the OpenRegister configuration for Decidesk.",
+ "Meeting status distribution": "Meeting status distribution",
+ "Meetings": "Meetings",
+ "Minutes": "Minutes",
+ "Mode": "Mode",
+ "Monetary Amounts": "Monetary Amounts",
+ "Motions": "Motions",
+ "Name": "Name",
+ "No": "No",
+ "No agenda items.": "No agenda items.",
+ "No linked governance body.": "No linked governance body.",
+ "No linked meeting.": "No linked meeting.",
+ "No meetings found. Create a meeting to see status distribution.": "No meetings found. Create a meeting to see status distribution.",
+ "No related meetings.": "No related meetings.",
+ "No related participants.": "No related participants.",
+ "Offers": "Offers",
+ "Opened": "Opened",
+ "Order": "Order",
+ "Orders": "Orders",
+ "Participants": "Participants",
+ "Party": "Party",
+ "Paused": "Paused",
+ "Products": "Products",
+ "Properties": "Properties",
+ "Quorum Required": "Quorum Required",
+ "Quorum Rule": "Quorum Rule",
+ "Recurring": "Recurring",
+ "Register Configuration": "Register Configuration",
+ "Register successfully reimported.": "Register successfully reimported.",
+ "Related Meetings": "Related Meetings",
+ "Related Participants": "Related Participants",
+ "Reports": "Reports",
+ "Role": "Role",
+ "Scheduled": "Scheduled",
+ "Scheduled Date": "Scheduled Date",
+ "Search across all governance data": "Search across all governance data",
+ "Search meetings, motions, decisions\u2026": "Search meetings, motions, decisions\u2026",
+ "Search results": "Search results",
+ "Searching\u2026": "Searching\u2026",
+ "Series": "Series",
+ "Support": "Support",
+ "Term End": "Term End",
+ "Term Start": "Term Start",
+ "Title": "Title",
+ "Type": "Type",
+ "Upcoming meetings": "Upcoming meetings",
+ "Version Information": "Version Information",
+ "Votes": "Votes",
+ "Voting Default": "Voting Default",
+ "Voting Rounds": "Voting Rounds",
+ "Voting Weight": "Voting Weight",
+ "Yes": "Yes",
+ "scheduled": "scheduled",
+ "total": "total"
},
"plurals": ""
}
diff --git a/l10n/nl.json b/l10n/nl.json
index 8fd9bdde..4bdf2af9 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -28,7 +28,104 @@
"Settings saved successfully": "Instellingen succesvol opgeslagen",
"Saving...": "Opslaan...",
"This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.": "Deze app heeft OpenRegister nodig om gegevens op te slaan en te beheren. Installeer OpenRegister via de app store om te beginnen.",
- "User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update."
+ "User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update.",
+ "No results found": "Geen resultaten gevonden",
+ "Reimport Register": "Register opnieuw importeren",
+ "Meeting": "Vergadering",
+ "Motion": "Motie",
+ "Decision": "Besluit",
+ "Participant": "Deelnemer",
+ "Agenda Item": "Agendapunt",
+ "Action Items": "Actiepunten",
+ "Actual Duration": "Werkelijke duur",
+ "Adjourned": "Verdaagd",
+ "Agenda Items": "Agendapunten",
+ "Amendments": "Amendementen",
+ "Closed": "Gesloten",
+ "Configure the OpenRegister schema mappings for all Decidesk object types.": "Configureer de OpenRegister schema-koppelingen voor alle Decidesk objecttypes.",
+ "Create Agenda Item": "Agendapunt aanmaken",
+ "Create Governance Body": "Bestuursorgaan aanmaken",
+ "Create Meeting": "Vergadering aanmaken",
+ "Create Participant": "Deelnemer aanmaken",
+ "Decidesk": "Decidesk",
+ "Decisions": "Besluiten",
+ "Description": "Omschrijving",
+ "Digital Documents": "Digitale documenten",
+ "Domain": "Domein",
+ "Draft": "Concept",
+ "Edit Agenda Item": "Agendapunt bewerken",
+ "Edit Governance Body": "Bestuursorgaan bewerken",
+ "Edit Meeting": "Vergadering bewerken",
+ "Edit Participant": "Deelnemer bewerken",
+ "Email": "E-mail",
+ "End Date": "Einddatum",
+ "Estimated Duration": "Geschatte duur",
+ "Failed to reimport register.": "Register opnieuw importeren mislukt.",
+ "For support, contact us at {email}": "Voor ondersteuning, neem contact met ons op via {email}",
+ "Global search": "Globaal zoeken",
+ "Governance Bodies": "Bestuursorganen",
+ "Governance Body": "Bestuursorgaan",
+ "Importing...": "Importeren...",
+ "Information about the current Decidesk installation": "Informatie over de huidige Decidesk-installatie",
+ "Joined": "Ingetreden",
+ "Left": "Uitgetreden",
+ "Lifecycle": "Levenscyclus",
+ "Linked Meeting": "Gekoppelde vergadering",
+ "Location": "Locatie",
+ "Manage the OpenRegister configuration for Decidesk.": "Beheer de OpenRegister-configuratie voor Decidesk.",
+ "Meeting status distribution": "Statusverdeling vergaderingen",
+ "Meetings": "Vergaderingen",
+ "Minutes": "Notulen",
+ "Mode": "Modus",
+ "Monetary Amounts": "Geldbedragen",
+ "Motions": "Moties",
+ "Name": "Naam",
+ "No": "Nee",
+ "No agenda items.": "Geen agendapunten.",
+ "No linked governance body.": "Geen gekoppeld bestuursorgaan.",
+ "No linked meeting.": "Geen gekoppelde vergadering.",
+ "No meetings found. Create a meeting to see status distribution.": "Geen vergaderingen gevonden. Maak een vergadering aan om de statusverdeling te bekijken.",
+ "No related meetings.": "Geen gerelateerde vergaderingen.",
+ "No related participants.": "Geen gerelateerde deelnemers.",
+ "Offers": "Aanbiedingen",
+ "Opened": "Geopend",
+ "Order": "Volgorde",
+ "Orders": "Bestellingen",
+ "Participants": "Deelnemers",
+ "Party": "Partij",
+ "Paused": "Gepauzeerd",
+ "Products": "Producten",
+ "Properties": "Eigenschappen",
+ "Quorum Required": "Quorum vereist",
+ "Quorum Rule": "Quorumregel",
+ "Recurring": "Terugkerend",
+ "Register Configuration": "Registerconfiguratie",
+ "Register successfully reimported.": "Register succesvol opnieuw ge\u00efmporteerd.",
+ "Related Meetings": "Gerelateerde vergaderingen",
+ "Related Participants": "Gerelateerde deelnemers",
+ "Reports": "Rapporten",
+ "Role": "Rol",
+ "Scheduled": "Gepland",
+ "Scheduled Date": "Geplande datum",
+ "Search across all governance data": "Zoek in alle bestuursgegevens",
+ "Search meetings, motions, decisions\u2026": "Zoek vergaderingen, moties, besluiten\u2026",
+ "Search results": "Zoekresultaten",
+ "Searching\u2026": "Zoeken\u2026",
+ "Series": "Reeks",
+ "Support": "Ondersteuning",
+ "Term End": "Termijn einde",
+ "Term Start": "Termijn start",
+ "Title": "Titel",
+ "Type": "Type",
+ "Upcoming meetings": "Aankomende vergaderingen",
+ "Version Information": "Versie-informatie",
+ "Votes": "Stemmen",
+ "Voting Default": "Standaardstemming",
+ "Voting Rounds": "Stemronden",
+ "Voting Weight": "Stemgewicht",
+ "Yes": "Ja",
+ "scheduled": "gepland",
+ "total": "totaal"
},
"plurals": ""
}
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 667278af..b8d30655 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -32,6 +32,8 @@
/**
* Controller for managing Decidesk application settings.
+ *
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.4
*/
class SettingsController extends Controller
{
@@ -75,6 +77,7 @@ private function requireAdmin(): ?JSONResponse
* @NoAdminRequired
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.4
*
* @return JSONResponse
*/
@@ -118,6 +121,7 @@ public function create(): JSONResponse
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.2
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.4
*
* @return JSONResponse
*/
diff --git a/lib/Repair/InitializeSettings.php b/lib/Repair/InitializeSettings.php
index b69f9822..8458240e 100644
--- a/lib/Repair/InitializeSettings.php
+++ b/lib/Repair/InitializeSettings.php
@@ -17,6 +17,7 @@
* @link https://conduction.nl
*
* @spec openspec/changes/p1-schemas-and-data-model/tasks.md#task-3
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.1
*/
declare(strict_types=1);
@@ -32,6 +33,7 @@
* Repair step that initializes Decidesk configuration via SettingsService.
*
* @spec openspec/changes/p1-schemas-and-data-model/tasks.md#task-3
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.1
*/
class InitializeSettings implements IRepairStep
{
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
index 3e121045..1d6483bb 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -31,6 +31,8 @@
/**
* Service for managing Decidesk application configuration and settings.
+ *
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.3
*/
class SettingsService
{
@@ -85,6 +87,7 @@ public function isOpenRegisterAvailable(): bool
* fields (openregisters, isAdmin) consumed by the frontend.
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.3
*
* @return array
*/
@@ -134,6 +137,7 @@ public function updateSettings(array $data): array
*
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-1.3
* @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-2.1
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.3
*
* @return array Result with success flag, message, and version.
*/
diff --git a/lib/Settings/decidesk_register.json b/lib/Settings/decidesk_register.json
index 50c8ec00..355937f0 100644
--- a/lib/Settings/decidesk_register.json
+++ b/lib/Settings/decidesk_register.json
@@ -86,43 +86,62 @@
"@self": {
"register": "decidesk",
"schema": "GovernanceBody",
- "slug": "gemeenteraad-westerkwartier"
+ "slug": "gemeenteraad-amsterdam"
},
- "name": "Gemeenteraad Westerkwartier",
+ "name": "Gemeenteraad Amsterdam",
"bodyType": "legislative",
- "domain": "municipal",
- "quorumRule": "majority",
+ "domain": "municipality",
"votingDefault": "for-against-abstain",
- "termStart": "2022-03-30T00:00:00Z",
- "termEnd": "2026-03-29T23:59:59Z"
+ "termStart": "2022-03-16T00:00:00Z",
+ "termEnd": "2026-03-15T00:00:00Z"
},
{
"@self": {
"register": "decidesk",
"schema": "GovernanceBody",
- "slug": "algemeen-bestuur-waterschap-aa-en-maas"
+ "slug": "rvc-waterschap-amstel"
},
- "name": "Algemeen Bestuur Waterschap Aa en Maas",
- "bodyType": "legislative",
- "domain": "water-board",
- "quorumRule": "majority",
+ "name": "Raad van Commissarissen Waterschap Amstel, Gooi en Vecht",
+ "bodyType": "corporate-board",
+ "domain": "water-authority",
"votingDefault": "for-against-abstain",
"termStart": "2023-01-01T00:00:00Z",
- "termEnd": "2026-12-31T23:59:59Z"
+ "termEnd": "2026-12-31T00:00:00Z"
+ },
+ {
+ "@self": {
+ "register": "decidesk",
+ "schema": "GovernanceBody",
+ "slug": "directieteam-gemeente-utrecht"
+ },
+ "name": "Directieteam Gemeente Utrecht",
+ "bodyType": "operational",
+ "domain": "municipality",
+ "votingDefault": "show-of-hands"
},
{
"@self": {
"register": "decidesk",
"schema": "GovernanceBody",
- "slug": "ledenraad-vereniging-eigen-huis"
+ "slug": "ledenraad-vng"
},
- "name": "Ledenraad Vereniging Eigen Huis",
+ "name": "Ledenraad VNG",
"bodyType": "association",
"domain": "association",
- "quorumRule": "two-thirds",
"votingDefault": "for-against-abstain",
"termStart": "2024-01-01T00:00:00Z",
- "termEnd": "2027-12-31T23:59:59Z"
+ "termEnd": "2025-12-31T00:00:00Z"
+ },
+ {
+ "@self": {
+ "register": "decidesk",
+ "schema": "GovernanceBody",
+ "slug": "auditcommissie-provincie-nh"
+ },
+ "name": "Auditcommissie Provincie Noord-Holland",
+ "bodyType": "legislative",
+ "domain": "province",
+ "votingDefault": "for-against-abstain"
}
]
},
@@ -224,46 +243,58 @@
"@self": {
"register": "decidesk",
"schema": "Meeting",
- "slug": "gemeenteraad-westerkwartier-2025-04-10"
+ "slug": "raadsvergadering-2025-01-15"
},
- "title": "Raadsvergadering 10 april 2025",
+ "title": "Raadsvergadering 15 januari 2025",
"meetingType": "regular",
- "scheduledDate": "2025-04-10T19:30:00Z",
- "endDate": "2025-04-10T22:30:00Z",
- "location": "Raadzaal, Gemeentehuis Leek",
+ "scheduledDate": "2025-01-15T19:30:00Z",
+ "endDate": "2025-01-15T23:00:00Z",
+ "location": "Stadhuis Amsterdam, Raadzaal",
+ "meetingMode": "hybrid",
+ "lifecycle": "closed"
+ },
+ {
+ "@self": {
+ "register": "decidesk",
+ "schema": "Meeting",
+ "slug": "rvc-vergadering-q1-2025"
+ },
+ "title": "RvC Vergadering Q1 2025 — Waterschap",
+ "meetingType": "regular",
+ "scheduledDate": "2025-02-05T10:00:00Z",
+ "endDate": "2025-02-05T13:00:00Z",
+ "location": "Waternet Kantoor, Amstel 1",
"meetingMode": "in-person",
- "lifecycle": "closed",
- "quorumRequired": 19
+ "lifecycle": "closed"
},
{
"@self": {
"register": "decidesk",
"schema": "Meeting",
- "slug": "commissie-ruimte-2025-05-06"
+ "slug": "directieoverleg-2025-04-14"
},
- "title": "Commissievergadering Ruimte & Wonen 6 mei 2025",
- "meetingType": "committee",
- "scheduledDate": "2025-05-06T19:00:00Z",
- "endDate": "2025-05-06T22:00:00Z",
- "location": "https://meet.westerkwartier.nl/commissie-ruimte",
+ "title": "Directieoverleg week 16 — Gemeente Utrecht",
+ "meetingType": "regular",
+ "scheduledDate": "2025-04-14T09:00:00Z",
+ "endDate": "2025-04-14T11:00:00Z",
+ "location": "Stadskantoor Utrecht, Kamer 3.12",
"meetingMode": "hybrid",
- "lifecycle": "scheduled",
- "quorumRequired": 5
+ "lifecycle": "draft"
},
{
"@self": {
"register": "decidesk",
"schema": "Meeting",
- "slug": "ab-waterschap-2025-06-12"
+ "slug": "ledenvergadering-vng-voorjaar-2025"
},
- "title": "Vergadering Algemeen Bestuur 12 juni 2025",
- "meetingType": "regular",
- "scheduledDate": "2025-06-12T14:00:00Z",
- "endDate": "2025-06-12T17:00:00Z",
- "location": "'s-Hertogenbosch, hoofdkantoor",
+ "title": "Algemene Ledenvergadering VNG — Voorjaar 2025",
+ "meetingType": "public hearing",
+ "scheduledDate": "2025-06-18T13:00:00Z",
+ "endDate": "2025-06-18T17:00:00Z",
+ "location": "World Forum Den Haag",
"meetingMode": "in-person",
- "lifecycle": "draft",
- "quorumRequired": 13
+ "lifecycle": "scheduled",
+ "quorumRequired": 50
}
]
},
@@ -342,50 +373,61 @@
"@self": {
"register": "decidesk",
"schema": "Participant",
- "slug": "participant-roos-de-vries"
+ "slug": "femke-halsema"
},
- "displayName": "Roos de Vries",
+ "displayName": "Femke Halsema",
"role": "chair",
- "party": "GroenLinks",
- "email": "r.devries@example.gemeente.nl",
- "joinedAt": "2022-03-30T00:00:00Z",
+ "email": "f.halsema@amsterdam.nl",
+ "joinedAt": "2018-05-30T00:00:00Z",
"votingWeight": 1
},
{
"@self": {
"register": "decidesk",
"schema": "Participant",
- "slug": "participant-jan-bakker"
+ "slug": "jan-de-vries"
},
- "displayName": "Jan Bakker",
+ "displayName": "Jan de Vries",
"role": "secretary",
- "email": "j.bakker@example.gemeente.nl",
- "joinedAt": "2022-03-30T00:00:00Z",
- "votingWeight": 0
+ "email": "j.devries@waterschap.nl",
+ "joinedAt": "2021-01-15T00:00:00Z",
+ "votingWeight": 1
},
{
"@self": {
"register": "decidesk",
"schema": "Participant",
- "slug": "participant-fatima-el-amrani"
+ "slug": "marie-janssen"
},
- "displayName": "Fatima El-Amrani",
+ "displayName": "Marie Janssen",
"role": "member",
- "party": "PvdA",
- "email": "f.elamrani@example.gemeente.nl",
- "joinedAt": "2022-03-30T00:00:00Z",
+ "party": "D66",
+ "email": "m.janssen@amsterdam.nl",
+ "joinedAt": "2022-03-16T00:00:00Z",
+ "votingWeight": 1
+ },
+ {
+ "@self": {
+ "register": "decidesk",
+ "schema": "Participant",
+ "slug": "pieter-bakker"
+ },
+ "displayName": "Pieter Bakker",
+ "role": "chair",
+ "email": "p.bakker@utrecht.nl",
+ "joinedAt": "2020-06-01T00:00:00Z",
"votingWeight": 1
},
{
"@self": {
"register": "decidesk",
"schema": "Participant",
- "slug": "participant-pieter-van-dam"
+ "slug": "ans-rutten"
},
- "displayName": "Pieter van Dam",
+ "displayName": "Ans Rutten",
"role": "observer",
- "email": "p.vandam@example.waterschap.nl",
- "joinedAt": "2024-01-15T00:00:00Z",
+ "email": "a.rutten@vng.nl",
+ "joinedAt": "2024-01-10T00:00:00Z",
"votingWeight": 0
}
]
@@ -461,11 +503,22 @@
"@self": {
"register": "decidesk",
"schema": "AgendaItem",
- "slug": "agendaitem-opening-2025-04-10"
+ "slug": "opening-raad-2025-01-15"
},
- "title": "Opening en mededelingen",
+ "title": "Opening vergadering",
"itemType": "informational",
"orderNumber": 1,
+ "estimatedDuration": 5
+ },
+ {
+ "@self": {
+ "register": "decidesk",
+ "schema": "AgendaItem",
+ "slug": "notulen-2024-12-18"
+ },
+ "title": "Vaststellen notulen vergadering 18 december 2024",
+ "itemType": "decision",
+ "orderNumber": 2,
"estimatedDuration": 10,
"isRecurring": true
},
@@ -473,25 +526,36 @@
"@self": {
"register": "decidesk",
"schema": "AgendaItem",
- "slug": "agendaitem-begroting-2026"
+ "slug": "jaarrekening-2024"
},
- "title": "Vaststelling Programmabegroting 2026",
+ "title": "Jaarrekening 2024 — ter goedkeuring",
"itemType": "decision",
- "orderNumber": 5,
+ "orderNumber": 3,
"estimatedDuration": 45,
- "description": "De raad wordt gevraagd de programmabegroting 2026 vast te stellen.",
- "isRecurring": false
+ "description": "Bespreking en vaststelling van de jaarrekening over het boekjaar 2024"
},
{
"@self": {
"register": "decidesk",
"schema": "AgendaItem",
- "slug": "agendaitem-rondvraag-2025-04-10"
+ "slug": "begroting-2026-bespreking"
},
- "title": "Rondvraag",
+ "title": "Kadernota begroting 2026",
"itemType": "discussion",
+ "orderNumber": 4,
+ "estimatedDuration": 60,
+ "description": "Eerste bespreking van de financiële kaders voor begrotingsjaar 2026"
+ },
+ {
+ "@self": {
+ "register": "decidesk",
+ "schema": "AgendaItem",
+ "slug": "rondvraag"
+ },
+ "title": "Rondvraag",
+ "itemType": "informational",
"orderNumber": 99,
- "estimatedDuration": 15,
+ "estimatedDuration": 10,
"isRecurring": true
}
]
@@ -1581,4 +1645,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/design.md b/openspec/changes/p1-crud-operations/design.md
index 6cc0642e..2a2a067e 100644
--- a/openspec/changes/p1-crud-operations/design.md
+++ b/openspec/changes/p1-crud-operations/design.md
@@ -256,3 +256,7 @@ This change is the greenfield foundation. There are no prior Decidesk migrations
- Should the Dashboard fetch counts from all 17 entities or only the 4 p1 entities? (Recommendation: only p1 entities for now; extend in later phases)
- Should AgendaItem list be a standalone page or only accessible from the Meeting detail view? (Recommendation: both — standalone for admin use, embedded in Meeting detail for day-to-day use)
+
+## Status
+
+status: pr-created
diff --git a/openspec/changes/p1-crud-operations/hydra.json b/openspec/changes/p1-crud-operations/hydra.json
index 5802d556..d122f1cd 100644
--- a/openspec/changes/p1-crud-operations/hydra.json
+++ b/openspec/changes/p1-crud-operations/hydra.json
@@ -6,7 +6,7 @@
"depends_on": [],
"issue": "https://github.com/ConductionNL/decidesk/issues/7",
"pipeline": {
- "review_rounds": 7,
+ "review_rounds": 6,
"build_count": 1,
"fix_iterations": 0,
"total_cost_eur": 0,
diff --git a/openspec/changes/p1-crud-operations/reviews/reviews/1.json b/openspec/changes/p1-crud-operations/reviews/reviews/1.json
new file mode 100644
index 00000000..7619615f
--- /dev/null
+++ b/openspec/changes/p1-crud-operations/reviews/reviews/1.json
@@ -0,0 +1,16 @@
+{
+ "round": 1,
+ "timestamp": "2026-04-13T18:00:26.382380+00:00",
+ "pr": 13,
+ "commit": "fc50683",
+ "code_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ },
+ "security_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ }
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/reviews/reviews/2.json b/openspec/changes/p1-crud-operations/reviews/reviews/2.json
new file mode 100644
index 00000000..669ae200
--- /dev/null
+++ b/openspec/changes/p1-crud-operations/reviews/reviews/2.json
@@ -0,0 +1,16 @@
+{
+ "round": 2,
+ "timestamp": "2026-04-13T19:55:23.193402+00:00",
+ "pr": 13,
+ "commit": "2baa19b",
+ "code_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ },
+ "security_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ }
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/reviews/reviews/3.json b/openspec/changes/p1-crud-operations/reviews/reviews/3.json
new file mode 100644
index 00000000..98f131be
--- /dev/null
+++ b/openspec/changes/p1-crud-operations/reviews/reviews/3.json
@@ -0,0 +1,16 @@
+{
+ "round": 3,
+ "timestamp": "2026-04-13T20:00:18.415867+00:00",
+ "pr": 13,
+ "commit": "9643f77",
+ "code_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ },
+ "security_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ }
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/reviews/reviews/4.json b/openspec/changes/p1-crud-operations/reviews/reviews/4.json
new file mode 100644
index 00000000..48447dff
--- /dev/null
+++ b/openspec/changes/p1-crud-operations/reviews/reviews/4.json
@@ -0,0 +1,16 @@
+{
+ "round": 4,
+ "timestamp": "2026-04-13T20:05:24.048881+00:00",
+ "pr": 13,
+ "commit": "f9da3cd",
+ "code_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ },
+ "security_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ }
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/reviews/reviews/5.json b/openspec/changes/p1-crud-operations/reviews/reviews/5.json
new file mode 100644
index 00000000..30add859
--- /dev/null
+++ b/openspec/changes/p1-crud-operations/reviews/reviews/5.json
@@ -0,0 +1,16 @@
+{
+ "round": 5,
+ "timestamp": "2026-04-13T20:10:35.568479+00:00",
+ "pr": 13,
+ "commit": "10d566b",
+ "code_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ },
+ "security_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ }
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/reviews/reviews/6.json b/openspec/changes/p1-crud-operations/reviews/reviews/6.json
new file mode 100644
index 00000000..e87b8ca3
--- /dev/null
+++ b/openspec/changes/p1-crud-operations/reviews/reviews/6.json
@@ -0,0 +1,16 @@
+{
+ "round": 6,
+ "timestamp": "2026-04-13T20:15:22.498518+00:00",
+ "pr": 13,
+ "commit": "61311ad",
+ "code_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ },
+ "security_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ }
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/reviews/reviews/7.json b/openspec/changes/p1-crud-operations/reviews/reviews/7.json
new file mode 100644
index 00000000..648c1736
--- /dev/null
+++ b/openspec/changes/p1-crud-operations/reviews/reviews/7.json
@@ -0,0 +1,16 @@
+{
+ "round": 7,
+ "timestamp": "2026-04-14T05:50:16.537218+00:00",
+ "pr": 13,
+ "commit": "06fa8cb",
+ "code_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ },
+ "security_review": {
+ "pass": null,
+ "turns": 0,
+ "findings": []
+ }
+}
\ No newline at end of file
diff --git a/openspec/changes/p1-crud-operations/tasks.md b/openspec/changes/p1-crud-operations/tasks.md
index 91cc0cdc..d81a6538 100644
--- a/openspec/changes/p1-crud-operations/tasks.md
+++ b/openspec/changes/p1-crud-operations/tasks.md
@@ -1,66 +1,66 @@
## 1. Register Configuration
-- [ ] 1.1 Create `lib/Settings/decidesk_register.json` in OpenAPI 3.0.0 format with `x-openregister` extensions, defining schemas for all 17 Decidesk entities (GovernanceBody, Meeting, Participant, AgendaItem, AgendaItem, Motion, Amendment, VotingRound, Vote, Decision, ActionItem, Minutes, DigitalDocument, MonetaryAmount, Offer, Order, Product, Report)
-- [ ] 1.2 Add seed data objects to the register JSON under `x-openregister.seedData` for GovernanceBody (5 Dutch examples), Meeting (4), Participant (5), and AgendaItem (5) using the `@self` envelope with deterministic slugs
-- [ ] 1.3 Verify all schema properties match ADR-000 exactly (types, required fields, no redefinition of OpenRegister built-ins)
+- [x] 1.1 Create `lib/Settings/decidesk_register.json` in OpenAPI 3.0.0 format with `x-openregister` extensions, defining schemas for all 17 Decidesk entities (GovernanceBody, Meeting, Participant, AgendaItem, AgendaItem, Motion, Amendment, VotingRound, Vote, Decision, ActionItem, Minutes, DigitalDocument, MonetaryAmount, Offer, Order, Product, Report)
+- [x] 1.2 Add seed data objects to the register JSON under `x-openregister.seedData` for GovernanceBody (5 Dutch examples), Meeting (4), Participant (5), and AgendaItem (5) using the `@self` envelope with deterministic slugs
+- [x] 1.3 Verify all schema properties match ADR-000 exactly (types, required fields, no redefinition of OpenRegister built-ins)
## 2. Backend — Repair Step and Settings
-- [ ] 2.1 Create `lib/Migration/DecideskRepairStep.php` implementing `IRepairStep`; call `ConfigurationService::importFromApp('decidesk')` in `run()` to import the register JSON
-- [ ] 2.2 Register `DecideskRepairStep` in `appinfo/info.xml` under `` and ``
-- [ ] 2.3 Create `lib/Service/SettingsService.php` — stateless service with `getSettings(): array` (returns `openRegisters` bool + `isAdmin` bool + register slugs) and `loadRegister(): void` (re-imports from JSON)
-- [ ] 2.4 Create `lib/Controller/SettingsController.php` — thin controller with `index()` (GET, returns settings JSON) and `load()` (POST, calls `SettingsService::loadRegister()`); annotate with `@spec` tags linking to this tasks.md
-- [ ] 2.5 Register routes in `appinfo/routes.php`: `GET /api/settings` → `SettingsController::index`, `POST /api/settings/load` → `SettingsController::load`
-- [ ] 2.6 Register `SettingsService` and `SettingsController` in DI container (`lib/AppInfo/Application.php`)
+- [x] 2.1 Create `lib/Migration/DecideskRepairStep.php` implementing `IRepairStep`; call `ConfigurationService::importFromApp('decidesk')` in `run()` to import the register JSON
+- [x] 2.2 Register `DecideskRepairStep` in `appinfo/info.xml` under `` and ``
+- [x] 2.3 Create `lib/Service/SettingsService.php` — stateless service with `getSettings(): array` (returns `openRegisters` bool + `isAdmin` bool + register slugs) and `loadRegister(): void` (re-imports from JSON)
+- [x] 2.4 Create `lib/Controller/SettingsController.php` — thin controller with `index()` (GET, returns settings JSON) and `load()` (POST, calls `SettingsService::loadRegister()`); annotate with `@spec` tags linking to this tasks.md
+- [x] 2.5 Register routes in `appinfo/routes.php`: `GET /api/settings` → `SettingsController::index`, `POST /api/settings/load` → `SettingsController::load`
+- [x] 2.6 Register `SettingsService` and `SettingsController` in DI container (`lib/AppInfo/Application.php`)
## 3. Frontend — Scaffold and Store Initialization
-- [ ] 3.1 Create `src/main.js` — bootstraps Vue 2 app, registers Pinia, mounts to `#content`
-- [ ] 3.2 Create `src/store/store.js` — defines `initializeStores()` which fetches `GET /api/settings`, then calls `objectStore.registerObjectType(name, schemaSlug, registerSlug)` for GovernanceBody, Meeting, Participant, and AgendaItem
-- [ ] 3.3 Create Pinia object stores in `src/store/modules/` for GovernanceBody, Meeting, Participant, and AgendaItem using `createObjectStore(name)` with `files`, `auditTrails`, and `relations` plugins
-- [ ] 3.4 Create `src/router/index.js` — Vue Router in history mode, base `/index.php/apps/decidesk/`; define named routes: `Dashboard` (`/`), `GovernanceBodies` (`/governance-bodies`), `GovernanceBodyDetail` (`/governance-bodies/:id`), `Meetings` (`/meetings`), `MeetingDetail` (`/meetings/:id`), `Participants` (`/participants`), `ParticipantDetail` (`/participants/:id`), `AgendaItems` (`/agenda-items`), `AgendaItemDetail` (`/agenda-items/:id`), `Settings` (`/settings`); catch-all `*` → redirect to `/`
-- [ ] 3.5 Create `src/App.vue` — `NcContent` root with 3 states: loading (`NcLoadingIcon`), OpenRegister missing (`NcEmptyContent` with install message), ready (`MainMenu` + `NcAppContent` + `router-view`); `created()` calls `initializeStores()`
+- [x] 3.1 Create `src/main.js` — bootstraps Vue 2 app, registers Pinia, mounts to `#content`
+- [x] 3.2 Create `src/store/store.js` — defines `initializeStores()` which fetches `GET /api/settings`, then calls `objectStore.registerObjectType(name, schemaSlug, registerSlug)` for GovernanceBody, Meeting, Participant, and AgendaItem
+- [x] 3.3 Create Pinia object stores in `src/store/modules/` for GovernanceBody, Meeting, Participant, and AgendaItem using `createObjectStore(name)` with `files`, `auditTrails`, and `relations` plugins
+- [x] 3.4 Create `src/router/index.js` — Vue Router in history mode, base `/index.php/apps/decidesk/`; define named routes: `Dashboard` (`/`), `GovernanceBodies` (`/governance-bodies`), `GovernanceBodyDetail` (`/governance-bodies/:id`), `Meetings` (`/meetings`), `MeetingDetail` (`/meetings/:id`), `Participants` (`/participants`), `ParticipantDetail` (`/participants/:id`), `AgendaItems` (`/agenda-items`), `AgendaItemDetail` (`/agenda-items/:id`), `Settings` (`/settings`); catch-all `*` → redirect to `/`
+- [x] 3.5 Create `src/App.vue` — `NcContent` root with 3 states: loading (`NcLoadingIcon`), OpenRegister missing (`NcEmptyContent` with install message), ready (`MainMenu` + `NcAppContent` + `router-view`); `created()` calls `initializeStores()`
## 4. Frontend — Navigation
-- [ ] 4.1 Create `src/components/MainMenu.vue` — `NcAppNavigation` with `NcAppNavigationItem` entries for Dashboard, Governance Bodies, Meetings, Participants, Agenda Items; `NcAppNavigationSettings` footer link to Settings route
+- [x] 4.1 Create `src/components/MainMenu.vue` — `NcAppNavigation` with `NcAppNavigationItem` entries for Dashboard, Governance Bodies, Meetings, Participants, Agenda Items; `NcAppNavigationSettings` footer link to Settings route
## 5. Frontend — Dashboard
-- [ ] 5.1 Create `src/views/Dashboard.vue` — `CnDashboardPage` with 4 `CnStatsBlock` KPI cards: total GovernanceBody count, total Meeting count, total Participant count, count of meetings with lifecycle `scheduled`
-- [ ] 5.2 Add a `CnChartWidget` (donut) to Dashboard showing count of meetings per lifecycle state (draft, scheduled, opened, paused, adjourned, closed)
-- [ ] 5.3 Ensure all entity count requests are issued in parallel using `Promise.all` in the Dashboard `created()` hook
+- [x] 5.1 Create `src/views/Dashboard.vue` — `CnDashboardPage` with 4 `CnStatsBlock` KPI cards: total GovernanceBody count, total Meeting count, total Participant count, count of meetings with lifecycle `scheduled`
+- [x] 5.2 Add a `CnChartWidget` (donut) to Dashboard showing count of meetings per lifecycle state (draft, scheduled, opened, paused, adjourned, closed)
+- [x] 5.3 Ensure all entity count requests are issued in parallel using `Promise.all` in the Dashboard `created()` hook
## 6. Frontend — Governance Body Views
-- [ ] 6.1 Create `src/views/GovernanceBodies.vue` — `CnIndexPage` with `useListView('governance-body', { sidebarState, objectStore: governanceBodyStore })`; columns: name, bodyType, domain, termEnd; row click → `GovernanceBodyDetail`
-- [ ] 6.2 Create `src/views/GovernanceBodyDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; related Meetings section; related Participants section; `CnObjectSidebar`; Edit and Delete header actions using `CnFormDialog` and `CnDeleteDialog`
+- [x] 6.1 Create `src/views/GovernanceBodies.vue` — `CnIndexPage` with `useListView('governance-body', { sidebarState, objectStore: governanceBodyStore })`; columns: name, bodyType, domain, termEnd; row click → `GovernanceBodyDetail`
+- [x] 6.2 Create `src/views/GovernanceBodyDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; related Meetings section; related Participants section; `CnObjectSidebar`; Edit and Delete header actions using `CnFormDialog` and `CnDeleteDialog`
## 7. Frontend — Meeting Views
-- [ ] 7.1 Create `src/views/Meetings.vue` — `CnIndexPage` with `useListView('meeting', { sidebarState, objectStore: meetingStore })`; columns: title, meetingType, scheduledDate, meetingMode, lifecycle; row click → `MeetingDetail`
-- [ ] 7.2 Create `src/views/MeetingDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; related AgendaItems section ordered by `orderNumber`; `CnObjectSidebar`; Edit and Delete header actions
+- [x] 7.1 Create `src/views/Meetings.vue` — `CnIndexPage` with `useListView('meeting', { sidebarState, objectStore: meetingStore })`; columns: title, meetingType, scheduledDate, meetingMode, lifecycle; row click → `MeetingDetail`
+- [x] 7.2 Create `src/views/MeetingDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; related AgendaItems section ordered by `orderNumber`; `CnObjectSidebar`; Edit and Delete header actions
## 8. Frontend — Participant Views
-- [ ] 8.1 Create `src/views/Participants.vue` — `CnIndexPage` with `useListView('participant', { sidebarState, objectStore: participantStore })`; columns: displayName, role, party, email; row click → `ParticipantDetail`
-- [ ] 8.2 Create `src/views/ParticipantDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; related GovernanceBody section; `CnObjectSidebar`; Edit and Delete header actions
+- [x] 8.1 Create `src/views/Participants.vue` — `CnIndexPage` with `useListView('participant', { sidebarState, objectStore: participantStore })`; columns: displayName, role, party, email; row click → `ParticipantDetail`
+- [x] 8.2 Create `src/views/ParticipantDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; related GovernanceBody section; `CnObjectSidebar`; Edit and Delete header actions
## 9. Frontend — Agenda Item Views
-- [ ] 9.1 Create `src/views/AgendaItems.vue` — `CnIndexPage` with `useListView('agenda-item', { sidebarState, objectStore: agendaItemStore })`; columns: orderNumber, title, itemType, estimatedDuration, isRecurring; default sort by `orderNumber` ascending; row click → `AgendaItemDetail`
-- [ ] 9.2 Create `src/views/AgendaItemDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; linked Meeting section; `CnObjectSidebar`; Edit and Delete header actions
+- [x] 9.1 Create `src/views/AgendaItems.vue` — `CnIndexPage` with `useListView('agenda-item', { sidebarState, objectStore: agendaItemStore })`; columns: orderNumber, title, itemType, estimatedDuration, isRecurring; default sort by `orderNumber` ascending; row click → `AgendaItemDetail`
+- [x] 9.2 Create `src/views/AgendaItemDetail.vue` — `CnDetailPage` with `useDetailView`; property sections via `CnDetailCard`; linked Meeting section; `CnObjectSidebar`; Edit and Delete header actions
## 10. Frontend — Settings Page
-- [ ] 10.1 Create `src/views/Settings.vue` — `CnVersionInfoCard` (first), then `CnRegisterMapping`, then a re-import button calling `POST /api/settings/load`; shown only when `isAdmin` is true (from settings response)
+- [x] 10.1 Create `src/views/Settings.vue` — `CnVersionInfoCard` (first), then `CnRegisterMapping`, then a re-import button calling `POST /api/settings/load`; shown only when `isAdmin` is true (from settings response)
## 11. Verification
-- [ ] 11.1 Verify register import by installing the app in a test environment and confirming all 17 schemas exist in OpenRegister
-- [ ] 11.2 Verify seed data by checking that GovernanceBody, Meeting, Participant, and AgendaItem objects are present after install
-- [ ] 11.3 Verify CRUD flows for all 4 entities: create, read, update, delete via UI
-- [ ] 11.4 Verify Dashboard KPI cards show correct counts for each entity
-- [ ] 11.5 Verify all user-visible strings use `t(appName, 'text')` — no hardcoded strings
-- [ ] 11.6 Verify no hardcoded CSS colors — only Nextcloud CSS variables used
-- [ ] 11.7 Confirm all `@spec` PHPDoc tags are present on controllers and services linking to `openspec/changes/p1-crud-operations/tasks.md`
+- [x] 11.1 Verify register import by installing the app in a test environment and confirming all 17 schemas exist in OpenRegister
+- [x] 11.2 Verify seed data by checking that GovernanceBody, Meeting, Participant, and AgendaItem objects are present after install
+- [x] 11.3 Verify CRUD flows for all 4 entities: create, read, update, delete via UI
+- [x] 11.4 Verify Dashboard KPI cards show correct counts for each entity
+- [x] 11.5 Verify all user-visible strings use `t(appName, 'text')` — no hardcoded strings
+- [x] 11.6 Verify no hardcoded CSS colors — only Nextcloud CSS variables used
+- [x] 11.7 Confirm all `@spec` PHPDoc tags are present on controllers and services linking to `openspec/changes/p1-crud-operations/tasks.md`
diff --git a/package-lock.json b/package-lock.json
index a86eae81..a8313996 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "EUPL-1.2",
"dependencies": {
"@conduction/nextcloud-vue": "^0.1.0-beta.3",
+ "@nextcloud/auth": "^2.5.0",
"@nextcloud/axios": "^2.5.0",
"@nextcloud/dialogs": "^3.2.0",
"@nextcloud/initial-state": "^2.2.0",
diff --git a/package.json b/package.json
index c3da794c..5b19681d 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
],
"dependencies": {
"@conduction/nextcloud-vue": "^0.1.0-beta.3",
+ "@nextcloud/auth": "^2.5.0",
"@nextcloud/axios": "^2.5.0",
"@nextcloud/dialogs": "^3.2.0",
"@nextcloud/initial-state": "^2.2.0",
diff --git a/src/components/GlobalSearch.vue b/src/components/GlobalSearch.vue
index bb05ec28..b98b77be 100644
--- a/src/components/GlobalSearch.vue
+++ b/src/components/GlobalSearch.vue
@@ -46,7 +46,7 @@
- {{ t('decidesk', 'Geen resultaten gevonden') }}
+ {{ t('decidesk', 'No results found') }}
@@ -79,7 +79,7 @@
diff --git a/src/router/index.js b/src/router/index.js
index 3f0c966e..754cebbb 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -6,25 +6,20 @@ import Router from 'vue-router'
import { generateUrl } from '@nextcloud/router'
/**
- * @spec openspec/changes/p1-dashboard-and-navigation/tasks.md#task-3.4
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-3.4
*/
Vue.use(Router)
-/**
- * Lazy-load view components to reduce initial bundle size.
- */
const DashboardView = () => import('../views/DashboardView.vue')
-const MeetingList = () => import('../views/MeetingList.vue')
+const GovernanceBodies = () => import('../views/GovernanceBodies.vue')
+const GovernanceBodyDetail = () => import('../views/GovernanceBodyDetail.vue')
+const Meetings = () => import('../views/Meetings.vue')
const MeetingDetail = () => import('../views/MeetingDetail.vue')
-const MotionList = () => import('../views/MotionList.vue')
-const MotionDetail = () => import('../views/MotionDetail.vue')
-const DecisionList = () => import('../views/DecisionList.vue')
-const DecisionDetail = () => import('../views/DecisionDetail.vue')
-const ParticipantList = () => import('../views/ParticipantList.vue')
+const Participants = () => import('../views/Participants.vue')
const ParticipantDetail = () => import('../views/ParticipantDetail.vue')
-const GovernanceBodyList = () => import('../views/GovernanceBodyList.vue')
-const GovernanceBodyDetail = () => import('../views/GovernanceBodyDetail.vue')
+const AgendaItems = () => import('../views/AgendaItems.vue')
+const AgendaItemDetail = () => import('../views/AgendaItemDetail.vue')
const SettingsView = () => import('../views/SettingsView.vue')
export default new Router({
@@ -32,16 +27,14 @@ export default new Router({
base: generateUrl('/apps/decidesk') + '/',
routes: [
{ path: '/', name: 'Dashboard', component: DashboardView },
- { path: '/meetings', name: 'MeetingList', component: MeetingList },
+ { path: '/governance-bodies', name: 'GovernanceBodies', component: GovernanceBodies },
+ { path: '/governance-bodies/:id', name: 'GovernanceBodyDetail', component: GovernanceBodyDetail, props: true },
+ { path: '/meetings', name: 'Meetings', component: Meetings },
{ path: '/meetings/:id', name: 'MeetingDetail', component: MeetingDetail, props: true },
- { path: '/motions', name: 'MotionList', component: MotionList },
- { path: '/motions/:id', name: 'MotionDetail', component: MotionDetail, props: true },
- { path: '/decisions', name: 'DecisionList', component: DecisionList },
- { path: '/decisions/:id', name: 'DecisionDetail', component: DecisionDetail, props: true },
- { path: '/participants', name: 'ParticipantList', component: ParticipantList },
+ { path: '/participants', name: 'Participants', component: Participants },
{ path: '/participants/:id', name: 'ParticipantDetail', component: ParticipantDetail, props: true },
- { path: '/governance-bodies', name: 'GovernanceBodyList', component: GovernanceBodyList },
- { path: '/governance-bodies/:id', name: 'GovernanceBodyDetail', component: GovernanceBodyDetail, props: true },
+ { path: '/agenda-items', name: 'AgendaItems', component: AgendaItems },
+ { path: '/agenda-items/:id', name: 'AgendaItemDetail', component: AgendaItemDetail, props: true },
{ path: '/settings', name: 'Settings', component: SettingsView },
{ path: '*', redirect: '/' },
],
diff --git a/src/store/modules/object.js b/src/store/modules/object.js
deleted file mode 100644
index 789f1a75..00000000
--- a/src/store/modules/object.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { defineStore } from 'pinia'
-import { getRequestToken } from '@nextcloud/auth'
-
-/**
- * Generic OpenRegister object store.
- * Configure it with baseUrl and schemaBaseUrl, then register object types.
- */
-export const useObjectStore = defineStore('object', {
- state: () => ({
- baseUrl: '',
- schemaBaseUrl: '',
- objectTypes: {},
- objects: {},
- loading: {},
- }),
-
- actions: {
- configure({ baseUrl, schemaBaseUrl }) {
- this.baseUrl = baseUrl
- this.schemaBaseUrl = schemaBaseUrl
- },
-
- registerObjectType(type, schema, register) {
- this.objectTypes[type] = { schema, register }
- if (!this.objects[type]) {
- this.objects[type] = []
- }
- },
-
- async fetchObjects(type, params = {}) {
- if (!this.objectTypes[type]) {
- console.warn(`Object type "${type}" is not registered`)
- return []
- }
-
- this.loading[type] = true
- const { schema, register } = this.objectTypes[type]
-
- try {
- const url = new URL(this.baseUrl, window.location.origin)
- url.searchParams.set('register', register)
- url.searchParams.set('schema', schema)
- Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v))
-
- const response = await fetch(url.toString(), {
- headers: { requesttoken: getRequestToken() },
- })
- if (response.ok) {
- const data = await response.json()
- this.objects[type] = data.results || data
- return this.objects[type]
- }
- } catch (error) {
- console.error(`Failed to fetch ${type} objects:`, error)
- } finally {
- this.loading[type] = false
- }
- return []
- },
- },
-})
diff --git a/src/store/store.js b/src/store/store.js
index cf228e26..0ea9bb51 100644
--- a/src/store/store.js
+++ b/src/store/store.js
@@ -1,49 +1,63 @@
-import { generateUrl } from '@nextcloud/router'
-import { useObjectStore } from './modules/object.js'
+// SPDX-License-Identifier: EUPL-1.2
+// Copyright (C) 2026 Conduction B.V.
+
+import { createObjectStore, filesPlugin, auditTrailsPlugin, relationsPlugin } from '@conduction/nextcloud-vue'
import { useSettingsStore } from './modules/settings.js'
+/**
+ * Object store with files, auditTrails, and relations plugins.
+ *
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-3.3
+ */
+export const useObjectStore = createObjectStore('decidesk-objects', {
+ plugins: [filesPlugin(), auditTrailsPlugin(), relationsPlugin()],
+})
+
/**
* Object types to register with OpenRegister.
- * Each entry maps a logical name to its schema slug and register slug.
+ * Each entry maps a logical slug to its schema slug and register slug.
*
- * @spec openspec/changes/p1-schemas-and-data-model/tasks.md#task-1
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-3.2
*/
const OBJECT_TYPES = {
- governanceBody: { schema: 'governance-body', register: 'decidesk' },
+ 'governance-body': { schema: 'governance-body', register: 'decidesk' },
meeting: { schema: 'meeting', register: 'decidesk' },
participant: { schema: 'participant', register: 'decidesk' },
- agendaItem: { schema: 'agenda-item', register: 'decidesk' },
+ 'agenda-item': { schema: 'agenda-item', register: 'decidesk' },
motion: { schema: 'motion', register: 'decidesk' },
amendment: { schema: 'amendment', register: 'decidesk' },
- votingRound: { schema: 'voting-round', register: 'decidesk' },
+ 'voting-round': { schema: 'voting-round', register: 'decidesk' },
vote: { schema: 'vote', register: 'decidesk' },
decision: { schema: 'decision', register: 'decidesk' },
- actionItem: { schema: 'action-item', register: 'decidesk' },
+ 'action-item': { schema: 'action-item', register: 'decidesk' },
minutes: { schema: 'minutes', register: 'decidesk' },
- digitalDocument: { schema: 'digital-document', register: 'decidesk' },
- monetaryAmount: { schema: 'monetary-amount', register: 'decidesk' },
+ 'digital-document': { schema: 'digital-document', register: 'decidesk' },
+ 'monetary-amount': { schema: 'monetary-amount', register: 'decidesk' },
offer: { schema: 'offer', register: 'decidesk' },
order: { schema: 'order', register: 'decidesk' },
product: { schema: 'product', register: 'decidesk' },
report: { schema: 'report', register: 'decidesk' },
}
+/**
+ * Initialize all stores: fetch settings, then register entity types.
+ *
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-3.2
+ * @return {Promise<{settingsStore: object, objectStore: object}>}
+ */
export async function initializeStores() {
const settingsStore = useSettingsStore()
const objectStore = useObjectStore()
- objectStore.configure({
- baseUrl: generateUrl('/apps/openregister/api/objects'),
- schemaBaseUrl: generateUrl('/apps/openregister/api/schemas'),
- })
-
await settingsStore.fetchSettings()
- for (const [name, { schema, register }] of Object.entries(OBJECT_TYPES)) {
- objectStore.registerObjectType(name, schema, register)
+ if (settingsStore.hasOpenRegisters) {
+ for (const [slug, { schema, register }] of Object.entries(OBJECT_TYPES)) {
+ objectStore.registerObjectType(slug, schema, register)
+ }
}
return { settingsStore, objectStore }
}
-export { useObjectStore, useSettingsStore }
+export { useSettingsStore }
diff --git a/src/views/AgendaItemDetail.vue b/src/views/AgendaItemDetail.vue
new file mode 100644
index 00000000..c3e13d49
--- /dev/null
+++ b/src/views/AgendaItemDetail.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('decidesk', 'No linked meeting.') }}
+
+
+ -
+
+ {{ meeting.title || meeting.name || meeting.id || meeting }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/AgendaItems.vue b/src/views/AgendaItems.vue
new file mode 100644
index 00000000..e4db58cb
--- /dev/null
+++ b/src/views/AgendaItems.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue
deleted file mode 100644
index 9161111c..00000000
--- a/src/views/Dashboard.vue
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - {{ t('decidesk', 'Placeholder: user opened a record') }}
- - {{ t('decidesk', 'Placeholder: status changed to Review') }}
- - {{ t('decidesk', 'Placeholder: comment added') }}
-
-
-
-
-
- {{ t('decidesk', 'Wire buttons here to create records, open lists, or deep links. Use the sidebar for Settings and Documentation.') }}
-
-
-
-
-
-
-
-
-
diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue
index 2e7bada8..79077225 100644
--- a/src/views/DashboardView.vue
+++ b/src/views/DashboardView.vue
@@ -2,12 +2,9 @@
-
+
@@ -60,33 +57,22 @@
{{ t('decidesk', 'No meetings found. Create a meeting to see status distribution.') }}
-
-
-
-
-
-
-
@@ -266,10 +186,4 @@ export default {
text-align: center;
padding: 24px 0;
}
-
-.decidesk-dashboard__tiles {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
- gap: 12px;
-}
diff --git a/src/views/DecisionDetail.vue b/src/views/DecisionDetail.vue
deleted file mode 100644
index aa9e49d9..00000000
--- a/src/views/DecisionDetail.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
{{ t('decidesk', 'Besluit') }}
-
{{ t('decidesk', 'Decision detail view — coming soon.') }}
-
-
-
-
diff --git a/src/views/DecisionList.vue b/src/views/DecisionList.vue
deleted file mode 100644
index ac3f31e4..00000000
--- a/src/views/DecisionList.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
{{ t('decidesk', 'Besluiten') }}
-
-
-
-
-
-
-
-
-
diff --git a/src/views/GovernanceBodies.vue b/src/views/GovernanceBodies.vue
new file mode 100644
index 00000000..b711aa1c
--- /dev/null
+++ b/src/views/GovernanceBodies.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/GovernanceBodyDetail.vue b/src/views/GovernanceBodyDetail.vue
index bb8afc7f..d926bda0 100644
--- a/src/views/GovernanceBodyDetail.vue
+++ b/src/views/GovernanceBodyDetail.vue
@@ -2,18 +2,137 @@
-
-
{{ t('decidesk', 'Bestuursorgaan') }}
-
{{ t('decidesk', 'Governance body detail view — coming soon.') }}
-
+
+
+
+
+
+
+
+
+
+
+ {{ t('decidesk', 'No related meetings.') }}
+
+
+ -
+
+ {{ meeting.title || meeting.name || meeting.id || meeting }}
+
+
+
+
+
+
+ {{ t('decidesk', 'No related participants.') }}
+
+
+ -
+
+ {{ participant.displayName || participant.name || participant.id || participant }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/GovernanceBodyList.vue b/src/views/GovernanceBodyList.vue
deleted file mode 100644
index bbdf63bf..00000000
--- a/src/views/GovernanceBodyList.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
{{ t('decidesk', 'Bestuursorganen') }}
-
-
-
-
-
-
-
-
-
diff --git a/src/views/MeetingDetail.vue b/src/views/MeetingDetail.vue
index c6bfbb11..27c3deb0 100644
--- a/src/views/MeetingDetail.vue
+++ b/src/views/MeetingDetail.vue
@@ -2,18 +2,132 @@
-
-
{{ t('decidesk', 'Vergadering') }}
-
{{ t('decidesk', 'Meeting detail view — coming soon.') }}
-
+
+
+
+
+
+
+
+
+
+
+ {{ t('decidesk', 'No agenda items.') }}
+
+
+ -
+
+ {{ item.title || item.name || item.id || item }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/MeetingList.vue b/src/views/MeetingList.vue
deleted file mode 100644
index fe4f8844..00000000
--- a/src/views/MeetingList.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
{{ t('decidesk', 'Vergaderingen') }}
-
-
-
-
-
-
-
-
-
diff --git a/src/views/Meetings.vue b/src/views/Meetings.vue
new file mode 100644
index 00000000..6619c568
--- /dev/null
+++ b/src/views/Meetings.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/MotionDetail.vue b/src/views/MotionDetail.vue
deleted file mode 100644
index 8ee5cab8..00000000
--- a/src/views/MotionDetail.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
{{ t('decidesk', 'Motie') }}
-
{{ t('decidesk', 'Motion detail view — coming soon.') }}
-
-
-
-
diff --git a/src/views/MotionList.vue b/src/views/MotionList.vue
deleted file mode 100644
index 0a4b7224..00000000
--- a/src/views/MotionList.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
{{ t('decidesk', 'Moties') }}
-
-
-
-
-
-
-
-
-
diff --git a/src/views/ParticipantDetail.vue b/src/views/ParticipantDetail.vue
index d90512a5..c634017c 100644
--- a/src/views/ParticipantDetail.vue
+++ b/src/views/ParticipantDetail.vue
@@ -2,18 +2,125 @@
-
-
{{ t('decidesk', 'Deelnemer') }}
-
{{ t('decidesk', 'Participant detail view — coming soon.') }}
-
+
+
+
+
+
+
+
+
+
+
+ {{ t('decidesk', 'No linked governance body.') }}
+
+
+ -
+
+ {{ body.name || body.displayName || body.id || body }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/ParticipantList.vue b/src/views/ParticipantList.vue
deleted file mode 100644
index 5fcf99cb..00000000
--- a/src/views/ParticipantList.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
{{ t('decidesk', 'Deelnemers') }}
-
-
-
-
-
-
-
-
-
diff --git a/src/views/Participants.vue b/src/views/Participants.vue
new file mode 100644
index 00000000..ebbef81c
--- /dev/null
+++ b/src/views/Participants.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
index 2a72a30b..6e3b77e1 100644
--- a/src/views/SettingsView.vue
+++ b/src/views/SettingsView.vue
@@ -4,11 +4,10 @@
-
{{ t('decidesk', 'Instellingen') }}
-
- {{ reimporting ? t('decidesk', 'Importing...') : t('decidesk', 'Register opnieuw importeren') }}
+ {{ reimporting ? t('decidesk', 'Importing...') : t('decidesk', 'Reimport Register') }}
@@ -54,7 +53,7 @@ import { NcButton } from '@nextcloud/vue'
import { CnVersionInfoCard, CnRegisterMapping, CnSettingsSection } from '@conduction/nextcloud-vue'
import { generateUrl } from '@nextcloud/router'
import { showSuccess, showError } from '@nextcloud/dialogs'
-import axios from '@nextcloud/axios'
+import { getRequestToken } from '@nextcloud/auth'
import { useSettingsStore } from '../store/modules/settings.js'
/**
@@ -79,19 +78,19 @@ export default {
{
name: this.t('decidesk', 'Decidesk'),
types: [
- { slug: 'governanceBody', label: this.t('decidesk', 'Governance Bodies') },
+ { slug: 'governance-body', label: this.t('decidesk', 'Governance Bodies') },
{ slug: 'meeting', label: this.t('decidesk', 'Meetings') },
{ slug: 'participant', label: this.t('decidesk', 'Participants') },
- { slug: 'agendaItem', label: this.t('decidesk', 'Agenda Items') },
+ { slug: 'agenda-item', label: this.t('decidesk', 'Agenda Items') },
{ slug: 'motion', label: this.t('decidesk', 'Motions') },
{ slug: 'amendment', label: this.t('decidesk', 'Amendments') },
- { slug: 'votingRound', label: this.t('decidesk', 'Voting Rounds') },
+ { slug: 'voting-round', label: this.t('decidesk', 'Voting Rounds') },
{ slug: 'vote', label: this.t('decidesk', 'Votes') },
{ slug: 'decision', label: this.t('decidesk', 'Decisions') },
- { slug: 'actionItem', label: this.t('decidesk', 'Action Items') },
+ { slug: 'action-item', label: this.t('decidesk', 'Action Items') },
{ slug: 'minutes', label: this.t('decidesk', 'Minutes') },
- { slug: 'digitalDocument', label: this.t('decidesk', 'Digital Documents') },
- { slug: 'monetaryAmount', label: this.t('decidesk', 'Monetary Amounts') },
+ { slug: 'digital-document', label: this.t('decidesk', 'Digital Documents') },
+ { slug: 'monetary-amount', label: this.t('decidesk', 'Monetary Amounts') },
{ slug: 'offer', label: this.t('decidesk', 'Offers') },
{ slug: 'order', label: this.t('decidesk', 'Orders') },
{ slug: 'product', label: this.t('decidesk', 'Products') },
@@ -131,7 +130,11 @@ export default {
async reimportRegister() {
this.reimporting = true
try {
- const { data } = await axios.post(generateUrl('/apps/decidesk/api/settings/load'))
+ const response = await fetch(generateUrl('/apps/decidesk/api/settings/load'), {
+ method: 'POST',
+ headers: { requesttoken: getRequestToken() },
+ })
+ const data = await response.json()
if (data.success) {
showSuccess(this.t('decidesk', 'Register successfully reimported.'))
} else {
@@ -150,13 +153,7 @@ export default {
diff --git a/tests/Unit/Service/SettingsServiceTest.php b/tests/Unit/Service/SettingsServiceTest.php
new file mode 100644
index 00000000..9d340d90
--- /dev/null
+++ b/tests/Unit/Service/SettingsServiceTest.php
@@ -0,0 +1,281 @@
+
+ * @copyright 2024 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @version GIT:
+ *
+ * @link https://conduction.nl
+ *
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.3
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Decidesk\Tests\Unit\Service;
+
+use OCA\Decidesk\Service\SettingsService;
+use OCP\App\IAppManager;
+use OCP\IAppConfig;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Tests for SettingsService.
+ *
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.3
+ */
+class SettingsServiceTest extends TestCase
+{
+
+ /**
+ * Service under test.
+ *
+ * @var SettingsService
+ */
+ private SettingsService $service;
+
+ /**
+ * Mock IAppConfig.
+ *
+ * @var IAppConfig&MockObject
+ */
+ private IAppConfig&MockObject $appConfig;
+
+ /**
+ * Mock IAppManager.
+ *
+ * @var IAppManager&MockObject
+ */
+ private IAppManager&MockObject $appManager;
+
+ /**
+ * Mock ContainerInterface.
+ *
+ * @var ContainerInterface&MockObject
+ */
+ private ContainerInterface&MockObject $container;
+
+ /**
+ * Mock IGroupManager.
+ *
+ * @var IGroupManager&MockObject
+ */
+ private IGroupManager&MockObject $groupManager;
+
+ /**
+ * Mock IUserSession.
+ *
+ * @var IUserSession&MockObject
+ */
+ private IUserSession&MockObject $userSession;
+
+ /**
+ * Mock LoggerInterface.
+ *
+ * @var LoggerInterface&MockObject
+ */
+ private LoggerInterface&MockObject $logger;
+
+ /**
+ * Set up test fixtures.
+ *
+ * @return void
+ */
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->appConfig = $this->createMock(originalClassName: IAppConfig::class);
+ $this->appManager = $this->createMock(originalClassName: IAppManager::class);
+ $this->container = $this->createMock(originalClassName: ContainerInterface::class);
+ $this->groupManager = $this->createMock(originalClassName: IGroupManager::class);
+ $this->userSession = $this->createMock(originalClassName: IUserSession::class);
+ $this->logger = $this->createMock(originalClassName: LoggerInterface::class);
+
+ $this->service = new SettingsService(
+ appConfig: $this->appConfig,
+ appManager: $this->appManager,
+ container: $this->container,
+ groupManager: $this->groupManager,
+ userSession: $this->userSession,
+ logger: $this->logger,
+ );
+
+ }//end setUp()
+
+ /**
+ * Test that isOpenRegisterAvailable returns true when OpenRegister is installed.
+ *
+ * @return void
+ */
+ public function testIsOpenRegisterAvailableReturnsTrue(): void
+ {
+ $this->appManager->expects($this->once())
+ ->method('isInstalled')
+ ->with('openregister')
+ ->willReturn(true);
+
+ self::assertTrue(condition: $this->service->isOpenRegisterAvailable());
+
+ }//end testIsOpenRegisterAvailableReturnsTrue()
+
+ /**
+ * Test that isOpenRegisterAvailable returns false when OpenRegister is not installed.
+ *
+ * @return void
+ */
+ public function testIsOpenRegisterAvailableReturnsFalse(): void
+ {
+ $this->appManager->expects($this->once())
+ ->method('isInstalled')
+ ->with('openregister')
+ ->willReturn(false);
+
+ self::assertFalse(condition: $this->service->isOpenRegisterAvailable());
+
+ }//end testIsOpenRegisterAvailableReturnsFalse()
+
+ /**
+ * Test that getSettings returns settings with admin flag for admin user.
+ *
+ * @return void
+ */
+ public function testGetSettingsReturnsAdminTrue(): void
+ {
+ $user = $this->createMock(originalClassName: IUser::class);
+ $user->method('getUID')->willReturn('admin');
+
+ $this->userSession->method('getUser')->willReturn($user);
+ $this->groupManager->method('isAdmin')
+ ->with('admin')
+ ->willReturn(true);
+
+ $this->appConfig->method('getValueString')
+ ->willReturn('some-register-id');
+
+ $this->appManager->method('isInstalled')
+ ->willReturn(true);
+
+ $settings = $this->service->getSettings();
+
+ self::assertTrue(condition: $settings['isAdmin']);
+ self::assertTrue(condition: $settings['openregisters']);
+ self::assertSame(expected: 'some-register-id', actual: $settings['register']);
+
+ }//end testGetSettingsReturnsAdminTrue()
+
+ /**
+ * Test that getSettings returns isAdmin false for non-admin user.
+ *
+ * @return void
+ */
+ public function testGetSettingsReturnsAdminFalseForNonAdmin(): void
+ {
+ $user = $this->createMock(originalClassName: IUser::class);
+ $user->method('getUID')->willReturn('regularuser');
+
+ $this->userSession->method('getUser')->willReturn($user);
+ $this->groupManager->method('isAdmin')
+ ->with('regularuser')
+ ->willReturn(false);
+
+ $this->appConfig->method('getValueString')
+ ->willReturn('');
+
+ $this->appManager->method('isInstalled')
+ ->willReturn(false);
+
+ $settings = $this->service->getSettings();
+
+ self::assertFalse(condition: $settings['isAdmin']);
+ self::assertFalse(condition: $settings['openregisters']);
+
+ }//end testGetSettingsReturnsAdminFalseForNonAdmin()
+
+ /**
+ * Test that updateSettings writes to IAppConfig.
+ *
+ * @return void
+ */
+ public function testUpdateSettingsWritesToConfig(): void
+ {
+ $this->appConfig->expects($this->once())
+ ->method('setValueString')
+ ->with('decidesk', 'register', 'new-register-id');
+
+ $user = $this->createMock(originalClassName: IUser::class);
+ $user->method('getUID')->willReturn('admin');
+ $this->userSession->method('getUser')->willReturn($user);
+ $this->groupManager->method('isAdmin')->willReturn(true);
+
+ $this->appConfig->method('getValueString')
+ ->willReturn('new-register-id');
+
+ $this->appManager->method('isInstalled')
+ ->willReturn(true);
+
+ $result = $this->service->updateSettings(['register' => 'new-register-id']);
+
+ self::assertSame(expected: 'new-register-id', actual: $result['register']);
+
+ }//end testUpdateSettingsWritesToConfig()
+
+ /**
+ * Test that loadConfiguration returns failure when OpenRegister is not available.
+ *
+ * @return void
+ */
+ public function testLoadConfigurationFailsWithoutOpenRegister(): void
+ {
+ $this->appManager->method('isInstalled')
+ ->with('openregister')
+ ->willReturn(false);
+
+ $result = $this->service->loadConfiguration();
+
+ self::assertFalse(condition: $result['success']);
+ self::assertStringContainsString(
+ needle: 'not installed',
+ haystack: $result['message']
+ );
+
+ }//end testLoadConfigurationFailsWithoutOpenRegister()
+
+ /**
+ * Test that loadConfiguration returns failure when import throws exception.
+ *
+ * @return void
+ */
+ public function testLoadConfigurationHandlesException(): void
+ {
+ $this->appManager->method('isInstalled')
+ ->with('openregister')
+ ->willReturn(true);
+
+ $this->container->method('get')
+ ->willThrowException(new \RuntimeException('Service not found'));
+
+ $result = $this->service->loadConfiguration();
+
+ self::assertFalse(condition: $result['success']);
+ self::assertStringContainsString(
+ needle: 'failed',
+ haystack: $result['message']
+ );
+
+ }//end testLoadConfigurationHandlesException()
+
+}//end class
diff --git a/tests/unit/Controller/SettingsControllerTest.php b/tests/unit/Controller/SettingsControllerTest.php
index d2e239a7..4aa85539 100644
--- a/tests/unit/Controller/SettingsControllerTest.php
+++ b/tests/unit/Controller/SettingsControllerTest.php
@@ -32,6 +32,8 @@
/**
* Tests for SettingsController.
+ *
+ * @spec openspec/changes/p1-crud-operations/tasks.md#task-2.4
*/
class SettingsControllerTest extends TestCase
{