feat(dashboard): render a lopende aanvraag as a CIBG melding
A Concept status now renders as a CIBG "melding" (warning) with its own verwijderen/openen actions, matching the real CIBG pattern for an in-progress application, instead of sharing the keuzelijst card shape used by resolved statuses. Alert atom switches from a hand-rolled surface to the vendored `.feedback` classes with a visually-hidden icon label per CIBG's a11y requirement.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ChoiceLinkComponent } from '@shared/ui/choice-link/choice-link.component';
|
||||
import { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import { blockActions } from '@registratie/domain/block-actions';
|
||||
@@ -10,33 +11,37 @@ const TYPE_LABELS: Record<AanvraagType, string> = {
|
||||
intake: $localize`:@@aanvraagBlock.type.intake:Herregistratie-intake`,
|
||||
};
|
||||
|
||||
/** Organism: one application on the dashboard's "Mijn aanvragen" list, rendered as
|
||||
a CIBG Huisstijl "keuzelijst" choice (choice-link). Which text/actions it shows
|
||||
is driven by the pure `blockActions(status)` and the status tag; the block only
|
||||
renders. Only a resumable Concept is clickable (stretched-link over the whole
|
||||
card) — a resolved status has nothing to navigate to, so it renders as a plain
|
||||
(non-interactive) card. */
|
||||
/** Organism: one application on the dashboard. A resumable Concept ("lopende
|
||||
aanvraag") renders as a CIBG "melding" (warning) with its actions — verwijderen
|
||||
as a link, aanvraag openen as a button; a submitted/resolved status renders as a
|
||||
plain keuzelijst card. Which actions show is driven by the pure
|
||||
`blockActions(status)`; the block only renders. NOTE: the caller places the two
|
||||
shapes differently — a Concept melding is a block element, the rest are `<li>`s
|
||||
that must sit inside a keuzelijst `<ul>` (see dashboard.page.ts). */
|
||||
@Component({
|
||||
selector: 'app-aanvraag-block',
|
||||
imports: [ButtonComponent, ChoiceLinkComponent],
|
||||
imports: [ButtonComponent, AlertComponent, ChoiceLinkComponent],
|
||||
styles: [`
|
||||
:host{display:contents} /* see choice-link.component.ts (keeps <li> a direct <ul> child) */
|
||||
/* Sits inside the same card as the stretched-link title — needs its own
|
||||
stacking context above the stretched-link overlay (z-index:1) to stay clickable. */
|
||||
.actions{position:relative;z-index:2;margin-block-start:var(--rhc-space-max-sm)}
|
||||
.actions{display:flex;align-items:center;gap:var(--rhc-space-max-md);margin-block-start:var(--rhc-space-max-sm)}
|
||||
`],
|
||||
template: `
|
||||
<app-choice-link
|
||||
[heading]="typeLabel()"
|
||||
[instructions]="instructions()"
|
||||
[clickable]="actions().includes('resume')"
|
||||
(activate)="resume.emit()">
|
||||
@if (actions().includes('cancel')) {
|
||||
<div choiceActions class="actions">
|
||||
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.annuleren">Annuleren</app-button>
|
||||
@if (aanvraag().status.tag === 'Concept') {
|
||||
<app-alert type="warning">
|
||||
<h3 class="h5">{{ typeLabel() }}</h3>
|
||||
<p>{{ conceptText() }}</p>
|
||||
<div class="actions">
|
||||
@if (actions().includes('cancel')) {
|
||||
<app-button variant="subtle" (click)="cancel.emit()" i18n="@@aanvraagBlock.verwijderen">Verwijderen</app-button>
|
||||
}
|
||||
@if (actions().includes('resume')) {
|
||||
<app-button (click)="resume.emit()" i18n="@@aanvraagBlock.openen">Aanvraag openen</app-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</app-choice-link>
|
||||
</app-alert>
|
||||
} @else {
|
||||
<app-choice-link [heading]="typeLabel()" [instructions]="instructions()" />
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class AanvraagBlockComponent {
|
||||
@@ -48,11 +53,26 @@ export class AanvraagBlockComponent {
|
||||
protected typeLabel = computed(() => TYPE_LABELS[this.aanvraag().type]);
|
||||
protected actions = computed(() => blockActions(this.aanvraag().status));
|
||||
|
||||
// ponytail: display-only deadline derived as createdAt + 30 dagen; move to a
|
||||
// server-sent `completeBefore` on the DTO when the expiry rule becomes real.
|
||||
private deadline = computed(() => {
|
||||
const d = new Date(this.aanvraag().createdAt);
|
||||
d.setDate(d.getDate() + 30);
|
||||
return d.toISOString();
|
||||
});
|
||||
|
||||
/** The melding body for a Concept: wizard not finished + complete-before date. */
|
||||
protected conceptText = computed(() => {
|
||||
const s = this.aanvraag().status;
|
||||
if (s.tag !== 'Concept') return '';
|
||||
return $localize`:@@aanvraagBlock.conceptMelding:Deze aanvraag is nog niet volledig afgerond — u bent gebleven bij stap ${s.stepIndex + 1}:stap: van ${s.stepCount}:totaal:. Rond de aanvraag af vóór ${formatNL(this.deadline())}:datum:.`;
|
||||
});
|
||||
|
||||
// Status-tag → the choice's description line (the UI's mapping, not a domain rule).
|
||||
private statusText = computed(() => {
|
||||
const s = this.aanvraag().status;
|
||||
switch (s.tag) {
|
||||
case 'Concept': return $localize`:@@aanvraagBlock.stap:Stap ${s.stepIndex + 1}:index: van ${s.stepCount}:count:`;
|
||||
case 'Concept': return '';
|
||||
case 'InBehandeling': return $localize`:@@aanvraagBlock.inBehandeling:Referentie ${s.referentie}:ref: · ingediend op ${formatNL(this.aanvraag().submittedAt)}:datum:`;
|
||||
case 'Goedgekeurd': return $localize`:@@aanvraagBlock.goedgekeurd:Referentie ${s.referentie}:ref:`;
|
||||
case 'Afgewezen': return $localize`:@@aanvraagBlock.afgewezen:Referentie ${s.referentie}:ref:`;
|
||||
|
||||
@@ -27,7 +27,11 @@ export default meta;
|
||||
type Story = StoryObj<AanvraagBlockComponent>;
|
||||
|
||||
// One story per status variant; the block renders its own body + actions.
|
||||
export const Concept: Story = { args: { aanvraag: { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } } } };
|
||||
// A Concept renders as a CIBG melding (block element), not a keuzelijst <li> — no <ul> wrapper.
|
||||
export const Concept: Story = {
|
||||
args: { aanvraag: { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } } },
|
||||
render: (args) => ({ props: args, template: `<app-aanvraag-block [aanvraag]="aanvraag" />` }),
|
||||
};
|
||||
export const InBehandelingAuto: Story = { args: { aanvraag: { ...base, status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false } } } };
|
||||
export const InBehandelingManual: Story = { args: { aanvraag: { ...base, type: 'registratie', status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true } } } };
|
||||
export const Goedgekeurd: Story = { args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } } };
|
||||
|
||||
@@ -35,11 +35,16 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
<div class="app-stack">
|
||||
@if (aanvragen().length) {
|
||||
<section>
|
||||
<app-choice-list i18n-heading="@@dashboard.mijnAanvragen" heading="Mijn aanvragen" class="app-section">
|
||||
@for (a of aanvragen(); track a.id) {
|
||||
<app-aanvraag-block animate.enter="app-item-enter" animate.leave="app-item-leave" [aanvraag]="a" (resume)="resume(a)" (cancel)="cancelAanvraag(a)" />
|
||||
}
|
||||
</app-choice-list>
|
||||
@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-choice-list i18n-heading="@@dashboard.mijnAanvragen" heading="Mijn aanvragen" class="app-section">
|
||||
@for (a of ingediend(); track a.id) {
|
||||
<app-aanvraag-block animate.enter="app-item-enter" animate.leave="app-item-leave" [aanvraag]="a" />
|
||||
}
|
||||
</app-choice-list>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -127,6 +132,10 @@ export class DashboardPage {
|
||||
const order: Record<Aanvraag['status']['tag'], number> = { Concept: 0, InBehandeling: 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',
|
||||
|
||||
Reference in New Issue
Block a user