feat(dashboard): admin "Beheer" section linking the admin pages
CI / frontend (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / semgrep (push) Has been cancelled
CI / storybook-a11y (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled

Add a capability-gated Beheer section to the dashboard listing the admin pages
(Huisstijl, Stamdata, Aanvragen), each shown when the principal holds its
capability — so admin pages are discoverable, not URL-only. Extract the admin
link list to shared/layout/admin-links.ts as one source of truth, reused by the
site header (was ADMIN_NAV_ITEMS) and the new section. Capability-gated, never
role-derived (PRD-0002).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 12:51:00 +02:00
co-authored by Claude Opus 4.8
parent 5b6023045e
commit 63fdacf622
5 changed files with 161 additions and 80 deletions
+34
View File
@@ -0,0 +1,34 @@
import { Capability } from '@shared/domain/capability';
/** One admin page: its label, a short description, its route, and the capability that
gates it. Single source of truth for "which admin pages exist" — consumed by the site
header's admin nav AND the dashboard's Beheer section, both filtered by `AccessStore.can`.
Capability-gated, never role-derived (PRD-0002 §6): the FE only mirrors server-resolved
capabilities, so this stays correct when real authz replaces the X-Role stub. */
export interface AdminLink {
readonly label: string;
readonly description: string;
readonly to: string;
readonly cap: Capability;
}
export const ADMIN_LINKS: readonly AdminLink[] = [
{
label: $localize`:@@header.nav.huisstijl:Huisstijl`,
description: $localize`:@@admin.link.huisstijl.desc:Organisatiesjablonen voor brieven beheren`,
to: '/brief/huisstijl',
cap: 'orgtemplate:edit',
},
{
label: $localize`:@@header.nav.stamdata:Stamdata`,
description: $localize`:@@admin.link.stamdata.desc:Business-tabellen onderhouden`,
to: '/beheer/stamdata',
cap: 'stamdata:edit',
},
{
label: $localize`:@@header.nav.zaken:Aanvragen`,
description: $localize`:@@admin.link.zaken.desc:Alle aanvragen bekijken en beheren`,
to: '/beheer/zaken',
cap: 'cases:manage',
},
];
@@ -4,7 +4,7 @@ import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/ro
import { filter, map } from 'rxjs/operators';
import { SESSION_PORT } from '@shared/application/session.port';
import { AccessStore } from '@shared/application/access.store';
import { Capability } from '@shared/domain/capability';
import { ADMIN_LINKS } from '@shared/layout/admin-links';
import { BreadcrumbComponent } from '@shared/layout/breadcrumb/breadcrumb.component';
import { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';
@@ -20,26 +20,6 @@ const NAV_ITEMS: readonly HeaderNavItem[] = [
{ label: $localize`:@@header.nav.inschrijven:Inschrijven`, to: '/registreren' },
];
/** Admin-only nav, shown only when `/me` grants the matching capability — the pages
are otherwise reachable by URL alone. */
const ADMIN_NAV_ITEMS: readonly (HeaderNavItem & { readonly cap: Capability })[] = [
{
label: $localize`:@@header.nav.huisstijl:Huisstijl`,
to: '/brief/huisstijl',
cap: 'orgtemplate:edit',
},
{
label: $localize`:@@header.nav.stamdata:Stamdata`,
to: '/beheer/stamdata',
cap: 'stamdata:edit',
},
{
label: $localize`:@@header.nav.zaken:Aanvragen`,
to: '/beheer/zaken',
cap: 'cases:manage',
},
];
/** Organism: CIBG Huisstijl site header — logo block, robijn titlebar (breadcrumb +
user menu), horizontal nav. ponytail: text wordmark, not the licensed Rijksoverheid
beeldmerk; no search box (no search feature yet). */
@@ -131,7 +111,7 @@ export class SiteHeaderComponent {
private access = inject(AccessStore);
/** Injecting AccessStore here also warms `/me` at app start (the header renders on
every page), so the admin routes' guard usually finds caps already resolved. */
protected adminItems = computed(() => ADMIN_NAV_ITEMS.filter((i) => this.access.can(i.cap)));
protected adminItems = computed(() => ADMIN_LINKS.filter((i) => this.access.can(i.cap)));
readonly session = computed(() => this.sessionPort?.session() ?? null);
private url = toSignal(