diff --git a/l10n/en.js b/l10n/en.js index 16785b7ab..e62464885 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -2214,7 +2214,14 @@ OC.L10N.register( "How the menu beside the visitor's own pages opens.": "How the menu beside the visitor's own pages opens.", "Card label": "Card label", "The line above the organisation's name at the top of the menu, shown while the visitor acts for an organisation, such as U regelt het voor. Empty shows no card.": "The line above the organisation's name at the top of the menu, shown while the visitor acts for an organisation, such as U regelt het voor. Empty shows no card.", - "Only on this demo. No password is asked.": "Only on this demo. No password is asked.", + "My cases page": "My cases page", + "How the page that lists every case draws them.": "How the page that lists every case draws them.", + "cards draws the folder cards; rows draws one line per case with its number, title, tag and the day it is due by.": "cards draws the folder cards; rows draws one line per case with its number, title, tag and the day it is due by.", + "The menu in this portal's own groups, in order: each a title and the items in it by name. An item is a section of the own area (overview, cases, tasks, inbox, messages, news, access, details, account) or a contributed page as app:page, such as dossiq:berichten. An item not named here keeps its place in the groups the site builds itself, so nothing becomes unreachable. Empty keeps the site's own groups.": "The menu in this portal's own groups, in order: each a title and the items in it by name. An item is a section of the own area (overview, cases, tasks, inbox, messages, news, access, details, account) or a contributed page as app:page, such as dossiq:berichten. An item not named here keeps its place in the groups the site builds itself, so nothing becomes unreachable. Empty keeps the site's own groups.", + "Group title": "Group title", + "The heading over this group of the menu, such as Zaken en taken.": "The heading over this group of the menu, such as Zaken en taken.", + "Group items": "Group items", + "The items in this group, in order, each by name: a section of the own area or a contributed page as app:page.": "The items in this group, in order, each by name: a section of the own area or a contributed page as app:page.", "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks) or one day as a timetable (calendar-timetable-display).": "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks) or one day as a timetable (calendar-timetable-display).", "On a timetable: the small label over the first item of the day.": "On a timetable: the small label over the first item of the day.", "firstLabel": "firstLabel", @@ -2225,7 +2232,8 @@ OC.L10N.register( "The role of that staff member in words, such as Mentor.": "The role of that staff member in words, such as Mentor.", "The subject line of the conversation.": "The subject line of the conversation.", "The words that name that record, copied when the thread is started, such as Vera, Groep 7.": "The words that name that record, copied when the thread is started, such as Vera, Groep 7.", - "With": "With" + "With": "With", + "Only on this demo. No password is asked.": "Only on this demo. No password is asked." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index 5f770d53f..3589c1c07 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -2225,7 +2225,14 @@ "How the menu beside the visitor's own pages opens.": "How the menu beside the visitor's own pages opens.", "Card label": "Card label", "The line above the organisation's name at the top of the menu, shown while the visitor acts for an organisation, such as U regelt het voor. Empty shows no card.": "The line above the organisation's name at the top of the menu, shown while the visitor acts for an organisation, such as U regelt het voor. Empty shows no card.", - "Only on this demo. No password is asked.": "Only on this demo. No password is asked.", + "My cases page": "My cases page", + "How the page that lists every case draws them.": "How the page that lists every case draws them.", + "cards draws the folder cards; rows draws one line per case with its number, title, tag and the day it is due by.": "cards draws the folder cards; rows draws one line per case with its number, title, tag and the day it is due by.", + "The menu in this portal's own groups, in order: each a title and the items in it by name. An item is a section of the own area (overview, cases, tasks, inbox, messages, news, access, details, account) or a contributed page as app:page, such as dossiq:berichten. An item not named here keeps its place in the groups the site builds itself, so nothing becomes unreachable. Empty keeps the site's own groups.": "The menu in this portal's own groups, in order: each a title and the items in it by name. An item is a section of the own area (overview, cases, tasks, inbox, messages, news, access, details, account) or a contributed page as app:page, such as dossiq:berichten. An item not named here keeps its place in the groups the site builds itself, so nothing becomes unreachable. Empty keeps the site's own groups.", + "Group title": "Group title", + "The heading over this group of the menu, such as Zaken en taken.": "The heading over this group of the menu, such as Zaken en taken.", + "Group items": "Group items", + "The items in this group, in order, each by name: a section of the own area or a contributed page as app:page.": "The items in this group, in order, each by name: a section of the own area or a contributed page as app:page.", "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks) or one day as a timetable (calendar-timetable-display).": "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks) or one day as a timetable (calendar-timetable-display).", "On a timetable: the small label over the first item of the day.": "On a timetable: the small label over the first item of the day.", "firstLabel": "firstLabel", @@ -2236,7 +2243,8 @@ "The role of that staff member in words, such as Mentor.": "The role of that staff member in words, such as Mentor.", "The subject line of the conversation.": "The subject line of the conversation.", "The words that name that record, copied when the thread is started, such as Vera, Groep 7.": "The words that name that record, copied when the thread is started, such as Vera, Groep 7.", - "With": "With" + "With": "With", + "Only on this demo. No password is asked.": "Only on this demo. No password is asked." }, "plurals": "", "pluralForm": "nplurals=2; plural=(n != 1);" diff --git a/l10n/nl.js b/l10n/nl.js index 01fc5917d..2ded1798a 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -2279,7 +2279,14 @@ OC.L10N.register( "How the menu beside the visitor's own pages opens.": "Hoe het menu naast de eigen pagina's van de bezoeker begint.", "Card label": "Regel boven de naam", "The line above the organisation's name at the top of the menu, shown while the visitor acts for an organisation, such as U regelt het voor. Empty shows no card.": "De regel boven de naam van de organisatie bovenaan het menu, getoond als de bezoeker voor een organisatie handelt, bijvoorbeeld U regelt het voor. Leeg toont geen kaart.", - "Only on this demo. No password is asked.": "Alleen op deze demo. Er wordt geen wachtwoord gevraagd.", + "My cases page": "Pagina Mijn zaken", + "How the page that lists every case draws them.": "Hoe de pagina die elke zaak opsomt ze tekent.", + "cards draws the folder cards; rows draws one line per case with its number, title, tag and the day it is due by.": "cards tekent de mapkaarten; rows tekent een regel per zaak met het nummer, de titel, het label en de dag waarop hij uiterlijk klaar is.", + "The menu in this portal's own groups, in order: each a title and the items in it by name. An item is a section of the own area (overview, cases, tasks, inbox, messages, news, access, details, account) or a contributed page as app:page, such as dossiq:berichten. An item not named here keeps its place in the groups the site builds itself, so nothing becomes unreachable. Empty keeps the site's own groups.": "Het menu in de eigen groepen van dit portaal, op volgorde: elk een titel en de onderdelen erin, op naam. Een onderdeel is een sectie van de eigen omgeving (overview, cases, tasks, inbox, messages, news, access, details, account) of een bijgedragen pagina als app:page, zoals dossiq:berichten. Een onderdeel dat hier niet staat houdt zijn plek in de groepen die de site zelf maakt, zodat niets onbereikbaar wordt. Leeg houdt de eigen groepen van de site.", + "Group title": "Groepstitel", + "The heading over this group of the menu, such as Zaken en taken.": "De kop boven deze groep van het menu, zoals Zaken en taken.", + "Group items": "Onderdelen van de groep", + "The items in this group, in order, each by name: a section of the own area or a contributed page as app:page.": "De onderdelen in deze groep, op volgorde, elk op naam: een sectie van de eigen omgeving of een bijgedragen pagina als app:page.", "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks) or one day as a timetable (calendar-timetable-display).": "Op een lijstblok: kaarten, rijen met datum, balken of cijferlabels in plaats van een tabel; bij taken de uitgelichte kaart; bij kerncijfers één verdeelde balk; bij de kalender datumtegels (site-school-blocks) of één dag als rooster (calendar-timetable-display).", "On a timetable: the small label over the first item of the day.": "Op een rooster: het kleine label boven het eerste onderdeel van de dag.", "firstLabel": "firstLabel", @@ -2290,7 +2297,8 @@ OC.L10N.register( "The role of that staff member in words, such as Mentor.": "De rol van die medewerker in woorden, zoals Mentor.", "The subject line of the conversation.": "De onderwerpregel van het gesprek.", "The words that name that record, copied when the thread is started, such as Vera, Groep 7.": "De woorden die dat record noemen, overgenomen als het gesprek begint, zoals Vera, Groep 7.", - "With": "Met" + "With": "Met", + "Only on this demo. No password is asked.": "Alleen op deze demo. Er wordt geen wachtwoord gevraagd." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index c23834608..67476735e 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -2290,7 +2290,14 @@ "How the menu beside the visitor's own pages opens.": "Hoe het menu naast de eigen pagina's van de bezoeker begint.", "Card label": "Regel boven de naam", "The line above the organisation's name at the top of the menu, shown while the visitor acts for an organisation, such as U regelt het voor. Empty shows no card.": "De regel boven de naam van de organisatie bovenaan het menu, getoond als de bezoeker voor een organisatie handelt, bijvoorbeeld U regelt het voor. Leeg toont geen kaart.", - "Only on this demo. No password is asked.": "Alleen op deze demo. Er wordt geen wachtwoord gevraagd.", + "My cases page": "Pagina Mijn zaken", + "How the page that lists every case draws them.": "Hoe de pagina die elke zaak opsomt ze tekent.", + "cards draws the folder cards; rows draws one line per case with its number, title, tag and the day it is due by.": "cards tekent de mapkaarten; rows tekent een regel per zaak met het nummer, de titel, het label en de dag waarop hij uiterlijk klaar is.", + "The menu in this portal's own groups, in order: each a title and the items in it by name. An item is a section of the own area (overview, cases, tasks, inbox, messages, news, access, details, account) or a contributed page as app:page, such as dossiq:berichten. An item not named here keeps its place in the groups the site builds itself, so nothing becomes unreachable. Empty keeps the site's own groups.": "Het menu in de eigen groepen van dit portaal, op volgorde: elk een titel en de onderdelen erin, op naam. Een onderdeel is een sectie van de eigen omgeving (overview, cases, tasks, inbox, messages, news, access, details, account) of een bijgedragen pagina als app:page, zoals dossiq:berichten. Een onderdeel dat hier niet staat houdt zijn plek in de groepen die de site zelf maakt, zodat niets onbereikbaar wordt. Leeg houdt de eigen groepen van de site.", + "Group title": "Groepstitel", + "The heading over this group of the menu, such as Zaken en taken.": "De kop boven deze groep van het menu, zoals Zaken en taken.", + "Group items": "Onderdelen van de groep", + "The items in this group, in order, each by name: a section of the own area or a contributed page as app:page.": "De onderdelen in deze groep, op volgorde, elk op naam: een sectie van de eigen omgeving of een bijgedragen pagina als app:page.", "On a collection block: cards, dated rows, bars or mark chips instead of a table; on tasks the highlight card; on kpi one segmented bar; on calendar date tiles (site-school-blocks) or one day as a timetable (calendar-timetable-display).": "Op een lijstblok: kaarten, rijen met datum, balken of cijferlabels in plaats van een tabel; bij taken de uitgelichte kaart; bij kerncijfers één verdeelde balk; bij de kalender datumtegels (site-school-blocks) of één dag als rooster (calendar-timetable-display).", "On a timetable: the small label over the first item of the day.": "Op een rooster: het kleine label boven het eerste onderdeel van de dag.", "firstLabel": "firstLabel", @@ -2301,7 +2308,8 @@ "The role of that staff member in words, such as Mentor.": "De rol van die medewerker in woorden, zoals Mentor.", "The subject line of the conversation.": "De onderwerpregel van het gesprek.", "The words that name that record, copied when the thread is started, such as Vera, Groep 7.": "De woorden die dat record noemen, overgenomen als het gesprek begint, zoals Vera, Groep 7.", - "With": "Met" + "With": "Met", + "Only on this demo. No password is asked.": "Alleen op deze demo. Er wordt geen wachtwoord gevraagd." }, "plurals": "", "pluralForm": "nplurals=2; plural=(n != 1);" diff --git a/lib/Contribution/BoardKeys.php b/lib/Contribution/BoardKeys.php new file mode 100644 index 000000000..8293e5ebc --- /dev/null +++ b/lib/Contribution/BoardKeys.php @@ -0,0 +1,267 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + +declare(strict_types=1); + +namespace OCA\Portaliq\Contribution; + +/** + * The board keys on blocks and pages, each kept only when well formed. + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ +class BoardKeys { + + /** + * The tones a highlight card may take besides its accent. + */ + private const HIGHLIGHT_TONES = ['warning', 'info']; + + /** + * How long a label over the facts may be. + */ + private const MAX_LABEL_LENGTH = 120; + + /** + * How many turn values a cases block may name. + */ + private const MAX_TURN_VALUES = 10; + + + /** + * A cases block's board keys: `display: compact`, `showAll` and `yourTurn`. + * + * @param array $block The declared block. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function casesKeys(array $block): array { + $out = []; + if (($block['display'] ?? null) === 'compact') { + $out['display'] = 'compact'; + } + + if (($block['showAll'] ?? null) === true) { + $out['showAll'] = true; + } + + $turns = $this->names(value: ($block['yourTurn'] ?? null), max: self::MAX_TURN_VALUES); + if ($turns !== []) { + $out['yourTurn'] = $turns; + } + + return $out; + }//end casesKeys() + + + /** + * A tasks block's board key: `emptyNotice: true` (with nothing to do, + * the sentence is a notice in the ok tone, as the board draws it on the + * case page). + * + * @param array $block The declared block. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function tasksKeys(array $block): array { + if (($block['emptyNotice'] ?? null) === true) { + return ['emptyNotice' => true]; + } + + return []; + }//end tasksKeys() + + + /** + * A highlight tasks block's board keys: `tone` and `dueInLine`. + * + * @param array $block The declared block. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function highlightKeys(array $block): array { + $out = []; + $tone = ($block['tone'] ?? null); + if (is_string($tone) === true && in_array($tone, self::HIGHLIGHT_TONES, true) === true) { + $out['tone'] = $tone; + } + + if (($block['dueInLine'] ?? null) === true) { + $out['dueInLine'] = true; + } + + return $out; + }//end highlightKeys() + + + /** + * An inbox block's board key: `display: list`. + * + * @param array $block The declared block. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function inboxKeys(array $block): array { + if (($block['display'] ?? null) === 'list') { + return ['display' => 'list']; + } + + return []; + }//end inboxKeys() + + + /** + * A documents block's board key: `upload: true`. + * + * @param array $block The declared block. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function documentsKeys(array $block): array { + if (($block['upload'] ?? null) === true) { + return ['upload' => true]; + } + + return []; + }//end documentsKeys() + + + /** + * A detail block's board keys: a `label` over the facts and `timeline: false`. + * + * @param array $block The declared block. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function detailKeys(array $block): array { + $out = []; + $label = ($block['label'] ?? null); + if (is_string($label) === true) { + $label = trim($label); + if ($label !== '' && mb_strlen($label) <= self::MAX_LABEL_LENGTH) { + $out['label'] = $label; + } + } + + if (($block['timeline'] ?? null) === false) { + $out['timeline'] = false; + } + + return $out; + }//end detailKeys() + + + /** + * A citizenCase block's board key: `display: actions`. + * + * @param array $block The declared block. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function citizenCaseKeys(array $block): array { + if (($block['display'] ?? null) === 'actions') { + return ['display' => 'actions']; + } + + return []; + }//end citizenCaseKeys() + + + /** + * A page record's board keys: `heading: record` and `under: cases`. + * + * @param array $record The declared record. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + public function recordKeys(array $record): array { + $out = []; + if (($record['heading'] ?? null) === 'record') { + $out['heading'] = 'record'; + } + + if (($record['under'] ?? null) === 'cases') { + $out['under'] = 'cases'; + } + + return $out; + }//end recordKeys() + + + /** + * The non-empty strings in a declared list, at most `max` of them. + * + * @param mixed $value The declared value. + * @param int $max How many to keep. + * + * @return array + */ + private function names(mixed $value, int $max): array { + if (is_array($value) === false) { + return []; + } + + $out = []; + foreach ($value as $entry) { + if (is_string($entry) === true && trim($entry) !== '') { + $out[] = trim($entry); + } + } + + return array_slice(array_values(array_unique($out)), 0, $max); + }//end names() +}//end class diff --git a/lib/Contribution/ListBlockNormaliser.php b/lib/Contribution/ListBlockNormaliser.php index 94cdf4540..e1a1c1eaf 100644 --- a/lib/Contribution/ListBlockNormaliser.php +++ b/lib/Contribution/ListBlockNormaliser.php @@ -93,7 +93,8 @@ public function tasksBlock(array $block, array $collections): ?array { $entry += $this->excludeWhen(declared: ($block['excludeWhen'] ?? null), lookups: ($entry['lookups'] ?? [])); // The highlight card (site-school-blocks). - return $entry + $this->common(block: $block) + (new SchoolBlockKeys())->tasksKeys(block: $block, collection: $collection); + return $entry + $this->common(block: $block) + (new SchoolBlockKeys())->tasksKeys(block: $block, collection: $collection) + + (new BoardKeys())->tasksKeys(block: $block); }//end tasksBlock() /** @@ -162,7 +163,8 @@ public function inboxBlock(array $block, array $collections): ?array { $entry['recordField'] = $recordField; } - return $entry + $this->common(block: $block); + // The plain list (zuiddrecht-resident-pages-match-the-boards). + return $entry + $this->common(block: $block) + (new BoardKeys())->inboxKeys(block: $block); }//end inboxBlock() /** @@ -187,7 +189,9 @@ public function casesBlock(array $block, array $collections): ?array { $entry['open'] = true; } - return $entry + $this->common(block: $block); + // The compact card, the link to every case and the resident's turn + // (zuiddrecht-resident-pages-match-the-boards). + return $entry + $this->common(block: $block) + (new BoardKeys())->casesKeys(block: $block); }//end casesBlock() /** @@ -213,7 +217,13 @@ public function recordBlock(string $type, array $block, array $collections): ?ar return null; } - return ['type' => $type, 'collection' => $collection['id']] + $this->common(block: $block); + $entry = ['type' => $type, 'collection' => $collection['id']] + $this->common(block: $block); + if ($type === 'documents') { + // The upload button beside the heading (zuiddrecht-resident-pages-match-the-boards). + $entry += (new BoardKeys())->documentsKeys(block: $block); + } + + return $entry; }//end recordBlock() /** diff --git a/lib/Contribution/PortalBlockResolver.php b/lib/Contribution/PortalBlockResolver.php index 6424bec8e..c6dafaf23 100644 --- a/lib/Contribution/PortalBlockResolver.php +++ b/lib/Contribution/PortalBlockResolver.php @@ -196,6 +196,16 @@ private function collectionBlock(string $type, array $block, array $entry, array $entry = $scopes->scope(declared: $block, entry: $entry); $entry = $scopes->lookups(declared: $block, entry: $entry, collectionIds: $collectionIds); + // The board keys of a detail card and of the case screen + // (zuiddrecht-resident-pages-match-the-boards). + if ($type === 'detail') { + return $entry + (new BoardKeys())->detailKeys(block: $block); + } + + if ($type === 'citizenCase') { + return $entry + (new BoardKeys())->citizenCaseKeys(block: $block); + } + if ($type !== 'collection') { return $entry; } diff --git a/lib/Contribution/RecordBlockNormaliser.php b/lib/Contribution/RecordBlockNormaliser.php index 7cbc0b290..acccc7a15 100644 --- a/lib/Contribution/RecordBlockNormaliser.php +++ b/lib/Contribution/RecordBlockNormaliser.php @@ -69,7 +69,9 @@ public function pageRecord(mixed $record, array $collectionIds): ?array { $out['titleFields'] = $titleFields; } - return $out; + // The record's name as the page's heading and its place in the + // breadcrumb (zuiddrecht-resident-pages-match-the-boards). + return $out + (new BoardKeys())->recordKeys(record: $record); }//end pageRecord() /** diff --git a/lib/Contribution/SchoolBlockKeys.php b/lib/Contribution/SchoolBlockKeys.php index a57026bc0..d4f09974f 100644 --- a/lib/Contribution/SchoolBlockKeys.php +++ b/lib/Contribution/SchoolBlockKeys.php @@ -83,7 +83,9 @@ public function tasksKeys(array $block, array $collection): array { $out['subtitleFields'] = $fields; } - return $out; + // The card's tone and the due day in its line + // (zuiddrecht-resident-pages-match-the-boards). + return $out + (new BoardKeys())->highlightKeys(block: $block); }//end tasksKeys() /** diff --git a/lib/Service/Cms/PortalShell.php b/lib/Service/Cms/PortalShell.php index 60ac21553..a891c32fb 100644 --- a/lib/Service/Cms/PortalShell.php +++ b/lib/Service/Cms/PortalShell.php @@ -71,6 +71,8 @@ public function project(array $portal): array { 'headerSearch' => $this->headerSearch(portal: $portal), 'accountLabel' => $this->text(value: ($portal['accountLabel'] ?? '')), 'residentMenu' => $this->residentMenu(portal: $portal), + // How Mijn zaken draws its list (zuiddrecht-resident-pages-match-the-boards). + 'myCases' => $this->myCases(portal: $portal), 'footer' => $this->footer(portal: $portal), 'regions' => $this->publicRegions(portal: $portal), ]; @@ -81,24 +83,70 @@ public function project(array $portal): array { * * @param array $portal The portal record. * - * @return array `{cardLabel?}`. + * @return array `{cardLabel?, groups?}`. * * @spec openspec/changes/resident-menu-badges-and-cards/specs/site-resident-menu/spec.md#requirement-the-menu-may-open-with-whom-the-resident-acts-for + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/site-resident-menu/spec.md#requirement-a-portal-may-lay-out-the-resident-menu-and-its-cases-page */ private function residentMenu(array $portal): array { - $menu = $portal['residentMenu'] ?? []; - $label = ''; - if (is_array($menu) === true) { - $label = $this->text(value: ($menu['cardLabel'] ?? '')); + $menu = $portal['residentMenu'] ?? []; + if (is_array($menu) === false) { + return []; } - if ($label === '') { - return []; + $out = []; + $label = $this->text(value: ($menu['cardLabel'] ?? '')); + if ($label !== '') { + $out['cardLabel'] = $label; + } + + // The portal's own groups (zuiddrecht-resident-pages-match-the-boards): + // each a title and its items by name, at most 12 groups of 20. + $groups = []; + foreach (array_slice((array)($menu['groups'] ?? []), 0, 12) as $group) { + if (is_array($group) === false) { + continue; + } + + $items = []; + foreach (array_slice((array)($group['items'] ?? []), 0, 20) as $item) { + if (is_string($item) === true && preg_match('/^[a-z0-9][a-z0-9:_-]{0,79}$/i', $item) === 1) { + $items[] = $item; + } + } + + $title = $this->text(value: ($group['title'] ?? '')); + if ($items !== [] && $title !== '') { + $groups[] = ['title' => $title, 'items' => $items]; + } } - return ['cardLabel' => $label]; + if ($groups !== []) { + $out['groups'] = $groups; + } + + return $out; }//end residentMenu() + /** + * How Mijn zaken draws its list: `{display: rows}` when the portal says + * so, else [] (zuiddrecht-resident-pages-match-the-boards). + * + * @param array $portal The portal record. + * + * @return array + * + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/site-resident-menu/spec.md#requirement-a-portal-may-lay-out-the-resident-menu-and-its-cases-page + */ + private function myCases(array $portal): array { + $cases = $portal['myCases'] ?? []; + if (is_array($cases) === true && ($cases['display'] ?? null) === 'rows') { + return ['display' => 'rows']; + } + + return []; + }//end myCases() + /** * The search box in the header: whether it shows (a declared box shows * unless `enabled` is false), its accessible name, its hint and the diff --git a/lib/Settings/portaliq_register.json b/lib/Settings/portaliq_register.json index 620ebc535..ec82a2a02 100644 --- a/lib/Settings/portaliq_register.json +++ b/lib/Settings/portaliq_register.json @@ -3,7 +3,7 @@ "info": { "title": "Portaliq Register", "description": "Portaliq — the fleet's shared external portal for clients and suppliers (ADR-046). Holds every portal account, page, session, submission, message, notification, audit entry, traffic record, form and landing-page submission the app reads and writes.", - "version": "0.68.0" + "version": "0.69.0" }, "x-openregister": { "type": "application", @@ -20,7 +20,7 @@ "slug": "portaliq", "title": "Portaliq Register", "description": "Portaliq — the fleet's shared external portal for clients and suppliers (ADR-046). Holds every portal account, page, session, submission, message, notification, audit entry and traffic record the app reads and writes.", - "version": "0.68.0", + "version": "0.69.0", "schemas": [ "exampleDocument", "glossaryTerm", @@ -2448,7 +2448,7 @@ "portal": { "slug": "portal", "icon": "Web", - "version": "0.11.0", + "version": "0.12.0", "title": "Portal", "authorization": { "read": [ @@ -3025,6 +3025,55 @@ "type": "string", "description": "The line above the organisation's name at the top of the menu, shown while the visitor acts for an organisation, such as U regelt het voor. Empty shows no card.", "example": "U regelt het voor" + }, + "groups": { + "title": "Groups", + "type": "array", + "description": "The menu in this portal's own groups, in order: each a title and the items in it by name. An item is a section of the own area (overview, cases, tasks, inbox, messages, news, access, details, account) or a contributed page as app:page, such as dossiq:berichten. An item not named here keeps its place in the groups the site builds itself, so nothing becomes unreachable. Empty keeps the site's own groups.", + "items": { + "type": "object", + "properties": { + "title": { + "title": "Group title", + "type": "string", + "description": "The heading over this group of the menu, such as Zaken en taken." + }, + "items": { + "title": "Group items", + "type": "array", + "description": "The items in this group, in order, each by name: a section of the own area or a contributed page as app:page.", + "items": { + "type": "string" + } + } + } + }, + "example": [ + { + "title": "Mijn Zuiddrecht", + "items": [ + "overview", + "inbox" + ] + } + ] + } + } + }, + "myCases": { + "title": "My cases page", + "type": "object", + "description": "How the page that lists every case draws them.", + "properties": { + "display": { + "title": "Display", + "type": "string", + "enum": [ + "cards", + "rows" + ], + "description": "cards draws the folder cards; rows draws one line per case with its number, title, tag and the day it is due by.", + "example": "rows" } } }, diff --git a/lib/Settings/sites/zuiddrecht.json b/lib/Settings/sites/zuiddrecht.json index 36a5b4bcd..afa9d8842 100644 --- a/lib/Settings/sites/zuiddrecht.json +++ b/lib/Settings/sites/zuiddrecht.json @@ -47,6 +47,40 @@ "route": "/zoeken" }, "accountLabel": "Mijn Zuiddrecht", + "residentMenu": { + "groups": [ + { + "title": "Mijn Zuiddrecht", + "items": [ + "overview", + "inbox" + ] + }, + { + "title": "Zaken en taken", + "items": [ + "cases", + "tasks" + ] + }, + { + "title": "Vragen en meldingen", + "items": [ + "portaliq:meldingen" + ] + }, + { + "title": "Uw gegevens", + "items": [ + "details", + "account" + ] + } + ] + }, + "myCases": { + "display": "rows" + }, "footer": { "description": "Heeft u een vraag? Bel of mail ons. We helpen u graag.", "colophon": "Gemeente Zuiddrecht is een voorbeeldgemeente. Deze website laat zien hoe portaliq en Nextcloud samen werken.", diff --git a/openspec/changes/zuiddrecht-resident-pages-match-the-boards/proposal.md b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/proposal.md new file mode 100644 index 000000000..6cacbb23b --- /dev/null +++ b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/proposal.md @@ -0,0 +1,68 @@ +--- +kind: code +--- + +## Why + +The signed-in pages of the Zuiddrecht example site (`occ portaliq:example-site:install zuiddrecht`, +the resident of `occ portaliq:example-resident:install zuiddrecht`) are rendered by the generic +Mijn omgeving blocks, and the Mijn Zuiddrecht boards draw several of them differently. Measured +on the demo instance (:8097, 6 October 2026): the case cards are Den Haag folder cards with the +step line above a 6px bar and a green status badge, where the board draws a white card with the +number left, a pill tag right (reading "Wacht op u" when the resident must act), a blue underlined +title, an 8px bar and one line "Stap 1 van 5 · klaar uiterlijk 1 november" under it; the open +question is an action row with a date badge, where the board draws a warning-toned card with a +bold title, the case line and a primary "Document toevoegen"; the newest messages are action rows +with "Nieuw" badges and chevrons, where the board lists a title and "vandaag"; the resident menu +has the site's own groups, where the board groups Overzicht and Berichten under Mijn Zuiddrecht; +Mijn zaken draws folder cards where the board draws rows with "Uiterlijk klaar op"; the case page +opens on "Uw zaak" as a heading with the title under it, shows Documenten and Wat er is gebeurd +twice (the documents block and the case screen, the timeline block and the detail card), and has +no "Gegevens" heading or a breadcrumb for Mijn zaken. + +The site serves every portal, so none of this can be the new default. Each difference is a key a +contribution or a portal declares; a page or portal that declares none renders as before. + +## What Changes + +- **Block keys a contribution may declare** (server normalisers keep them only when well + formed): `cases` with `display: compact`, `showAll: true` and `yourTurn: [values]`; a highlight + `tasks` block with `tone: warning|info` and `dueInLine: true`; `inbox` with `display: list`; + `documents` with `upload: true`; `detail` with `label` and `timeline: false`; `citizenCase` with + `display: actions`; a page `record` with `heading: record` and `under: cases`. +- **Portal keys**: `residentMenu.groups` (the menu in the portal's own groups, by section name + or `app:page`; an item not named keeps its place, so nothing becomes unreachable) and + `myCases.display: rows`. Portal schema 0.12.0, register 0.67.0. The Zuiddrecht example site + declares both. +- **The components**: CaseCard `display` compact and row, an info tone on the data badge, + CasesBlock's "Alle zaken" beside its heading, TasksBlock's highlight tone and due line, + InboxBlock's plain list, DocumentsBlock's upload button, DetailCard's heading and timeline + switch, CitizenCase's actions display (the closed-window sentence as a green notice, no status + block, no documents section), ContributionPage's record eyebrow and h1, the breadcrumb's + Mijn zaken crumb, MyCasesPage's rows and tab roles, the resident menu's Overzicht item. +- **Every moved thing keeps a route**: Bezwaar maken and Klacht indienen stay on the overview; + Mijn wijziging opslaan and Deze aanvraag intrekken stay on the case page under Stukken; Toegang + tot zaken and Melding indienen stay in the menu (appended in their own groups when the portal's + layout does not name them). `tests/zuiddrecht-resident-boards.spec.mjs` proves each. +- **Every other portal renders unchanged**: the same spec renders the blocks without the keys + and holds them to the markup they had. + +**Out of scope, named for dossiq** (`lib/Portal/PortalPages.php`, `CitizenManifest.php`): the +keys above on its blocks and pages, `valueLabels.portalTurn.applicant` reading "Wacht op u", a +lookup of the case title for the open question's line. The case page's section order stays +dossiq's (steps, documents, timeline, facts), an open decision. + +## Capabilities + +### Modified Capabilities +- `portal-contribution-contract`: the board keys on blocks and pages. +- `site-resident-menu`: a portal's own menu groups and the cases page display. + +## Impact +- `lib/Contribution/BoardKeys.php` (new), `ListBlockNormaliser.php`, `SchoolBlockKeys.php`, + `PortalBlockResolver.php`, `RecordBlockNormaliser.php`, `lib/Settings/portaliq_register.json`, + `lib/Settings/sites/zuiddrecht.json`. +- `src/site/components/mijn/*`, `src/site/components/e/CitizenCase.vue`, + `src/site/components/collections/DetailCard.vue`, `src/site/pages/collections/ContributionPage.vue`, + `src/site/pages/e/MyCasesPage.vue`, `src/site/lib/residentMenu.js`, `src/site/lib/accountArea.js`, + `src/site/pages/registry.js`, `src/site/App.vue`, strings. diff --git a/openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md new file mode 100644 index 000000000..f15f33b75 --- /dev/null +++ b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md @@ -0,0 +1,53 @@ +# Spec: Portal contribution contract + +## ADDED Requirements + +### Requirement: A contribution may declare the board displays +A contribution MAY declare, and the server MUST keep only when well formed: on a `cases` block +`display: compact` (the number left, a tag right, the title as a link, a bar and one line under +it), `showAll: true` (the link to every case beside the heading whenever the collection has a +page) and `yourTurn` (the values of the collection's `turnField` at which the tag reads the turn's +words in the warning tone instead of the status); on a `tasks` block with `display: highlight` +a `tone` of `warning` or `info` and `dueInLine: true` (the due day joins the sub line); on an +`inbox` block `display: list` (a title and the day in words, no badge); on a `documents` block +`upload: true` (an outlined button beside the heading that adds a document through the case +screen's route); on a `detail` block a `label` (a heading over the facts) and `timeline: false` +(the card draws no history of its own); on a `citizenCase` block `display: actions` (the closed +window sentence as a notice in the ok tone, no status block, no documents section; the fields, +save and withdraw stay); on a page `record` `heading: record` (the record's name is the page's +h1, the page label and the record's reference its eyebrow) and `under: cases` (the breadcrumb +passes through Mijn zaken). A block or page that declares none of these MUST render as before. + +#### Scenario: The keys survive normalisation and nothing else does +@e2e exclude Static: tests/Unit/Contribution/BoardKeysTest.php and tests/zuiddrecht-resident-boards.spec.mjs +- GIVEN a contribution whose blocks declare the keys above, some well formed and some not +- WHEN the contribution is normalised +- THEN each well formed key MUST be on its block or page +- AND a malformed one (an unknown display, a tone outside the two, a non-boolean flag, a non-string turn value) MUST be dropped + +#### Scenario: A compact case card reads the resident's turn +@e2e exclude Static: tests/zuiddrecht-resident-boards.spec.mjs renders the card +- GIVEN a `cases` block with `display: compact` and `yourTurn: ['applicant']` on a collection with `turnField: portalTurn` and the value labelled "Wacht op u" +- WHEN a row with `portalTurn: applicant` is rendered +- THEN the tag MUST read "Wacht op u" in the warning tone +- AND a row at another turn MUST carry its status in the info tone + +#### Scenario: Every moved control keeps a route +@e2e exclude Static: tests/zuiddrecht-resident-boards.spec.mjs +- GIVEN the Zuiddrecht pages as dossiq declares them with the keys above +- WHEN the overview and the case page render +- THEN Bezwaar maken and Klacht indienen MUST be on the overview +- AND Mijn wijziging opslaan and Deze aanvraag intrekken MUST be on the case page +- AND the documents block MUST offer Document toevoegen + +### Requirement: Every other portal renders unchanged +The blocks, pages and menu of a portal that declares none of the board keys MUST render the +markup they rendered before the keys existed: the Den Haag folder card, action rows with badges, +the site's own menu groups with icons, folder cards on Mijn zaken, the page label as h1. + +#### Scenario: The school blocks keep their markup +@e2e exclude Static: tests/zuiddrecht-resident-boards.spec.mjs renders each block without the keys +- GIVEN a `cases`, `tasks`, `inbox`, `documents`, `detail` and `citizenCase` block without the keys +- WHEN each is rendered +- THEN none MUST carry a compact, row, list, tone or actions class +- AND the case card MUST still be a Den Haag case card with its step line above the bar diff --git a/openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/site-resident-menu/spec.md b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/site-resident-menu/spec.md new file mode 100644 index 000000000..67104b89a --- /dev/null +++ b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/site-resident-menu/spec.md @@ -0,0 +1,28 @@ +# Spec: Site resident menu + +## ADDED Requirements + +### Requirement: A portal may lay out the resident menu and its cases page +A portal MAY declare `residentMenu.groups`: an ordered list of groups, each a `title` and its +`items` by name, where a name is a section of the own area (`overview`, `cases`, `tasks`, +`inbox`, `messages`, `news`, `access`, `details`, `account`) or a contributed page as +`app:page`. The menu MUST then show those groups in that order, `overview` as an item that opens +`/mijn` itself, without icons, and MUST append every item the layout does not name in the groups +the site builds itself, so nothing becomes unreachable. A portal MAY declare +`myCases.display: rows`: the page that lists every case then draws one row per case with its +number, title, tag and the day it is due by. Without either key the menu and the page MUST be as +they were. + +#### Scenario: The Zuiddrecht menu +@e2e exclude Static: tests/zuiddrecht-resident-boards.spec.mjs builds the groups from a nav +- GIVEN the Zuiddrecht portal's groups (Mijn Zuiddrecht: overview, inbox; Zaken en taken: cases, tasks; Vragen en meldingen: portaliq:meldingen; Uw gegevens: details, account) +- AND a nav that also holds access +- WHEN the groups are built +- THEN the five declared groups MUST come first, in order, Overzicht linking to /mijn +- AND Toegang tot zaken MUST follow in the site's own Zaken en taken group + +#### Scenario: Mijn zaken as rows +@e2e exclude Static: tests/zuiddrecht-resident-boards.spec.mjs renders the page +- GIVEN a portal with `myCases.display: rows` and a contribution whose cases collection names a due field +- WHEN Mijn zaken renders +- THEN each case MUST be a row with "Uiterlijk klaar op" and the day in words diff --git a/openspec/changes/zuiddrecht-resident-pages-match-the-boards/tasks.md b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/tasks.md new file mode 100644 index 000000000..9e43b667a --- /dev/null +++ b/openspec/changes/zuiddrecht-resident-pages-match-the-boards/tasks.md @@ -0,0 +1,24 @@ +# Tasks: The Zuiddrecht resident pages match the boards + +## 1. Spec +- [x] 1.1 Proposal and the two spec deltas. + +## 2. Server +- [x] 2.1 `lib/Contribution/BoardKeys.php`: the board keys, each kept only when well formed; wired into the list, school, record and resolver normalisers. +- [x] 2.2 Portal schema: `residentMenu.groups` and `myCases.display`; register 0.67.0, portal 0.12.0. +- [x] 2.3 The Zuiddrecht example site declares its menu groups and the rows display. + +## 3. Client +- [x] 3.1 CaseCard compact and row displays, the info tone, the number and due day on the card data. +- [x] 3.2 CasesBlock "Alle zaken" beside the heading; TasksBlock highlight tone and due line; InboxBlock plain list. +- [x] 3.3 DocumentsBlock upload; DetailCard heading and timeline switch; CitizenCase actions display. +- [x] 3.4 ContributionPage record eyebrow and h1; registry and breadcrumb; MyCasesPage rows and tab roles. +- [x] 3.5 Resident menu groups from the portal, with an Overzicht item; App.vue hands them in. + +## 4. Tests +- [x] 4.1 `tests/Unit/Contribution/BoardKeysTest.php`. +- [x] 4.2 `tests/zuiddrecht-resident-boards.spec.mjs` (`check:resident-boards`): each display, each moved thing's route, and the unchanged default render. +- [x] 4.3 `tests/example-site.spec.mjs`: the Zuiddrecht portal's menu groups and rows display. + +## 5. Verify +- [ ] 5.1 Live on :8097 with dossiq declaring the keys (named in the PR), board beside render at 1440 and 390. diff --git a/package.json b/package.json index be2e73e59..4d41b4ebf 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "check:register": "node tests/validate-register.js", "check:registry": "node tests/registry.spec.js", "check:json-strict": "node tests/validate-json-strict.js", - "check:specs": "npm run check:json-strict && npm run check:manifest-v2 && npm run check:register && npm run check:registry && npm run check:site-auth && npm run check:federated-search && npm run check:campaign-tracking && npm run check:traffic-client && npm run check:traffic-summary && npm run check:traffic-paths && npm run check:page-traffic && npm run check:open-portal-site && npm run check:shared-instance && npm run check:connection-registry && npm run check:leaf-integrations && npm run check:page-layout-entry && npm run check:collection-table && npm run check:case-timeline && npm run check:schema-form-file-field && npm run check:timed-task && npm run check:row-action && npm run check:translated-message-notice && npm run check:access-requests && npm run check:news-item-translation && npm run check:news-title-and-newsletter-translation && npm run check:newsletter-title-translation && npm run check:intake-entry && npm run check:access-request-asker && npm run check:registered-details && npm run check:branch-choice && npm run check:account-page && npm run check:signing-dialog && npm run check:form-binding-preview && npm run check:open-record && npm run check:message-box-channel && npm run check:portal-live-regions && npm run check:case-withdraw-screen && npm run check:case-documents-screen && npm run check:my-cases-page && npm run check:my-cases-acting-for && npm run check:proposal-queue-leaf && npm run check:portal-theme-choice && npm run check:site-head && npm run check:page-history && npm run check:media-library && npm run check:broker-login && npm run check:portal-signin-settings && npm run check:site-shell-blocks && npm run check:site-grid && npm run check:site-regions && npm run check:page-editor && npm run check:page-editor-regions && npm run check:notices && npm run check:site-edit-mode && npm run check:site-page-tree && npm run check:branch-in-effect && npm run check:idle-session && npm run check:publication-documents && npm run check:woo-entry-points && npm run check:attached-actions && npm run check:my-dossiers && npm run check:staff-account-screens && npm run check:site-signed-in-shell && npm run check:site-resident-menu && npm run check:create-names-its-action && npm run check:site-shared-dossier && npm run check:guest-action-page && npm run check:ways-in-screens && npm run check:visible-when-local && npm run check:schema-form && npm run check:propose-change && npm run check:site-collections && npm run check:collection-loader && npm run check:rich-text && npm run check:tasks-page && npm run check:site-inbox-pages && npm run check:embed-copy && npm run check:embed-height && npm run check:service-worker && npm run check:install-banner && npm run check:embed-frame && npm run check:array-cells && npm run check:collection-groups && npm run check:record-page && npm run check:case-type-portal-header && npm run check:inbox-unread && npm run check:inbox-attachments && npm run check:inbox-open-link && npm run check:inbox-delete && npm run check:value-labels && npm run check:admin-menu-access && npm run check:news-list-refresh && npm run check:site-navigation && npm run check:site-theme-bridge && npm run check:mijn-components && npm run check:mijn-cases && npm run check:mijn-home && npm run check:mijn-lists && npm run check:mijn-wave6 && npm run check:mijn-documents && npm run check:site-form-fields && npm run check:widget-registry && npm run check:action-summary-sentence && npm run check:site-school-blocks && npm run check:site-school-displays && npm run check:widget-palette && npm run check:editor-text-toolbar && npm run check:widget-tokens && npm run check:portal-home-page && npm run check:claim-invitation && npm run check:site-chrome && npm run check:resident-menu-badges && npm run check:example-site && npm run check:example-resident && npm run check:portal-public-catalogue && npm run check:calendar-timetable && npm run check:site-messages-per-record && npm run check:site-pixel-match && npm run check:site-woo-pages", + "check:specs": "npm run check:json-strict && npm run check:manifest-v2 && npm run check:register && npm run check:registry && npm run check:site-auth && npm run check:federated-search && npm run check:campaign-tracking && npm run check:traffic-client && npm run check:traffic-summary && npm run check:traffic-paths && npm run check:page-traffic && npm run check:open-portal-site && npm run check:shared-instance && npm run check:connection-registry && npm run check:leaf-integrations && npm run check:page-layout-entry && npm run check:collection-table && npm run check:case-timeline && npm run check:schema-form-file-field && npm run check:timed-task && npm run check:row-action && npm run check:translated-message-notice && npm run check:access-requests && npm run check:news-item-translation && npm run check:news-title-and-newsletter-translation && npm run check:newsletter-title-translation && npm run check:intake-entry && npm run check:access-request-asker && npm run check:registered-details && npm run check:branch-choice && npm run check:account-page && npm run check:signing-dialog && npm run check:form-binding-preview && npm run check:open-record && npm run check:message-box-channel && npm run check:portal-live-regions && npm run check:case-withdraw-screen && npm run check:case-documents-screen && npm run check:my-cases-page && npm run check:my-cases-acting-for && npm run check:proposal-queue-leaf && npm run check:portal-theme-choice && npm run check:site-head && npm run check:page-history && npm run check:media-library && npm run check:broker-login && npm run check:portal-signin-settings && npm run check:site-shell-blocks && npm run check:site-grid && npm run check:site-regions && npm run check:page-editor && npm run check:page-editor-regions && npm run check:notices && npm run check:site-edit-mode && npm run check:site-page-tree && npm run check:branch-in-effect && npm run check:idle-session && npm run check:publication-documents && npm run check:woo-entry-points && npm run check:attached-actions && npm run check:my-dossiers && npm run check:staff-account-screens && npm run check:site-signed-in-shell && npm run check:site-resident-menu && npm run check:create-names-its-action && npm run check:site-shared-dossier && npm run check:guest-action-page && npm run check:ways-in-screens && npm run check:visible-when-local && npm run check:schema-form && npm run check:propose-change && npm run check:site-collections && npm run check:collection-loader && npm run check:rich-text && npm run check:tasks-page && npm run check:site-inbox-pages && npm run check:embed-copy && npm run check:embed-height && npm run check:service-worker && npm run check:install-banner && npm run check:embed-frame && npm run check:array-cells && npm run check:collection-groups && npm run check:record-page && npm run check:case-type-portal-header && npm run check:inbox-unread && npm run check:inbox-attachments && npm run check:inbox-open-link && npm run check:inbox-delete && npm run check:value-labels && npm run check:admin-menu-access && npm run check:news-list-refresh && npm run check:site-navigation && npm run check:site-theme-bridge && npm run check:mijn-components && npm run check:mijn-cases && npm run check:mijn-home && npm run check:mijn-lists && npm run check:mijn-wave6 && npm run check:mijn-documents && npm run check:site-form-fields && npm run check:widget-registry && npm run check:action-summary-sentence && npm run check:site-school-blocks && npm run check:site-school-displays && npm run check:widget-palette && npm run check:editor-text-toolbar && npm run check:widget-tokens && npm run check:portal-home-page && npm run check:claim-invitation && npm run check:site-chrome && npm run check:resident-menu-badges && npm run check:example-site && npm run check:example-resident && npm run check:portal-public-catalogue && npm run check:calendar-timetable && npm run check:site-messages-per-record && npm run check:resident-boards && npm run check:site-pixel-match && npm run check:site-woo-pages", "prepare": "git config core.hooksPath .githooks || true", "build:site": "NODE_ENV=production webpack --config webpack.site.js --progress", "postbuild:site": "node scripts/check-single-dexie.js && node scripts/check-site-chunks.js", @@ -158,7 +158,8 @@ "check:site-chrome": "node --test tests/site-chrome.spec.mjs", "check:resident-menu-badges": "node --test tests/resident-menu-badges.spec.mjs", "check:example-site": "node --test tests/example-site.spec.mjs", - "check:example-resident": "node --test tests/example-resident.spec.mjs" + "check:example-resident": "node --test tests/example-resident.spec.mjs", + "check:resident-boards": "node --test tests/zuiddrecht-resident-boards.spec.mjs" }, "browserslist": [ "extends @nextcloud/browserslist-config" diff --git a/src/shared/i18n/en.json b/src/shared/i18n/en.json index e888f16c5..e5b7eec66 100644 --- a/src/shared/i18n/en.json +++ b/src/shared/i18n/en.json @@ -56,7 +56,6 @@ "Save my change": "Save my change", "Your change has been saved.": "Your change has been saved.", "The change could not be saved. Please try again.": "The change could not be saved. Please try again.", - "Download": "Download", "Documents": "Documents", "Add a document": "Add a document", "{name} has been added to your case.": "{name} has been added to your case.", @@ -200,7 +199,8 @@ "Sent by you": "Sent by you", "There are no documents on this case yet.": "There are no documents on this case yet.", "The document could not be opened. Try again later.": "The document could not be opened. Try again later.", - "Only on this demo. No password is asked.": "Only on this demo. No password is asked.", + "Overview": "Overview", + "Due by": "Due by", "My cases": "My cases", "Open ({count})": "Open ({count})", "Closed ({count})": "Closed ({count})", @@ -396,5 +396,7 @@ "{count} new": "{count} new", "{count} open": "{count} open", "One less": "One less", - "One more": "One more" + "One more": "One more", + "Download": "Download", + "Only on this demo. No password is asked.": "Only on this demo. No password is asked." } diff --git a/src/shared/i18n/nl.json b/src/shared/i18n/nl.json index 08e0889ee..2147faeda 100644 --- a/src/shared/i18n/nl.json +++ b/src/shared/i18n/nl.json @@ -56,7 +56,6 @@ "Save my change": "Mijn wijziging opslaan", "Your change has been saved.": "Uw wijziging is opgeslagen.", "The change could not be saved. Please try again.": "De wijziging kon niet worden opgeslagen. Probeer het opnieuw.", - "Download": "Downloaden", "Documents": "Documenten", "Add a document": "Document toevoegen", "{name} has been added to your case.": "{name} is aan uw zaak toegevoegd.", @@ -200,7 +199,8 @@ "Sent by you": "Door u gestuurd", "There are no documents on this case yet.": "Er staan nog geen documenten bij deze zaak.", "The document could not be opened. Try again later.": "Het document kon niet worden geopend. Probeer het later opnieuw.", - "Only on this demo. No password is asked.": "Alleen op deze demo. Er wordt geen wachtwoord gevraagd.", + "Overview": "Overzicht", + "Due by": "Uiterlijk klaar op", "My cases": "Mijn zaken", "Open ({count})": "Lopend ({count})", "Closed ({count})": "Afgerond ({count})", @@ -396,5 +396,7 @@ "{count} new": "{count} nieuw", "{count} open": "{count} open", "One less": "Eén minder", - "One more": "Eén meer" + "One more": "Eén meer", + "Download": "Downloaden", + "Only on this demo. No password is asked.": "Alleen op deze demo. Er wordt geen wachtwoord gevraagd." } diff --git a/src/site/App.vue b/src/site/App.vue index 6296f7ed6..039bdd3ac 100644 --- a/src/site/App.vue +++ b/src/site/App.vue @@ -963,6 +963,7 @@ export default { this.unreadCount, this.hrefForRoute, this.recordRows, + this.site?.residentMenu?.groups, ) : [], menus: headerMenusOf(this.menus), @@ -1053,6 +1054,8 @@ export default { this.unreadCount, this.hrefForRoute, this.recordRows, + // The portal's own groups (zuiddrecht-resident-pages-match-the-boards). + this.site?.residentMenu?.groups, ) }, diff --git a/src/site/components/collections/DetailCard.vue b/src/site/components/collections/DetailCard.vue index 0f08c1bb8..e3d494b6b 100644 --- a/src/site/components/collections/DetailCard.vue +++ b/src/site/components/collections/DetailCard.vue @@ -18,6 +18,15 @@

{{ notice }}

+ + + {{ label }} + @@ -96,7 +105,7 @@ :locale="locale" /> {{ t('This case is not yours.') }}

-
+
+

@@ -50,11 +58,31 @@

+
+ +

{{ windowReason }}

+

- {{ (writableSet.window && writableSet.window.reason) || '' }} + {{ windowReason }}

@@ -79,7 +107,10 @@ {{ t('Save my change') }} -
+

{{ data.documentsLabel || t('Documents') }}

@@ -202,6 +233,8 @@ export default { props: { /** The manifest collection the case lives in (`{id, register, schema, ...}`). */ collection: { type: Object, required: true }, + /** The page block (`display: actions` keeps only the actions). */ + block: { type: Object, default: null }, /** The case row selected in the table; `_mandate.id` reads it under that mandate. */ row: { type: Object, default: null }, /** Say nothing until a case is chosen: a detail card on the page already asks. */ @@ -254,6 +287,25 @@ export default { return this.writableSet.window?.open === true }, + /** + * @return {string} Why the window is shut, in the server's words. + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + windowReason() { + return (this.writableSet.window && this.writableSet.window.reason) || '' + }, + + /** + * Whether this screen keeps only the actions: the status and the + * documents are other blocks' on the page. + * + * @return {boolean} + * @spec openspec/changes/zuiddrecht-resident-pages-match-the-boards/specs/portal-contribution-contract/spec.md#requirement-a-contribution-may-declare-the-board-displays + */ + actionsOnly() { + return this.block?.display === 'actions' + }, + documentsOpen() { return this.writableSet.documents?.open === true }, @@ -522,4 +574,41 @@ export default { ); font-weight: var(--utrecht-typography-weight-scale-bold-font-weight, bold); } + +/* The closed window as a notice in the ok tone: a tick and one sentence. */ +.pq-case-ok { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + border: 1px solid + var( + --utrecht-alert-ok-border-color, + var(--nldesign-color-success, currentcolor) + ); + border-radius: var(--utrecht-alert-border-radius, 0.25rem); + background-color: var( + --utrecht-alert-ok-background-color, + var( + --nldesign-component-status-badge-success-background-color, + rgba(var(--nldesign-color-success-rgb, 57, 135, 12), 0.12) + ) + ); + color: var(--utrecht-alert-ok-color, inherit); +} + +.pq-case-ok__icon { + flex: none; + inline-size: 1.375rem; + block-size: 1.375rem; + color: var( + --utrecht-alert-icon-ok-color, + var(--nldesign-color-success, currentcolor) + ); +} + +.pq-case-ok__text { + margin: 0; + font-size: 1.125rem; +} diff --git a/src/site/components/mijn/CaseCard.vue b/src/site/components/mijn/CaseCard.vue index f59ce0d7a..bcd4e582e 100644 --- a/src/site/components/mijn/CaseCard.vue +++ b/src/site/components/mijn/CaseCard.vue @@ -11,6 +11,14 @@ position as text ("Stap 2 van 4") with a decorative bar, the answer date and whose turn it is. Colour never carries the status alone. + Two more displays draw the Mijn Zuiddrecht boards + (zuiddrecht-resident-pages-match-the-boards), chosen by `display`: + `compact` is a white card with the number left and a tag right, the title + as a link, a bar and one line under it ("Stap 1 van 5 · klaar uiterlijk 1 + november"); `row` is one line per case with its number and title, the tag + and the day it is due by. The tag reads the turn's words in the warning + tone when the resident must act, else the status in the info tone. + The title is the card's ONE control, so the control's name is the case title. It stretches over the whole card (as Den Haag's own action link does), so the card is one target. With a route it is a link with a real @@ -18,8 +26,97 @@ text. -->