refactor: split dashboard.page.ts into per-concern sections
Each dashboard section (Mijn aanvragen, Wat moet ik regelen, Mijn registratie, Specialismen, Wat wilt u doen, Beheer) now owns its own store access, async state, and template. DashboardPage becomes pure composition. Extract the repeated RemoteData Success-narrowing pattern into successOf() and the dashboard sort/split logic into sortForDashboard/concepten/ingediend, both with tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { submittedRow, detailRows, purposeLabel, statusLabel, TYPE_LABELS } from './aanvraag-view';
|
||||
import {
|
||||
submittedRow,
|
||||
detailRows,
|
||||
purposeLabel,
|
||||
statusLabel,
|
||||
TYPE_LABELS,
|
||||
sortForDashboard,
|
||||
concepten,
|
||||
ingediend,
|
||||
} from './aanvraag-view';
|
||||
import { Aanvraag } from './aanvraag';
|
||||
|
||||
const base: Omit<Aanvraag, 'status'> = {
|
||||
@@ -114,3 +123,40 @@ describe('detailRows', () => {
|
||||
expect(rows.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortForDashboard / concepten / ingediend', () => {
|
||||
const withStatus = (id: string, tag: Aanvraag['status']['tag']): Aanvraag => ({
|
||||
...base,
|
||||
id,
|
||||
status:
|
||||
tag === 'Concept'
|
||||
? { tag, stepIndex: 0, stepCount: 1 }
|
||||
: tag === 'Afgewezen' || tag === 'MeerInfoGevraagd'
|
||||
? { tag, referentie: 'R', reden: 'x' }
|
||||
: tag === 'InBehandeling'
|
||||
? { tag, referentie: 'R', manual: false }
|
||||
: { tag, referentie: 'R' },
|
||||
});
|
||||
|
||||
it('sorts Concept, then still-open, then resolved last', () => {
|
||||
const goedgekeurd = withStatus('1', 'Goedgekeurd');
|
||||
const concept = withStatus('2', 'Concept');
|
||||
const inBehandeling = withStatus('3', 'InBehandeling');
|
||||
const sorted = sortForDashboard([goedgekeurd, concept, inBehandeling]);
|
||||
expect(sorted.map((a) => a.id)).toEqual(['2', '3', '1']);
|
||||
});
|
||||
|
||||
it('does not mutate the input array', () => {
|
||||
const list = [withStatus('1', 'Goedgekeurd'), withStatus('2', 'Concept')];
|
||||
const copy = [...list];
|
||||
sortForDashboard(list);
|
||||
expect(list).toEqual(copy);
|
||||
});
|
||||
|
||||
it('concepten/ingediend split on the Concept tag', () => {
|
||||
const concept = withStatus('1', 'Concept');
|
||||
const ingediendItem = withStatus('2', 'Ingediend');
|
||||
expect(concepten([concept, ingediendItem])).toEqual([concept]);
|
||||
expect(ingediend([concept, ingediendItem])).toEqual([ingediendItem]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,3 +106,30 @@ export function detailRows(a: Aanvraag): { key: string; value: string }[] {
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Dashboard sort order: still-open work first (Concept, then submitted-and-pending),
|
||||
resolved aanvragen (Goedgekeurd/Afgewezen) last. Within a group, order is stable
|
||||
(the sort is by rank only). */
|
||||
const SORT_RANK: Record<AanvraagStatus['tag'], number> = {
|
||||
Concept: 0,
|
||||
Ingediend: 1,
|
||||
InBehandeling: 1,
|
||||
MeerInfoGevraagd: 1,
|
||||
Goedgekeurd: 2,
|
||||
Afgewezen: 2,
|
||||
};
|
||||
|
||||
/** The dashboard's "Mijn aanvragen" ordering: open work before resolved cases. */
|
||||
export function sortForDashboard(aanvragen: Aanvraag[]): Aanvraag[] {
|
||||
return aanvragen.slice().sort((a, b) => SORT_RANK[a.status.tag] - SORT_RANK[b.status.tag]);
|
||||
}
|
||||
|
||||
/** A Concept ("lopende aanvraag") renders as a melding above the list; a submitted
|
||||
aanvraag as a keuzelijst item — the two shapes need different HTML contexts. */
|
||||
export function concepten(aanvragen: Aanvraag[]): Aanvraag[] {
|
||||
return aanvragen.filter((a) => a.status.tag === 'Concept');
|
||||
}
|
||||
|
||||
export function ingediend(aanvragen: Aanvraag[]): Aanvraag[] {
|
||||
return aanvragen.filter((a) => a.status.tag !== 'Concept');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
@@ -66,9 +67,7 @@ export class AanvraagDetailPage {
|
||||
protected find = (list: Aanvraag[]): Aanvraag | undefined => list.find((a) => a.id === this.id);
|
||||
protected rows = detailRows;
|
||||
|
||||
/** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */
|
||||
protected readonly aanvragen = computed(() => {
|
||||
const rd = this.store.aanvragen();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
/** `successOf`: `<ng-template>` can't inherit a generic from a sibling host input,
|
||||
so the Success value is unwrapped here instead of through `let-`. */
|
||||
protected readonly aanvragen = computed(() => successOf(this.store.aanvragen()));
|
||||
}
|
||||
|
||||
@@ -1,47 +1,25 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
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 { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||
import { FLAG_INSCHRIJVING_OPEN } from '@shared/domain/feature-flag';
|
||||
import { ADMIN_LINKS } from '../../shell/nav.config';
|
||||
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';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import { submittedRow } from '@registratie/domain/aanvraag-view';
|
||||
import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
import { MijnAanvragenSection } from './dashboard/mijn-aanvragen.section';
|
||||
import { WatMoetIkRegelenSection } from './dashboard/wat-moet-ik-regelen.section';
|
||||
import { MijnRegistratieSection } from './dashboard/mijn-registratie.section';
|
||||
import { SpecialismenSection } from './dashboard/specialismen.section';
|
||||
import { WatWiltUDoenSection } from './dashboard/wat-wilt-u-doen.section';
|
||||
import { BeheerLinksSection } from './dashboard/beheer-links.section';
|
||||
|
||||
/** Page:"Mijn overzicht" — the portal home, following the NL Design System
|
||||
"Mijn omgeving" pattern (side nav +"Wat moet ik regelen" +"Mijn zaken"). */
|
||||
/** Page: "Mijn overzicht" — the portal home, following the NL Design System "Mijn
|
||||
omgeving" pattern. Composition only: each section below answers its own data
|
||||
question (own store, own async state) — see `ui/dashboard/*.section.ts`. */
|
||||
@Component({
|
||||
selector: 'app-dashboard-page',
|
||||
imports: [
|
||||
PageShellComponent,
|
||||
HeadingComponent,
|
||||
AlertComponent,
|
||||
SkeletonComponent,
|
||||
DataRowComponent,
|
||||
DataBlockComponent,
|
||||
TaskListComponent,
|
||||
ApplicationListComponent,
|
||||
ApplicationLinkComponent,
|
||||
...ASYNC,
|
||||
RegistrationSummaryComponent,
|
||||
RegistrationTableComponent,
|
||||
AanvraagBlockComponent,
|
||||
MijnAanvragenSection,
|
||||
WatMoetIkRegelenSection,
|
||||
MijnRegistratieSection,
|
||||
SpecialismenSection,
|
||||
WatWiltUDoenSection,
|
||||
BeheerLinksSection,
|
||||
],
|
||||
template: `
|
||||
<app-page-shell
|
||||
@@ -51,290 +29,14 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken."
|
||||
>
|
||||
<div class="app-stack">
|
||||
@if (cancelError(); as err) {
|
||||
<app-alert type="error">{{ err }}</app-alert>
|
||||
}
|
||||
@if (aanvragen().length) {
|
||||
<section>
|
||||
@for (a of concepten(); track a.id) {
|
||||
<app-aanvraag-block
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[aanvraag]="a"
|
||||
(resume)="resume(a)"
|
||||
(cancel)="cancelAanvraag(a)"
|
||||
/>
|
||||
}
|
||||
@if (ingediend().length) {
|
||||
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen"
|
||||
>Mijn aanvragen</app-heading
|
||||
>
|
||||
<app-application-list>
|
||||
@for (a of ingediend(); track a.id) {
|
||||
@let row = submittedRow(a);
|
||||
<li
|
||||
app-application-link
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[heading]="row.heading"
|
||||
[subtitle]="row.subtitle"
|
||||
[status]="row.status"
|
||||
[to]="'/aanvraag/' + a.id"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (store.pendingHerregistratie()) {
|
||||
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie"
|
||||
>Uw herregistratie-aanvraag is in behandeling.</app-alert
|
||||
>
|
||||
}
|
||||
|
||||
<app-async [data]="store.profile()" (retryClicked)="store.reloadProfile()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (profile(); as p) {
|
||||
@let tasks = tasksFor(p.registration);
|
||||
|
||||
<section>
|
||||
@if (tasks.length) {
|
||||
<app-task-list
|
||||
class="app-section"
|
||||
i18n-listHeading="@@dashboard.watMoetIkRegelen"
|
||||
listHeading="Wat moet ik regelen"
|
||||
[tasks]="tasks"
|
||||
/>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
|
||||
>Wat moet ik regelen</app-heading
|
||||
>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
|
||||
U heeft op dit moment niets openstaan.
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
|
||||
>Mijn registratie</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="p.registration" />
|
||||
</div>
|
||||
<app-data-block
|
||||
class="app-section"
|
||||
i18n-heading="@@dashboard.persoonsgegevens"
|
||||
heading="Persoonsgegevens (BRP)"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.straat"
|
||||
key="Straat"
|
||||
[value]="p.person.adres.straat"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.postcode"
|
||||
key="Postcode"
|
||||
[value]="p.person.adres.postcode"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.woonplaats"
|
||||
key="Woonplaats"
|
||||
[value]="p.person.adres.woonplaats"
|
||||
></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="6" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.specialismen"
|
||||
>Specialismen en aantekeningen</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-async [data]="store.aantekeningen()" (retryClicked)="store.reloadAantekeningen()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (aantekeningen(); as r) {
|
||||
<app-registration-table [rows]="r" />
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncEmpty>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">
|
||||
U heeft nog geen specialismen of aantekeningen.
|
||||
</p>
|
||||
</ng-template>
|
||||
</app-async>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (a of acties(); track a.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="a.titel"
|
||||
[subtitle]="a.tekst"
|
||||
[cta]="a.actie"
|
||||
[to]="a.to"
|
||||
></li>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
<app-mijn-aanvragen-section />
|
||||
<app-wat-moet-ik-regelen-section />
|
||||
<app-mijn-registratie-section />
|
||||
<app-specialismen-section />
|
||||
<app-wat-wilt-u-doen-section />
|
||||
<app-beheer-links-section />
|
||||
</div>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class DashboardPage {
|
||||
protected store = inject(BigProfileStore);
|
||||
private apps = inject(AanvragenStore);
|
||||
private access = inject(AccessStore);
|
||||
private flags = inject(FeatureFlagStore);
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
// Re-fetch on each visit so server-computed auto-approval transitions show up
|
||||
// (Concept → In behandeling → Goedgekeurd after the processing window).
|
||||
this.apps.reload();
|
||||
}
|
||||
|
||||
/** The user's aanvragen, sorted Concept → In behandeling → resolved. Empty →
|
||||
the"Mijn aanvragen" section is hidden (see template). */
|
||||
protected aanvragen = computed<Aanvraag[]>(() => {
|
||||
const rd = this.apps.aanvragen();
|
||||
if (rd.tag !== 'Success') return [];
|
||||
const order: Record<Aanvraag['status']['tag'], number> = {
|
||||
Concept: 0,
|
||||
Ingediend: 1,
|
||||
InBehandeling: 1,
|
||||
MeerInfoGevraagd: 1,
|
||||
Goedgekeurd: 2,
|
||||
Afgewezen: 2,
|
||||
};
|
||||
return rd.value.slice().sort((a, b) => order[a.status.tag] - order[b.status.tag]);
|
||||
});
|
||||
/** A Concept ("lopende aanvraag") renders as a melding above the list; the rest
|
||||
as keuzelijst items — the two shapes need different HTML contexts. */
|
||||
protected concepten = computed(() => this.aanvragen().filter((a) => a.status.tag === 'Concept'));
|
||||
protected ingediend = computed(() => this.aanvragen().filter((a) => a.status.tag !== 'Concept'));
|
||||
|
||||
private readonly resumeRoutes: Record<AanvraagType, string> = {
|
||||
registratie: '/registreren',
|
||||
herregistratie: '/herregistratie',
|
||||
intake: '/intake',
|
||||
};
|
||||
protected resume(a: Aanvraag) {
|
||||
void this.router.navigate([this.resumeRoutes[a.type]], { queryParams: { aanvraag: a.id } });
|
||||
}
|
||||
protected cancelAanvraag(a: Aanvraag) {
|
||||
void this.apps.cancel(a.id);
|
||||
}
|
||||
/** RB-20: the message from a failed cancel, rendered above the list. */
|
||||
protected cancelError = computed(() => this.apps.lastError());
|
||||
|
||||
/** Server-computed eligibility (rendered, not recomputed). */
|
||||
private readonly eligible = computed(() => {
|
||||
const d = this.store.decisions();
|
||||
return d.tag === 'Success' && d.value.eligibleForHerregistratie;
|
||||
});
|
||||
|
||||
protected tasksFor(reg: Registration) {
|
||||
return tasksFromProfile(reg, this.eligible());
|
||||
}
|
||||
|
||||
/** Typed narrowing for the `<app-async>` loaded slot — `<ng-template>`'s own
|
||||
context can't inherit a generic from a sibling host input (Angular only infers
|
||||
a structural directive's type parameter from an input on that same node), so
|
||||
the Success value is unwrapped here instead of through `let-`. */
|
||||
protected readonly profile = computed(() => {
|
||||
const rd = this.store.profile();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
protected readonly aantekeningen = computed(() => {
|
||||
const rd = this.store.aantekeningen();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
|
||||
/** Primary transactional actions, as an "aanvragen" list (see CIBG's
|
||||
componenten/aanvragen). The core portal sections live in the header nav now;
|
||||
the teaching pages (concepts/brief) are only reachable from here. */
|
||||
private readonly allActies = [
|
||||
{
|
||||
to: '/registreren',
|
||||
titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,
|
||||
tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,
|
||||
actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,
|
||||
},
|
||||
{
|
||||
to: '/herregistratie',
|
||||
titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,
|
||||
tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,
|
||||
actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,
|
||||
},
|
||||
{
|
||||
to: '/intake',
|
||||
titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,
|
||||
tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,
|
||||
actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,
|
||||
},
|
||||
{
|
||||
to: '/registratie',
|
||||
titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,
|
||||
tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,
|
||||
actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,
|
||||
},
|
||||
{
|
||||
to: '/concepts',
|
||||
titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,
|
||||
tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,
|
||||
actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,
|
||||
},
|
||||
{
|
||||
to: '/brief',
|
||||
titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,
|
||||
tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,
|
||||
actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,
|
||||
},
|
||||
];
|
||||
|
||||
/** Hide the "Inschrijven" action when self-service registration is flagged off (WP-47). */
|
||||
protected readonly acties = computed(() =>
|
||||
this.allActies.filter(
|
||||
(a) => a.to !== '/registreren' || this.flags.enabled(FLAG_INSCHRIJVING_OPEN),
|
||||
),
|
||||
);
|
||||
}
|
||||
export class DashboardPage {}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { ADMIN_LINKS } from '../../../shell/nav.config';
|
||||
|
||||
/** Section: "Beheer" — the 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 → the section renders nothing (see the page). */
|
||||
@Component({
|
||||
selector: 'app-beheer-links-section',
|
||||
imports: [HeadingComponent, ApplicationListComponent, ApplicationLinkComponent],
|
||||
template: `
|
||||
@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>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class BeheerLinksSection {
|
||||
private access = inject(AccessStore);
|
||||
protected adminLinks = computed(() => ADMIN_LINKS.filter((l) => this.access.can(l.cap)));
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig } from '@storybook/angular';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { MijnAanvragenSection } from './mijn-aanvragen.section';
|
||||
import { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag } from '@registratie/domain/aanvraag';
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
import { loading, success, failure } from '@shared/testing/remote-data';
|
||||
|
||||
const base = {
|
||||
id: 'a1',
|
||||
type: 'herregistratie',
|
||||
documentIds: [],
|
||||
createdAt: '2026-06-28T10:00:00Z',
|
||||
updatedAt: '2026-06-28T10:05:00Z',
|
||||
submittedAt: '2026-06-28T10:05:00Z',
|
||||
} satisfies Omit<Aanvraag, 'status'>;
|
||||
|
||||
const concept: Aanvraag = { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } };
|
||||
const ingediend: Aanvraag = {
|
||||
...base,
|
||||
id: 'a2',
|
||||
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false },
|
||||
};
|
||||
|
||||
/** Minimal store stand-in — only the members the section's template reads. */
|
||||
function storeStub(aanvragen: RemoteData<Error | undefined, Aanvraag[]>, lastError = '') {
|
||||
return {
|
||||
aanvragen: () => aanvragen,
|
||||
reload: () => {},
|
||||
cancel: async () => {},
|
||||
lastError: () => lastError,
|
||||
};
|
||||
}
|
||||
|
||||
const meta: Meta<MijnAanvragenSection> = {
|
||||
title: 'Domein/Registratie/Dashboard/Mijn Aanvragen',
|
||||
component: MijnAanvragenSection,
|
||||
decorators: [applicationConfig({ providers: [provideRouter([])] })],
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<MijnAanvragenSection>;
|
||||
|
||||
export const Loading: Story = {
|
||||
decorators: [
|
||||
applicationConfig({ providers: [{ provide: AanvragenStore, useValue: storeStub(loading()) }] }),
|
||||
],
|
||||
};
|
||||
export const WithConceptAndSubmitted: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: AanvragenStore, useValue: storeStub(success([concept, ingediend])) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Empty: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: AanvragenStore, useValue: storeStub(success([])) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const CancelFailed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
{
|
||||
provide: AanvragenStore,
|
||||
useValue: storeStub(
|
||||
success([concept]),
|
||||
$localize`:@@dashboard.cancel.failed:Verwijderen is niet gelukt.`,
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Failed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: AanvragenStore, useValue: storeStub(failure(new Error('offline'))) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.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 { AanvragenStore } from '@registratie/application/aanvragen.store';
|
||||
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import {
|
||||
submittedRow,
|
||||
sortForDashboard,
|
||||
concepten,
|
||||
ingediend,
|
||||
} from '@registratie/domain/aanvraag-view';
|
||||
import { AanvraagBlockComponent } from '@registratie/ui/aanvraag-block/aanvraag-block.component';
|
||||
|
||||
/** Section: "Mijn aanvragen" — the user's own aanvragen (concepten as resumable
|
||||
meldingen, submitted ones as keuzelijst rows), owning its own fetch, sort and
|
||||
the resume/cancel actions. Empty → renders nothing, same as the aanvraag-block
|
||||
convention (see AanvraagBlockComponent). */
|
||||
@Component({
|
||||
selector: 'app-mijn-aanvragen-section',
|
||||
imports: [
|
||||
AlertComponent,
|
||||
SkeletonComponent,
|
||||
HeadingComponent,
|
||||
ApplicationListComponent,
|
||||
ApplicationLinkComponent,
|
||||
AanvraagBlockComponent,
|
||||
...ASYNC,
|
||||
],
|
||||
template: `
|
||||
@if (cancelError(); as err) {
|
||||
<app-alert type="error">{{ err }}</app-alert>
|
||||
}
|
||||
<app-async [data]="store.aanvragen()" (retryClicked)="store.reload()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (aanvragen().length) {
|
||||
<section>
|
||||
@for (a of concepten_(); track a.id) {
|
||||
<app-aanvraag-block
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[aanvraag]="a"
|
||||
(resume)="resume(a)"
|
||||
(cancel)="cancel(a)"
|
||||
/>
|
||||
}
|
||||
@if (ingediend_().length) {
|
||||
<app-heading [level]="2" class="app-section" i18n="@@dashboard.mijnAanvragen"
|
||||
>Mijn aanvragen</app-heading
|
||||
>
|
||||
<app-application-list>
|
||||
@for (a of ingediend_(); track a.id) {
|
||||
@let row = submittedRow(a);
|
||||
<li
|
||||
app-application-link
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[heading]="row.heading"
|
||||
[subtitle]="row.subtitle"
|
||||
[status]="row.status"
|
||||
[to]="'/aanvraag/' + a.id"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="2" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
`,
|
||||
})
|
||||
export class MijnAanvragenSection {
|
||||
protected store = inject(AanvragenStore);
|
||||
private router = inject(Router);
|
||||
|
||||
constructor() {
|
||||
// Re-fetch on each visit so server-computed auto-approval transitions show up
|
||||
// (Concept → In behandeling → Goedgekeurd after the processing window).
|
||||
this.store.reload();
|
||||
}
|
||||
|
||||
protected submittedRow = submittedRow;
|
||||
|
||||
protected aanvragen = computed<Aanvraag[]>(() => {
|
||||
const rd = this.store.aanvragen();
|
||||
return rd.tag === 'Success' ? sortForDashboard(rd.value) : [];
|
||||
});
|
||||
protected concepten_ = computed(() => concepten(this.aanvragen()));
|
||||
protected ingediend_ = computed(() => ingediend(this.aanvragen()));
|
||||
|
||||
private readonly resumeRoutes: Record<AanvraagType, string> = {
|
||||
registratie: '/registreren',
|
||||
herregistratie: '/herregistratie',
|
||||
intake: '/intake',
|
||||
};
|
||||
protected resume(a: Aanvraag) {
|
||||
void this.router.navigate([this.resumeRoutes[a.type]], { queryParams: { aanvraag: a.id } });
|
||||
}
|
||||
protected cancel(a: Aanvraag) {
|
||||
void this.store.cancel(a.id);
|
||||
}
|
||||
|
||||
/** The message from a failed cancel, rendered above the list. */
|
||||
protected cancelError = computed(() => this.store.lastError());
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig } from '@storybook/angular';
|
||||
import { MijnRegistratieSection } from './mijn-registratie.section';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { BigProfile } from '@registratie/domain/big-profile';
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
import { loading, success, failure } from '@shared/testing/remote-data';
|
||||
|
||||
const profile: BigProfile = {
|
||||
registration: {
|
||||
bigNummer: '19012345601',
|
||||
naam: 'Dr. A. (Anna) de Vries',
|
||||
beroep: 'Arts',
|
||||
registratiedatum: '2012-09-01',
|
||||
geboortedatum: '1985-03-14',
|
||||
status: { tag: 'Geregistreerd', herregistratieDatum: '2027-09-01' },
|
||||
},
|
||||
person: {
|
||||
naam: 'Dr. A. (Anna) de Vries',
|
||||
geboortedatum: '1985-03-14',
|
||||
adres: { straat: 'Rijksweg 1', postcode: '2514 EA', woonplaats: 'Den Haag' },
|
||||
},
|
||||
};
|
||||
|
||||
/** Minimal store stand-in — only the members the section's template reads. */
|
||||
function storeStub(profileRd: RemoteData<Error | undefined, BigProfile>) {
|
||||
return { profile: () => profileRd, reloadProfile: () => {} };
|
||||
}
|
||||
|
||||
const meta: Meta<MijnRegistratieSection> = {
|
||||
title: 'Domein/Registratie/Dashboard/Mijn Registratie',
|
||||
component: MijnRegistratieSection,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<MijnRegistratieSection>;
|
||||
|
||||
export const Loading: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(loading()) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Loaded: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(success(profile)) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Failed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(failure(new Error('offline'))) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';
|
||||
|
||||
/** Section: "Mijn registratie" — the BIG-register summary plus the BRP
|
||||
persoonsgegevens, both from the one screen-shaped dashboard call
|
||||
(BigProfileStore). */
|
||||
@Component({
|
||||
selector: 'app-mijn-registratie-section',
|
||||
imports: [
|
||||
HeadingComponent,
|
||||
SkeletonComponent,
|
||||
DataBlockComponent,
|
||||
DataRowComponent,
|
||||
RegistrationSummaryComponent,
|
||||
...ASYNC,
|
||||
],
|
||||
template: `
|
||||
<app-async [data]="store.profile()" (retryClicked)="store.reloadProfile()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (profile(); as p) {
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
|
||||
>Mijn registratie</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="p.registration" />
|
||||
</div>
|
||||
<app-data-block
|
||||
class="app-section"
|
||||
i18n-heading="@@dashboard.persoonsgegevens"
|
||||
heading="Persoonsgegevens (BRP)"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.straat"
|
||||
key="Straat"
|
||||
[value]="p.person.adres.straat"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.postcode"
|
||||
key="Postcode"
|
||||
[value]="p.person.adres.postcode"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.woonplaats"
|
||||
key="Woonplaats"
|
||||
[value]="p.person.adres.woonplaats"
|
||||
></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="4" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
`,
|
||||
})
|
||||
export class MijnRegistratieSection {
|
||||
protected store = inject(BigProfileStore);
|
||||
protected profile = () => successOf(this.store.profile());
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { applicationConfig } from '@storybook/angular';
|
||||
import { SpecialismenSection } from './specialismen.section';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { Aantekening } from '@registratie/domain/registration';
|
||||
import { RemoteData } from '@shared/application/remote-data';
|
||||
import { loading, success, empty, failure } from '@shared/testing/remote-data';
|
||||
|
||||
const rows: Aantekening[] = [
|
||||
{ type: 'Specialisme', omschrijving: 'Huisartsgeneeskunde', datum: '2016-04-12' },
|
||||
{ type: 'Aantekening', omschrijving: 'Erkend opleider huisartsgeneeskunde', datum: '2019-01-08' },
|
||||
];
|
||||
|
||||
/** Minimal store stand-in — only the members the section's template reads. */
|
||||
function storeStub(aantekeningen: RemoteData<Error | undefined, Aantekening[]>) {
|
||||
return { aantekeningen: () => aantekeningen, reloadAantekeningen: () => {} };
|
||||
}
|
||||
|
||||
const meta: Meta<SpecialismenSection> = {
|
||||
title: 'Domein/Registratie/Dashboard/Specialismen',
|
||||
component: SpecialismenSection,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<SpecialismenSection>;
|
||||
|
||||
export const Loading: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(loading()) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Loaded: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(success(rows)) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
export const Empty: Story = {
|
||||
decorators: [
|
||||
applicationConfig({ providers: [{ provide: BigProfileStore, useValue: storeStub(empty()) }] }),
|
||||
],
|
||||
};
|
||||
export const Failed: Story = {
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [{ provide: BigProfileStore, useValue: storeStub(failure(new Error('offline'))) }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';
|
||||
|
||||
/** Section: "Specialismen en aantekeningen" — a separate resource from the rest of
|
||||
the dashboard (own load/empty/error state), because it is genuinely a second
|
||||
endpoint (`big-register.adapter.ts`), not part of the BFF-lite view call. */
|
||||
@Component({
|
||||
selector: 'app-specialismen-section',
|
||||
imports: [HeadingComponent, SkeletonComponent, RegistrationTableComponent, ...ASYNC],
|
||||
template: `
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.specialismen"
|
||||
>Specialismen en aantekeningen</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-async [data]="store.aantekeningen()" (retryClicked)="store.reloadAantekeningen()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (aantekeningen(); as r) {
|
||||
<app-registration-table [rows]="r" />
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
</ng-template>
|
||||
<ng-template appAsyncEmpty>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.geenSpecialismen">
|
||||
U heeft nog geen specialismen of aantekeningen.
|
||||
</p>
|
||||
</ng-template>
|
||||
</app-async>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class SpecialismenSection {
|
||||
protected store = inject(BigProfileStore);
|
||||
protected aantekeningen = () => successOf(this.store.aantekeningen());
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { TaskListComponent } from '@shared/ui/task-list/task-list.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
|
||||
/** Section: "Wat moet ik regelen" — the open tasks derived from the registration
|
||||
+ the server-computed herregistratie eligibility (rendered, never recomputed;
|
||||
ADR-0001). Empty task list → a plain "niets openstaan" message, not an empty
|
||||
async state (the registration itself did load). */
|
||||
@Component({
|
||||
selector: 'app-wat-moet-ik-regelen-section',
|
||||
imports: [HeadingComponent, AlertComponent, SkeletonComponent, TaskListComponent, ...ASYNC],
|
||||
template: `
|
||||
@if (store.pendingHerregistratie()) {
|
||||
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie"
|
||||
>Uw herregistratie-aanvraag is in behandeling.</app-alert
|
||||
>
|
||||
}
|
||||
<app-async [data]="store.profile()" (retryClicked)="store.reloadProfile()">
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (tasks(); as t) {
|
||||
<section>
|
||||
@if (t.length) {
|
||||
<app-task-list
|
||||
i18n-listHeading="@@dashboard.watMoetIkRegelen"
|
||||
listHeading="Wat moet ik regelen"
|
||||
[tasks]="t"
|
||||
/>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
|
||||
>Wat moet ik regelen</app-heading
|
||||
>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
|
||||
U heeft op dit moment niets openstaan.
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="2" />
|
||||
</ng-template>
|
||||
</app-async>
|
||||
`,
|
||||
})
|
||||
export class WatMoetIkRegelenSection {
|
||||
protected store = inject(BigProfileStore);
|
||||
|
||||
private eligible = computed(() => {
|
||||
const d = successOf(this.store.decisions());
|
||||
return d?.eligibleForHerregistratie ?? false;
|
||||
});
|
||||
|
||||
protected tasks = computed(() => {
|
||||
const p = successOf(this.store.profile());
|
||||
return p ? tasksFromProfile(p.registration, this.eligible()) : undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';
|
||||
import { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';
|
||||
import { FeatureFlagStore } from '@shared/application/feature-flags.store';
|
||||
import { FLAG_INSCHRIJVING_OPEN } from '@shared/domain/feature-flag';
|
||||
|
||||
/** Section: "Wat wilt u doen?" — the portal's primary transactional actions (see
|
||||
CIBG's componenten/aanvragen). The core pages live in the header nav now; the
|
||||
teaching pages (concepts/brief) are only reachable from here. */
|
||||
@Component({
|
||||
selector: 'app-wat-wilt-u-doen-section',
|
||||
imports: [HeadingComponent, ApplicationListComponent, ApplicationLinkComponent],
|
||||
template: `
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
|
||||
<app-application-list class="app-section">
|
||||
@for (a of acties(); track a.to) {
|
||||
<li
|
||||
app-application-link
|
||||
[heading]="a.titel"
|
||||
[subtitle]="a.tekst"
|
||||
[cta]="a.actie"
|
||||
[to]="a.to"
|
||||
></li>
|
||||
}
|
||||
</app-application-list>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class WatWiltUDoenSection {
|
||||
private flags = inject(FeatureFlagStore);
|
||||
|
||||
private readonly allActies = [
|
||||
{
|
||||
to: '/registreren',
|
||||
titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,
|
||||
tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,
|
||||
actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,
|
||||
},
|
||||
{
|
||||
to: '/herregistratie',
|
||||
titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,
|
||||
tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,
|
||||
actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,
|
||||
},
|
||||
{
|
||||
to: '/intake',
|
||||
titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,
|
||||
tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,
|
||||
actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,
|
||||
},
|
||||
{
|
||||
to: '/registratie',
|
||||
titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,
|
||||
tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,
|
||||
actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,
|
||||
},
|
||||
{
|
||||
to: '/concepts',
|
||||
titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,
|
||||
tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,
|
||||
actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,
|
||||
},
|
||||
{
|
||||
to: '/brief',
|
||||
titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,
|
||||
tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,
|
||||
actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,
|
||||
},
|
||||
];
|
||||
|
||||
/** Hide "Inschrijven" when self-service registration is flagged off. */
|
||||
protected readonly acties = computed(() =>
|
||||
this.allActies.filter(
|
||||
(a) => a.to !== '/registreren' || this.flags.enabled(FLAG_INSCHRIJVING_OPEN),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { successOf } from '@shared/application/remote-data';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
@@ -41,9 +42,7 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
export class RegistrationDetailPage {
|
||||
protected store = inject(BigProfileStore);
|
||||
|
||||
/** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */
|
||||
protected readonly profile = computed(() => {
|
||||
const rd = this.store.profile();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
/** `successOf`: `<ng-template>` can't inherit a generic from a sibling host input,
|
||||
so the Success value is unwrapped here instead of through `let-`. */
|
||||
protected readonly profile = computed(() => successOf(this.store.profile()));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RemoteData, map2, map } from './remote-data';
|
||||
import { loading, failure, success } from '../testing/remote-data';
|
||||
import { RemoteData, map2, map, successOf } from './remote-data';
|
||||
import { loading, failure, empty, success } from '../testing/remote-data';
|
||||
|
||||
const loadingRd: RemoteData<string, number> = loading();
|
||||
const failureRd: RemoteData<string, number> = failure('x');
|
||||
@@ -21,3 +21,15 @@ describe('RemoteData combinators', () => {
|
||||
expect(map2(ok(2), ok(3), add)).toEqual({ tag: 'Success', value: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('successOf', () => {
|
||||
it('unwraps a Success value', () => {
|
||||
expect(successOf(ok(2))).toBe(2);
|
||||
});
|
||||
|
||||
it('is undefined for every other state', () => {
|
||||
expect(successOf(loadingRd)).toBeUndefined();
|
||||
expect(successOf(failureRd)).toBeUndefined();
|
||||
expect(successOf(empty())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,3 +80,12 @@ export function andThen<E, A, B>(
|
||||
): RemoteData<E, B> {
|
||||
return rd.tag === 'Success' ? f(rd.value) : rd;
|
||||
}
|
||||
|
||||
/** Unwrap a Success value, or `undefined` for every other state. Used to narrow
|
||||
an `<app-async>` loaded slot: `<ng-template>` can't inherit a generic from a
|
||||
sibling host input (Angular only infers a structural directive's type
|
||||
parameter from an input on that same node), so the caller unwraps here
|
||||
instead of through `let-`. */
|
||||
export function successOf<E, T>(rd: RemoteData<E, T>): T | undefined {
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
}
|
||||
|
||||
@@ -11,3 +11,5 @@ export const loading = <E = never, T = never>(): RemoteData<E, T> => ({ tag: 'Lo
|
||||
export const success = <T, E = never>(value: T): RemoteData<E, T> => ({ tag: 'Success', value });
|
||||
|
||||
export const failure = <E, T = never>(error: E): RemoteData<E, T> => ({ tag: 'Failure', error });
|
||||
|
||||
export const empty = <E = never, T = never>(): RemoteData<E, T> => ({ tag: 'Empty' });
|
||||
|
||||
Reference in New Issue
Block a user