feat(dashboard): admin "Beheer" section linking the admin pages
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:
@@ -10,6 +10,8 @@ import { TaskListComponent } from '@shared/ui/task-list/task-list.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { ADMIN_LINKS } from '@shared/layout/admin-links';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
import { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';
|
||||
import { AanvraagBlockComponent } from '@registratie/ui/aanvraag-block/aanvraag-block.component';
|
||||
@@ -185,6 +187,22 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
|
||||
@if (adminLinks().length) {
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.beheer">Beheer</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (link of adminLinks(); track link.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="link.label"
|
||||
[subtitle]="link.description"
|
||||
[to]="link.to"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
</app-page-shell>
|
||||
`,
|
||||
@@ -192,8 +210,13 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
export class DashboardPage {
|
||||
protected store = inject(BigProfileStore);
|
||||
private apps = inject(ApplicationsStore);
|
||||
private access = inject(AccessStore);
|
||||
private router = inject(Router);
|
||||
|
||||
/** Admin pages the current principal may reach — capability-gated (never role-derived),
|
||||
the same source + filter the site header uses. Empty for a non-admin → section hidden. */
|
||||
protected adminLinks = computed(() => ADMIN_LINKS.filter((l) => this.access.can(l.cap)));
|
||||
|
||||
/** Pure view mapping for a submitted aanvraag → CIBG aanvragen-row fields. */
|
||||
protected submittedRow = submittedRow;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user