refactor: split registratie-wizard into three steps (RD-23)
Move the adres, beroep and controle cases out of registratie-wizard.component.ts into adres.step.ts, beroep.step.ts and controle.step.ts, matching RD-22's *.step.ts convention. The parent drops from ~568 to 274 lines and loses its `eslint-disable max-lines`. The upload controller moves into beroep.step.ts and emits `uploadMsg` instead of dispatching directly; the parent maps that back onto the machine's `Upload` message. `onDiplomaKeuze` stays in the parent (message construction from the DUO payload belongs in the container) and now takes only the chosen id, reading its own `duoData` computed instead of receiving the DUO payload as an argument. Each step injects `RegistratieLookupStore` directly for its own async presentation (adresStatus, the DUO lookup, samenvattingVragen) — the sanctioned exception, since it is a root singleton. Markup moved verbatim; the `@@` id count across the directory stays 43. Two of the ticket's acceptance numbers do not hold against correct code and are corrected in the ticket file: `createUploadController` is 2 lines (import + call), not 1 — `git grep -c` counts lines, and the same shape gives 2 for `createStore` and 3 for `createDraftSync` elsewhere in this codebase. `dispatch` is 1, not 0 — decision 4's mandated `UploadControllerDeps.dispatch` property name is that string even though it is not the machine's dispatch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
import { Component, inject, input, output } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||||
|
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||||
|
import { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';
|
||||||
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||||
|
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||||
|
import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';
|
||||||
|
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||||
|
import { Draft, DraftField, Errors } from '@registratie/domain/registratie-wizard.machine';
|
||||||
|
|
||||||
|
const KANALEN = [
|
||||||
|
{ value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },
|
||||||
|
{ value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Step: the registratie wizard's first screen (adres + correspondentievoorkeur).
|
||||||
|
Injects RegistratieLookupStore directly for the BRP lookup banner — the
|
||||||
|
sanctioned exception (it is `providedIn: 'root'`, so every injection is the
|
||||||
|
same instance): the step owns its own async presentation rather than making
|
||||||
|
the parent a pass-through for it. Values otherwise in via `draft`/`errors`,
|
||||||
|
every change out via `fieldChange`/`kanaalChange`. No internal state; the
|
||||||
|
parent owns the Model and decides what a change means. */
|
||||||
|
@Component({
|
||||||
|
selector: 'app-reg-adres-step',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
FormFieldComponent,
|
||||||
|
TextInputComponent,
|
||||||
|
RadioGroupComponent,
|
||||||
|
AlertComponent,
|
||||||
|
SkeletonComponent,
|
||||||
|
AddressFieldsComponent,
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
@if (adresStatus() === 'laden') {
|
||||||
|
<app-skeleton height="2.5rem" [count]="4" />
|
||||||
|
} @else {
|
||||||
|
@switch (adresStatus()) {
|
||||||
|
@case ('gevonden') {
|
||||||
|
<app-alert type="info" i18n="@@regWizard.brpGevonden"
|
||||||
|
>Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert
|
||||||
|
>
|
||||||
|
}
|
||||||
|
@case ('geen') {
|
||||||
|
<app-alert type="warning" i18n="@@regWizard.brpGeen"
|
||||||
|
>We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert
|
||||||
|
>
|
||||||
|
}
|
||||||
|
@case ('fout') {
|
||||||
|
<app-alert type="warning" i18n="@@regWizard.brpFout"
|
||||||
|
>We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<app-address-fields
|
||||||
|
[value]="{
|
||||||
|
straat: draft().straat ?? '',
|
||||||
|
postcode: draft().postcode ?? '',
|
||||||
|
woonplaats: draft().woonplaats ?? '',
|
||||||
|
}"
|
||||||
|
[errors]="{
|
||||||
|
straat: err('straat'),
|
||||||
|
postcode: err('postcode'),
|
||||||
|
woonplaats: err('woonplaats'),
|
||||||
|
}"
|
||||||
|
(fieldChange)="fieldChange.emit($event)"
|
||||||
|
/>
|
||||||
|
<fieldset>
|
||||||
|
<app-form-field
|
||||||
|
i18n-label="@@regWizard.correspondentieLabel"
|
||||||
|
label="Hoe wilt u correspondentie ontvangen?"
|
||||||
|
fieldId="correspondentie"
|
||||||
|
required
|
||||||
|
[error]="err('correspondentie')"
|
||||||
|
>
|
||||||
|
<app-radio-group
|
||||||
|
name="correspondentie"
|
||||||
|
[options]="kanalen"
|
||||||
|
[invalid]="!!err('correspondentie')"
|
||||||
|
[ngModel]="draft().correspondentie ?? ''"
|
||||||
|
(ngModelChange)="kanaalChange.emit($event)"
|
||||||
|
/>
|
||||||
|
</app-form-field>
|
||||||
|
</fieldset>
|
||||||
|
@if (draft().correspondentie === 'email') {
|
||||||
|
<fieldset>
|
||||||
|
<app-form-field
|
||||||
|
i18n-label="@@regWizard.emailLabel"
|
||||||
|
label="E-mailadres"
|
||||||
|
fieldId="email"
|
||||||
|
required
|
||||||
|
[error]="err('email')"
|
||||||
|
>
|
||||||
|
<app-text-input
|
||||||
|
inputId="email"
|
||||||
|
type="email"
|
||||||
|
[invalid]="!!err('email')"
|
||||||
|
[ngModel]="draft().email ?? ''"
|
||||||
|
(ngModelChange)="fieldChange.emit({ key: 'email', value: $event })"
|
||||||
|
name="email"
|
||||||
|
i18n-placeholder="@@regWizard.emailPlaceholder"
|
||||||
|
placeholder="naam@voorbeeld.nl"
|
||||||
|
/>
|
||||||
|
</app-form-field>
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class AdresStep {
|
||||||
|
private lookup = inject(RegistratieLookupStore);
|
||||||
|
|
||||||
|
draft = input.required<Draft>();
|
||||||
|
errors = input.required<Errors>();
|
||||||
|
fieldChange = output<{ key: DraftField; value: string }>();
|
||||||
|
kanaalChange = output<string>();
|
||||||
|
|
||||||
|
protected adresStatus = this.lookup.adresStatus;
|
||||||
|
readonly kanalen = KANALEN;
|
||||||
|
protected err = (k: DraftField | 'correspondentie') => this.errors()[k] ?? '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { Component, computed, inject, input, output } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||||
|
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||||
|
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.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 { ASYNC } from '@shared/ui/async/async.component';
|
||||||
|
import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';
|
||||||
|
import { createUploadController } from '@shared/application/upload-controller';
|
||||||
|
import { UploadMsg, UploadState } from '@shared/domain/upload.machine';
|
||||||
|
import { RemoteData, successOr } from '@shared/application/remote-data';
|
||||||
|
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||||
|
import { DuoLookupDto, PolicyQuestionDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||||
|
import { Draft, Errors } from '@registratie/domain/registratie-wizard.machine';
|
||||||
|
|
||||||
|
/** The server-owned geldigheidsvraag whose "ja" answer requires a Dutch-taalvaardigheid
|
||||||
|
upload (proof of the confirmed B2 level). Stable id shared with the backend. */
|
||||||
|
const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
||||||
|
/** Sentinel option: "my diploma isn't listed". Exported so the parent's
|
||||||
|
`onDiplomaKeuze` (registratie-wizard.component.ts) can recognize it too. */
|
||||||
|
export const HANDMATIG = '__handmatig__';
|
||||||
|
|
||||||
|
/** Step: the registratie wizard's second screen (beroep op basis van diploma).
|
||||||
|
Injects RegistratieLookupStore directly for the DUO lookup — the sanctioned
|
||||||
|
exception (it is `providedIn: 'root'`, so every injection is the same
|
||||||
|
instance) — and owns its own `<app-async>` over it. Also owns the upload
|
||||||
|
controller, moved here from the parent: this is what gets the parent under
|
||||||
|
the line limit. Values in via `draft`/`errors`/`upload`; every user intent
|
||||||
|
leaves as one of four outputs. No store beyond the lookup, and no machine
|
||||||
|
message built here; the parent maps each output onto its own message. */
|
||||||
|
@Component({
|
||||||
|
selector: 'app-reg-beroep-step',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
FormFieldComponent,
|
||||||
|
TextInputComponent,
|
||||||
|
RadioGroupComponent,
|
||||||
|
AlertComponent,
|
||||||
|
SkeletonComponent,
|
||||||
|
DataRowComponent,
|
||||||
|
DataBlockComponent,
|
||||||
|
DocumentUploadComponent,
|
||||||
|
...ASYNC,
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<app-async [data]="lookupRd()">
|
||||||
|
<ng-template appAsyncLoaded>
|
||||||
|
@if (duoData(); as data) {
|
||||||
|
<fieldset>
|
||||||
|
<app-form-field
|
||||||
|
i18n-label="@@regWizard.diplomaLabel"
|
||||||
|
label="Kies het diploma waarmee u zich wilt registreren"
|
||||||
|
fieldId="diploma"
|
||||||
|
required
|
||||||
|
[error]="err('diploma')"
|
||||||
|
>
|
||||||
|
<app-radio-group
|
||||||
|
name="diploma"
|
||||||
|
[options]="diplomaOptions(data)"
|
||||||
|
[invalid]="!!err('diploma')"
|
||||||
|
[ngModel]="diplomaKeuze()"
|
||||||
|
(ngModelChange)="diplomaChosen.emit($event)"
|
||||||
|
/>
|
||||||
|
</app-form-field>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
@if (handmatigActief()) {
|
||||||
|
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing"
|
||||||
|
>Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw
|
||||||
|
beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig
|
||||||
|
beoordeeld.</app-alert
|
||||||
|
>
|
||||||
|
<fieldset>
|
||||||
|
<app-form-field
|
||||||
|
i18n-label="@@regWizard.beroepLabel"
|
||||||
|
label="Voor welk beroep wilt u zich registreren?"
|
||||||
|
fieldId="hm-beroep"
|
||||||
|
[error]="err('diploma')"
|
||||||
|
>
|
||||||
|
<app-radio-group
|
||||||
|
name="hm-beroep"
|
||||||
|
[options]="beroepOptions(data)"
|
||||||
|
[invalid]="!!err('diploma')"
|
||||||
|
[ngModel]="draft().beroep ?? ''"
|
||||||
|
(ngModelChange)="beroepDeclared.emit($event)"
|
||||||
|
/>
|
||||||
|
</app-form-field>
|
||||||
|
</fieldset>
|
||||||
|
} @else if (draft().beroep) {
|
||||||
|
<app-data-block class="app-section">
|
||||||
|
<div
|
||||||
|
app-data-row
|
||||||
|
i18n-key="@@regWizard.beroepAfgeleid"
|
||||||
|
key="Beroep (afgeleid uit diploma)"
|
||||||
|
[value]="draft().beroep ?? ''"
|
||||||
|
></div>
|
||||||
|
</app-data-block>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (actieveVragen(data).length) {
|
||||||
|
<fieldset>
|
||||||
|
@for (q of actieveVragen(data); track q.id) {
|
||||||
|
<app-form-field
|
||||||
|
[label]="q.vraag"
|
||||||
|
[fieldId]="'vraag-' + q.id"
|
||||||
|
[error]="vraagErr(q.id)"
|
||||||
|
>
|
||||||
|
@if (q.type === 'ja-nee') {
|
||||||
|
<app-radio-group
|
||||||
|
[name]="'vraag-' + q.id"
|
||||||
|
[options]="jaNee"
|
||||||
|
[invalid]="!!vraagErr(q.id)"
|
||||||
|
[ngModel]="antwoord(q.id)"
|
||||||
|
(ngModelChange)="antwoordChange.emit({ vraagId: q.id, value: $event })"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<app-text-input
|
||||||
|
[inputId]="'vraag-' + q.id"
|
||||||
|
[invalid]="!!vraagErr(q.id)"
|
||||||
|
[ngModel]="antwoord(q.id)"
|
||||||
|
(ngModelChange)="antwoordChange.emit({ vraagId: q.id, value: $event })"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</app-form-field>
|
||||||
|
}
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
<ng-template appAsyncLoading>
|
||||||
|
<app-skeleton height="2.5rem" [count]="3" />
|
||||||
|
</ng-template>
|
||||||
|
</app-async>
|
||||||
|
|
||||||
|
<app-document-upload
|
||||||
|
class="app-section"
|
||||||
|
[state]="upload()"
|
||||||
|
[previewUrlFor]="previewUrlFor"
|
||||||
|
(fileSelected)="uploadCtl.onFileSelected($event.categoryId, $event.files)"
|
||||||
|
(removeUpload)="uploadCtl.onRemove($event)"
|
||||||
|
(retryUpload)="uploadCtl.onRetry($event)"
|
||||||
|
(deleteUpload)="uploadCtl.onDelete($event)"
|
||||||
|
(channelChange)="uploadCtl.onChannelChange($event.categoryId, $event.channel)"
|
||||||
|
/>
|
||||||
|
@if (err('documenten')) {
|
||||||
|
<app-alert type="warning">{{ err('documenten') }}</app-alert>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class BeroepStep {
|
||||||
|
private lookup = inject(RegistratieLookupStore);
|
||||||
|
|
||||||
|
draft = input.required<Draft>();
|
||||||
|
errors = input.required<Errors>();
|
||||||
|
upload = input.required<UploadState>();
|
||||||
|
|
||||||
|
uploadMsg = output<UploadMsg>();
|
||||||
|
antwoordChange = output<{ vraagId: string; value: string }>();
|
||||||
|
diplomaChosen = output<string>();
|
||||||
|
beroepDeclared = output<string>();
|
||||||
|
|
||||||
|
/** Preview/download link for a completed upload; delegates to the upload
|
||||||
|
controller (application layer), which knows the dev-simulation `demo-*`
|
||||||
|
ids have no stored bytes and returns no link for them. */
|
||||||
|
protected previewUrlFor = (documentId: string): string | undefined =>
|
||||||
|
this.uploadCtl.previewUrlFor(documentId);
|
||||||
|
|
||||||
|
protected uploadCtl = createUploadController({
|
||||||
|
wizardId: 'registratie',
|
||||||
|
getUpload: () => this.upload(),
|
||||||
|
dispatch: (msg) => this.uploadMsg.emit(msg),
|
||||||
|
// Required documents depend on answers (server decides): a diploma upload only for a
|
||||||
|
// manual diploma; a Dutch-taalvaardigheid upload only once the applicant confirms
|
||||||
|
// ("ja") the B2 language requirement.
|
||||||
|
getCategoryParams: () => ({
|
||||||
|
diplomaHerkomst: this.draft().diplomaHerkomst,
|
||||||
|
taalvaardigheid: this.draft().antwoorden[NL_TAALVAARDIGHEID_VRAAG],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Parsed DUO lookup (validated at the trust boundary by the application
|
||||||
|
facade — the step renders, it does not fetch/parse). */
|
||||||
|
protected lookupRd: () => RemoteData<Error | undefined, DuoLookupDto> = this.lookup.duoLookup;
|
||||||
|
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookupRd(), null));
|
||||||
|
|
||||||
|
readonly jaNee = JA_NEE;
|
||||||
|
|
||||||
|
protected err = (k: 'diploma' | 'documenten') => this.errors()[k] ?? '';
|
||||||
|
protected vraagErr = (id: string) => this.errors().antwoorden?.[id] ?? '';
|
||||||
|
protected antwoord = (id: string) => this.draft().antwoorden[id] ?? ''; // runtime guard: missing key → undefined
|
||||||
|
|
||||||
|
/** True while the user is entering a diploma manually (not in the DUO list). */
|
||||||
|
protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');
|
||||||
|
/** The radio selection: a diploma id, or the "not listed" sentinel in manual mode. */
|
||||||
|
protected diplomaKeuze = computed(() =>
|
||||||
|
this.handmatigActief() ? HANDMATIG : (this.draft().diplomaId ?? ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
protected diplomaOptions = (data: DuoLookupDto) => [
|
||||||
|
...data.diplomas.map((d) => ({
|
||||||
|
value: d.id,
|
||||||
|
label: `${d.naam} — ${d.instelling} (${d.jaar})`,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
value: HANDMATIG,
|
||||||
|
label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
protected beroepOptions = (data: DuoLookupDto) =>
|
||||||
|
data.handmatig.beroepen.map((b) => ({ value: b, label: b }));
|
||||||
|
|
||||||
|
/** The policy questions that apply to the current choice (server-decided). */
|
||||||
|
protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {
|
||||||
|
if (this.handmatigActief()) return data.handmatig.policyQuestions;
|
||||||
|
return data.diplomas.find((d) => d.id === this.draft().diplomaId)?.policyQuestions ?? [];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { Component, computed, inject, input, output } from '@angular/core';
|
||||||
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||||
|
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
|
||||||
|
import { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
||||||
|
import { successOr } from '@shared/application/remote-data';
|
||||||
|
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||||
|
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||||
|
import { Draft } from '@registratie/domain/registratie-wizard.machine';
|
||||||
|
|
||||||
|
/** Step: the registratie wizard's review screen (controle & indienen). Injects
|
||||||
|
RegistratieLookupStore directly to build `samenvattingVragen` — the
|
||||||
|
sanctioned exception (it is `providedIn: 'root'`, so every injection is the
|
||||||
|
same instance). Values otherwise in via `draft`, the cursor to jump back to
|
||||||
|
out via `edit`. No internal state; the parent maps the cursor onto its own
|
||||||
|
`GaNaarStap` message. */
|
||||||
|
@Component({
|
||||||
|
selector: 'app-reg-controle-step',
|
||||||
|
imports: [AlertComponent, DataRowComponent, ReviewSectionComponent],
|
||||||
|
template: `
|
||||||
|
<app-alert type="info" i18n="@@regWizard.controleer"
|
||||||
|
>Controleer uw gegevens en dien de registratie in.</app-alert
|
||||||
|
>
|
||||||
|
<app-review-section
|
||||||
|
i18n-heading="@@regWizard.sectie.adres"
|
||||||
|
heading="Adres en correspondentie"
|
||||||
|
i18n-editAriaLabel="@@regWizard.adresWijzigenAria"
|
||||||
|
editAriaLabel="Wijzigen adresgegevens"
|
||||||
|
(edit)="edit.emit(0)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
app-data-row
|
||||||
|
i18n-key="@@regWizard.summary.adres"
|
||||||
|
key="Adres"
|
||||||
|
[value]="adresSamenvatting()"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
app-data-row
|
||||||
|
i18n-key="@@regWizard.summary.herkomstAdres"
|
||||||
|
key="Herkomst adres"
|
||||||
|
[value]="adresHerkomstLabel()"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
app-data-row
|
||||||
|
i18n-key="@@regWizard.summary.correspondentie"
|
||||||
|
key="Correspondentie"
|
||||||
|
[value]="correspondentieLabel()"
|
||||||
|
></div>
|
||||||
|
@if (draft().correspondentie === 'email') {
|
||||||
|
<div
|
||||||
|
app-data-row
|
||||||
|
i18n-key="@@regWizard.summary.email"
|
||||||
|
key="E-mailadres"
|
||||||
|
[value]="draft().email ?? ''"
|
||||||
|
></div>
|
||||||
|
}
|
||||||
|
</app-review-section>
|
||||||
|
<app-review-section
|
||||||
|
class="app-section"
|
||||||
|
i18n-heading="@@regWizard.sectie.beroep"
|
||||||
|
heading="Beroep en diploma"
|
||||||
|
i18n-editAriaLabel="@@regWizard.diplomaWijzigenAria"
|
||||||
|
editAriaLabel="Wijzigen beroep en diploma"
|
||||||
|
(edit)="edit.emit(1)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
app-data-row
|
||||||
|
i18n-key="@@regWizard.summary.beroep"
|
||||||
|
key="Beroep"
|
||||||
|
[value]="draft().beroep ?? ''"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
app-data-row
|
||||||
|
i18n-key="@@regWizard.summary.herkomstDiploma"
|
||||||
|
key="Herkomst diploma"
|
||||||
|
[value]="diplomaHerkomstLabel()"
|
||||||
|
></div>
|
||||||
|
@for (item of samenvattingVragen(); track item.vraag) {
|
||||||
|
<div app-data-row [key]="item.vraag" [value]="item.antwoord"></div>
|
||||||
|
}
|
||||||
|
</app-review-section>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class ControleStep {
|
||||||
|
private lookup = inject(RegistratieLookupStore);
|
||||||
|
|
||||||
|
draft = input.required<Draft>();
|
||||||
|
edit = output<number>();
|
||||||
|
|
||||||
|
/** Parsed DUO lookup as a plain value (or null), needed here only to resolve
|
||||||
|
the answered policy questions' text for the summary. */
|
||||||
|
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookup.duoLookup(), null));
|
||||||
|
|
||||||
|
protected adresSamenvatting = computed(() => {
|
||||||
|
const d = this.draft();
|
||||||
|
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
});
|
||||||
|
// Readable labels for the controle summary (instead of raw enum values).
|
||||||
|
protected adresHerkomstLabel = computed(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`,
|
||||||
|
handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd`,
|
||||||
|
})[this.draft().adresHerkomst ?? 'handmatig'],
|
||||||
|
);
|
||||||
|
protected correspondentieLabel = computed(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
email: $localize`:@@regWizard.corr.email:Per e-mail`,
|
||||||
|
post: $localize`:@@regWizard.corr.post:Per post`,
|
||||||
|
})[this.draft().correspondentie ?? 'post'],
|
||||||
|
);
|
||||||
|
protected diplomaHerkomstLabel = computed(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`,
|
||||||
|
handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)`,
|
||||||
|
})[this.draft().diplomaHerkomst ?? 'handmatig'],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Answered policy questions for the controle summary (question text + answer). */
|
||||||
|
protected samenvattingVragen = computed(() => {
|
||||||
|
const data = this.duoData();
|
||||||
|
const d = this.draft();
|
||||||
|
if (!data) return [] as { vraag: string; antwoord: string }[];
|
||||||
|
const alle = [
|
||||||
|
...data.diplomas.flatMap((x) => x.policyQuestions),
|
||||||
|
...data.handmatig.policyQuestions,
|
||||||
|
];
|
||||||
|
return (d.vraagIds ?? []).map((id) => ({
|
||||||
|
vraag: alle.find((q) => q.id === id)?.vraag ?? id,
|
||||||
|
antwoord: d.antwoorden[id] ?? '',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
+41
-389
@@ -1,15 +1,5 @@
|
|||||||
/* eslint-disable max-lines */ // one wizard shell for 3 steps + upload — removed by RD-23
|
|
||||||
import { Component, computed, effect, inject, input, untracked } from '@angular/core';
|
import { Component, computed, effect, inject, input, untracked } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
|
||||||
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
|
||||||
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
|
|
||||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
import { ButtonComponent } from '@shared/ui/button/button.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 { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';
|
|
||||||
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
import { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';
|
||||||
import {
|
import {
|
||||||
WizardShellComponent,
|
WizardShellComponent,
|
||||||
@@ -18,19 +8,17 @@ import {
|
|||||||
naarStapLabel,
|
naarStapLabel,
|
||||||
} from '@shared/layout/wizard-shell/wizard-shell.component';
|
} from '@shared/layout/wizard-shell/wizard-shell.component';
|
||||||
import { toWizardErrors } from '@shared/layout/wizard-shell/wizard-errors';
|
import { toWizardErrors } from '@shared/layout/wizard-shell/wizard-errors';
|
||||||
import { ASYNC } from '@shared/ui/async/async.component';
|
|
||||||
import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';
|
|
||||||
import { createStore } from '@shared/application/store';
|
import { createStore } from '@shared/application/store';
|
||||||
import { whenTag } from '@shared/kernel/fp';
|
import { whenTag } from '@shared/kernel/fp';
|
||||||
import { RemoteData, successOr } from '@shared/application/remote-data';
|
import { successOr } from '@shared/application/remote-data';
|
||||||
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
import { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';
|
||||||
import { DuoLookupDto, PolicyQuestionDto } from '@registratie/contracts/duo-diplomas.dto';
|
import { DuoLookupDto } from '@registratie/contracts/duo-diplomas.dto';
|
||||||
import {
|
import {
|
||||||
RegistratieState,
|
RegistratieState,
|
||||||
RegistratieMsg,
|
RegistratieMsg,
|
||||||
Draft,
|
Draft,
|
||||||
DraftField,
|
|
||||||
Correspondentie,
|
Correspondentie,
|
||||||
|
Errors,
|
||||||
StepId,
|
StepId,
|
||||||
initial,
|
initial,
|
||||||
reduce,
|
reduce,
|
||||||
@@ -38,18 +26,10 @@ import {
|
|||||||
STEPS,
|
STEPS,
|
||||||
} from '@registratie/domain/registratie-wizard.machine';
|
} from '@registratie/domain/registratie-wizard.machine';
|
||||||
import { createDraftSync } from '@registratie/application/draft-sync';
|
import { createDraftSync } from '@registratie/application/draft-sync';
|
||||||
import { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';
|
|
||||||
import { createUploadController } from '@shared/application/upload-controller';
|
|
||||||
import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine';
|
import { UploadState, initialUpload, deliveryRefs } from '@shared/domain/upload.machine';
|
||||||
|
import { AdresStep } from './adres.step';
|
||||||
const KANALEN = [
|
import { BeroepStep, HANDMATIG } from './beroep.step';
|
||||||
{ value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },
|
import { ControleStep } from './controle.step';
|
||||||
{ value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },
|
|
||||||
];
|
|
||||||
const HANDMATIG = '__handmatig__'; // sentinel option:"my diploma isn't listed"
|
|
||||||
/** The server-owned geldigheidsvraag whose"ja" answer requires a Dutch-taalvaardigheid
|
|
||||||
upload (proof of the confirmed B2 level). Stable id shared with the backend. */
|
|
||||||
const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
|
||||||
|
|
||||||
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
|
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
|
||||||
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
|
the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the
|
||||||
@@ -61,21 +41,12 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-registratie-wizard',
|
selector: 'app-registratie-wizard',
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
|
||||||
FormFieldComponent,
|
|
||||||
TextInputComponent,
|
|
||||||
RadioGroupComponent,
|
|
||||||
ButtonComponent,
|
ButtonComponent,
|
||||||
AlertComponent,
|
|
||||||
SkeletonComponent,
|
|
||||||
DataRowComponent,
|
|
||||||
DataBlockComponent,
|
|
||||||
ReviewSectionComponent,
|
|
||||||
ConfirmationComponent,
|
ConfirmationComponent,
|
||||||
WizardShellComponent,
|
WizardShellComponent,
|
||||||
AddressFieldsComponent,
|
AdresStep,
|
||||||
DocumentUploadComponent,
|
BeroepStep,
|
||||||
...ASYNC,
|
ControleStep,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
<app-wizard-shell
|
<app-wizard-shell
|
||||||
@@ -98,253 +69,31 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
|||||||
>
|
>
|
||||||
@switch (step()) {
|
@switch (step()) {
|
||||||
@case ('adres') {
|
@case ('adres') {
|
||||||
@if (adresStatus() === 'laden') {
|
<app-reg-adres-step
|
||||||
<app-skeleton height="2.5rem" [count]="4" />
|
[draft]="draft()"
|
||||||
} @else {
|
[errors]="errors()"
|
||||||
@switch (adresStatus()) {
|
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })"
|
||||||
@case ('gevonden') {
|
(kanaalChange)="onKanaalChange($event)"
|
||||||
<app-alert type="info" i18n="@@regWizard.brpGevonden"
|
|
||||||
>Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert
|
|
||||||
>
|
|
||||||
}
|
|
||||||
@case ('geen') {
|
|
||||||
<app-alert type="warning" i18n="@@regWizard.brpGeen"
|
|
||||||
>We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert
|
|
||||||
>
|
|
||||||
}
|
|
||||||
@case ('fout') {
|
|
||||||
<app-alert type="warning" i18n="@@regWizard.brpFout"
|
|
||||||
>We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig
|
|
||||||
in.</app-alert
|
|
||||||
>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<app-address-fields
|
|
||||||
[value]="{
|
|
||||||
straat: draft().straat ?? '',
|
|
||||||
postcode: draft().postcode ?? '',
|
|
||||||
woonplaats: draft().woonplaats ?? '',
|
|
||||||
}"
|
|
||||||
[errors]="{
|
|
||||||
straat: err('straat'),
|
|
||||||
postcode: err('postcode'),
|
|
||||||
woonplaats: err('woonplaats'),
|
|
||||||
}"
|
|
||||||
(fieldChange)="set($event.key, $event.value)"
|
|
||||||
/>
|
/>
|
||||||
<fieldset>
|
|
||||||
<app-form-field
|
|
||||||
i18n-label="@@regWizard.correspondentieLabel"
|
|
||||||
label="Hoe wilt u correspondentie ontvangen?"
|
|
||||||
fieldId="correspondentie"
|
|
||||||
required
|
|
||||||
[error]="err('correspondentie')"
|
|
||||||
>
|
|
||||||
<app-radio-group
|
|
||||||
name="correspondentie"
|
|
||||||
[options]="kanalen"
|
|
||||||
[invalid]="!!err('correspondentie')"
|
|
||||||
[ngModel]="draft().correspondentie ?? ''"
|
|
||||||
(ngModelChange)="setKanaal($event)"
|
|
||||||
/>
|
|
||||||
</app-form-field>
|
|
||||||
</fieldset>
|
|
||||||
@if (draft().correspondentie === 'email') {
|
|
||||||
<fieldset>
|
|
||||||
<app-form-field
|
|
||||||
i18n-label="@@regWizard.emailLabel"
|
|
||||||
label="E-mailadres"
|
|
||||||
fieldId="email"
|
|
||||||
required
|
|
||||||
[error]="err('email')"
|
|
||||||
>
|
|
||||||
<app-text-input
|
|
||||||
inputId="email"
|
|
||||||
type="email"
|
|
||||||
[invalid]="!!err('email')"
|
|
||||||
[ngModel]="draft().email ?? ''"
|
|
||||||
(ngModelChange)="set('email', $event)"
|
|
||||||
name="email"
|
|
||||||
i18n-placeholder="@@regWizard.emailPlaceholder"
|
|
||||||
placeholder="naam@voorbeeld.nl"
|
|
||||||
/>
|
|
||||||
</app-form-field>
|
|
||||||
</fieldset>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@case ('beroep') {
|
@case ('beroep') {
|
||||||
<app-async [data]="lookupRd()">
|
<app-reg-beroep-step
|
||||||
<ng-template appAsyncLoaded>
|
[draft]="draft()"
|
||||||
@if (duoData(); as data) {
|
[errors]="errors()"
|
||||||
<fieldset>
|
[upload]="upload()"
|
||||||
<app-form-field
|
(uploadMsg)="dispatch({ tag: 'Upload', msg: $event })"
|
||||||
i18n-label="@@regWizard.diplomaLabel"
|
(antwoordChange)="
|
||||||
label="Kies het diploma waarmee u zich wilt registreren"
|
dispatch({ tag: 'SetAntwoord', vraagId: $event.vraagId, value: $event.value })
|
||||||
fieldId="diploma"
|
|
||||||
required
|
|
||||||
[error]="err('diploma')"
|
|
||||||
>
|
|
||||||
<app-radio-group
|
|
||||||
name="diploma"
|
|
||||||
[options]="diplomaOptions(data)"
|
|
||||||
[invalid]="!!err('diploma')"
|
|
||||||
[ngModel]="diplomaKeuze()"
|
|
||||||
(ngModelChange)="onDiplomaKeuze(data, $event)"
|
|
||||||
/>
|
|
||||||
</app-form-field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
@if (handmatigActief()) {
|
|
||||||
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing"
|
|
||||||
>Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies
|
|
||||||
uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna
|
|
||||||
handmatig beoordeeld.</app-alert
|
|
||||||
>
|
|
||||||
<fieldset>
|
|
||||||
<app-form-field
|
|
||||||
i18n-label="@@regWizard.beroepLabel"
|
|
||||||
label="Voor welk beroep wilt u zich registreren?"
|
|
||||||
fieldId="hm-beroep"
|
|
||||||
[error]="err('diploma')"
|
|
||||||
>
|
|
||||||
<app-radio-group
|
|
||||||
name="hm-beroep"
|
|
||||||
[options]="beroepOptions(data)"
|
|
||||||
[invalid]="!!err('diploma')"
|
|
||||||
[ngModel]="draft().beroep ?? ''"
|
|
||||||
(ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })"
|
|
||||||
/>
|
|
||||||
</app-form-field>
|
|
||||||
</fieldset>
|
|
||||||
} @else if (draft().beroep) {
|
|
||||||
<app-data-block class="app-section">
|
|
||||||
<div
|
|
||||||
app-data-row
|
|
||||||
i18n-key="@@regWizard.beroepAfgeleid"
|
|
||||||
key="Beroep (afgeleid uit diploma)"
|
|
||||||
[value]="draft().beroep ?? ''"
|
|
||||||
></div>
|
|
||||||
</app-data-block>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (actieveVragen(data).length) {
|
|
||||||
<fieldset>
|
|
||||||
@for (q of actieveVragen(data); track q.id) {
|
|
||||||
<app-form-field
|
|
||||||
[label]="q.vraag"
|
|
||||||
[fieldId]="'vraag-' + q.id"
|
|
||||||
[error]="vraagErr(q.id)"
|
|
||||||
>
|
|
||||||
@if (q.type === 'ja-nee') {
|
|
||||||
<app-radio-group
|
|
||||||
[name]="'vraag-' + q.id"
|
|
||||||
[options]="jaNee"
|
|
||||||
[invalid]="!!vraagErr(q.id)"
|
|
||||||
[ngModel]="antwoord(q.id)"
|
|
||||||
(ngModelChange)="
|
|
||||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
|
||||||
"
|
"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
(diplomaChosen)="onDiplomaKeuze($event)"
|
||||||
|
(beroepDeclared)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })"
|
||||||
/>
|
/>
|
||||||
} @else {
|
|
||||||
<app-text-input
|
|
||||||
[inputId]="'vraag-' + q.id"
|
|
||||||
[invalid]="!!vraagErr(q.id)"
|
|
||||||
[ngModel]="antwoord(q.id)"
|
|
||||||
(ngModelChange)="
|
|
||||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
|
||||||
"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</app-form-field>
|
|
||||||
}
|
|
||||||
</fieldset>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</ng-template>
|
|
||||||
<ng-template appAsyncLoading>
|
|
||||||
<app-skeleton height="2.5rem" [count]="3" />
|
|
||||||
</ng-template>
|
|
||||||
</app-async>
|
|
||||||
|
|
||||||
<app-document-upload
|
|
||||||
class="app-section"
|
|
||||||
[state]="upload()"
|
|
||||||
[previewUrlFor]="previewUrlFor"
|
|
||||||
(fileSelected)="uploadCtl.onFileSelected($event.categoryId, $event.files)"
|
|
||||||
(removeUpload)="uploadCtl.onRemove($event)"
|
|
||||||
(retryUpload)="uploadCtl.onRetry($event)"
|
|
||||||
(deleteUpload)="uploadCtl.onDelete($event)"
|
|
||||||
(channelChange)="uploadCtl.onChannelChange($event.categoryId, $event.channel)"
|
|
||||||
/>
|
|
||||||
@if (err('documenten')) {
|
|
||||||
<app-alert type="warning">{{ err('documenten') }}</app-alert>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@case ('controle') {
|
@case ('controle') {
|
||||||
<app-alert type="info" i18n="@@regWizard.controleer"
|
<app-reg-controle-step
|
||||||
>Controleer uw gegevens en dien de registratie in.</app-alert
|
[draft]="draft()"
|
||||||
>
|
(edit)="dispatch({ tag: 'GaNaarStap', cursor: $event })"
|
||||||
<app-review-section
|
/>
|
||||||
i18n-heading="@@regWizard.sectie.adres"
|
|
||||||
heading="Adres en correspondentie"
|
|
||||||
i18n-editAriaLabel="@@regWizard.adresWijzigenAria"
|
|
||||||
editAriaLabel="Wijzigen adresgegevens"
|
|
||||||
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 0 })"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
app-data-row
|
|
||||||
i18n-key="@@regWizard.summary.adres"
|
|
||||||
key="Adres"
|
|
||||||
[value]="adresSamenvatting()"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
app-data-row
|
|
||||||
i18n-key="@@regWizard.summary.herkomstAdres"
|
|
||||||
key="Herkomst adres"
|
|
||||||
[value]="adresHerkomstLabel()"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
app-data-row
|
|
||||||
i18n-key="@@regWizard.summary.correspondentie"
|
|
||||||
key="Correspondentie"
|
|
||||||
[value]="correspondentieLabel()"
|
|
||||||
></div>
|
|
||||||
@if (draft().correspondentie === 'email') {
|
|
||||||
<div
|
|
||||||
app-data-row
|
|
||||||
i18n-key="@@regWizard.summary.email"
|
|
||||||
key="E-mailadres"
|
|
||||||
[value]="draft().email ?? ''"
|
|
||||||
></div>
|
|
||||||
}
|
|
||||||
</app-review-section>
|
|
||||||
<app-review-section
|
|
||||||
class="app-section"
|
|
||||||
i18n-heading="@@regWizard.sectie.beroep"
|
|
||||||
heading="Beroep en diploma"
|
|
||||||
i18n-editAriaLabel="@@regWizard.diplomaWijzigenAria"
|
|
||||||
editAriaLabel="Wijzigen beroep en diploma"
|
|
||||||
(edit)="dispatch({ tag: 'GaNaarStap', cursor: 1 })"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
app-data-row
|
|
||||||
i18n-key="@@regWizard.summary.beroep"
|
|
||||||
key="Beroep"
|
|
||||||
[value]="draft().beroep ?? ''"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
app-data-row
|
|
||||||
i18n-key="@@regWizard.summary.herkomstDiploma"
|
|
||||||
key="Herkomst diploma"
|
|
||||||
[value]="diplomaHerkomstLabel()"
|
|
||||||
></div>
|
|
||||||
@for (item of samenvattingVragen(); track item.vraag) {
|
|
||||||
<div app-data-row [key]="item.vraag" [value]="item.antwoord"></div>
|
|
||||||
}
|
|
||||||
</app-review-section>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,16 +130,9 @@ export class RegistratieWizardComponent {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Preview/download link for a completed upload; delegates to the upload
|
|
||||||
controller (application layer), which knows the dev-simulation `demo-*` ids
|
|
||||||
have no stored bytes and returns no link for them. */
|
|
||||||
protected previewUrlFor = (documentId: string): string | undefined =>
|
|
||||||
this.uploadCtl.previewUrlFor(documentId);
|
|
||||||
|
|
||||||
/** Optional seed so Storybook / tests can mount any state directly. */
|
/** Optional seed so Storybook / tests can mount any state directly. */
|
||||||
seed = input<RegistratieState>(initial);
|
seed = input<RegistratieState>(initial);
|
||||||
|
|
||||||
readonly kanalen = KANALEN;
|
|
||||||
readonly stepLabels = [
|
readonly stepLabels = [
|
||||||
$localize`:@@regWizard.step.adres:Adres`,
|
$localize`:@@regWizard.step.adres:Adres`,
|
||||||
$localize`:@@regWizard.step.beroep:Beroep`,
|
$localize`:@@regWizard.step.beroep:Beroep`,
|
||||||
@@ -407,19 +149,8 @@ export class RegistratieWizardComponent {
|
|||||||
private invullen = computed(() => whenTag(this.state(), 'Invullen'));
|
private invullen = computed(() => whenTag(this.state(), 'Invullen'));
|
||||||
protected cursor = computed(() => this.invullen()?.cursor ?? 0);
|
protected cursor = computed(() => this.invullen()?.cursor ?? 0);
|
||||||
protected draft = computed<Draft>(() => this.invullen()?.draft ?? { antwoorden: {} });
|
protected draft = computed<Draft>(() => this.invullen()?.draft ?? { antwoorden: {} });
|
||||||
|
protected errors = computed<Errors>(() => this.invullen()?.errors ?? {});
|
||||||
protected upload = computed<UploadState>(() => this.invullen()?.upload ?? initialUpload);
|
protected upload = computed<UploadState>(() => this.invullen()?.upload ?? initialUpload);
|
||||||
protected uploadCtl = createUploadController({
|
|
||||||
wizardId: 'registratie',
|
|
||||||
getUpload: () => this.upload(),
|
|
||||||
dispatch: (msg) => this.dispatch({ tag: 'Upload', msg }),
|
|
||||||
// Required documents depend on answers (server decides): a diploma upload only for a
|
|
||||||
// manual diploma; a Dutch-taalvaardigheid upload only once the applicant confirms
|
|
||||||
// ("ja") the B2 language requirement.
|
|
||||||
getCategoryParams: () => ({
|
|
||||||
diplomaHerkomst: this.draft().diplomaHerkomst,
|
|
||||||
taalvaardigheid: this.draft().antwoorden[NL_TAALVAARDIGHEID_VRAAG],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
// Backend draft-sync (replaces sessionStorage): create a Concept once the user has
|
// Backend draft-sync (replaces sessionStorage): create a Concept once the user has
|
||||||
// made progress, then debounced-sync the whole machine snapshot; resume by `?aanvraag`.
|
// made progress, then debounced-sync the whole machine snapshot; resume by `?aanvraag`.
|
||||||
private draftSync = createDraftSync({
|
private draftSync = createDraftSync({
|
||||||
@@ -471,101 +202,16 @@ export class RegistratieWizardComponent {
|
|||||||
const e = this.invullen()?.errors ?? {};
|
const e = this.invullen()?.errors ?? {};
|
||||||
return [...toWizardErrors(e), ...toWizardErrors(e.antwoorden ?? {}, 'vraag-')];
|
return [...toWizardErrors(e), ...toWizardErrors(e.antwoorden ?? {}, 'vraag-')];
|
||||||
});
|
});
|
||||||
protected adresSamenvatting = computed(() => {
|
|
||||||
const d = this.draft();
|
|
||||||
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ');
|
|
||||||
});
|
|
||||||
// Readable labels for the controle summary (instead of raw enum values).
|
|
||||||
protected adresHerkomstLabel = computed(
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`,
|
|
||||||
handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd`,
|
|
||||||
})[this.draft().adresHerkomst ?? 'handmatig'],
|
|
||||||
);
|
|
||||||
protected correspondentieLabel = computed(
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
email: $localize`:@@regWizard.corr.email:Per e-mail`,
|
|
||||||
post: $localize`:@@regWizard.corr.post:Per post`,
|
|
||||||
})[this.draft().correspondentie ?? 'post'],
|
|
||||||
);
|
|
||||||
protected diplomaHerkomstLabel = computed(
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`,
|
|
||||||
handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)`,
|
|
||||||
})[this.draft().diplomaHerkomst ?? 'handmatig'],
|
|
||||||
);
|
|
||||||
|
|
||||||
/** BRP lookup outcome (laden/gevonden/geen/fout) and the parsed DUO lookup, both
|
/** Parsed lookup as a plain value (or null) — needed here only to resolve
|
||||||
served by the application facade — the wizard renders, it does not fetch/parse. */
|
`onDiplomaKeuze`'s message from an id (the DUO payload maps an id to a
|
||||||
protected adresStatus = this.lookup.adresStatus;
|
beroep and its question ids; that is machine-message construction, and it
|
||||||
protected lookupRd: () => RemoteData<Error | undefined, DuoLookupDto> = this.lookup.duoLookup;
|
belongs in the container, not the beroep step). */
|
||||||
|
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookup.duoLookup(), null));
|
||||||
|
|
||||||
/** Parsed lookup as a plain value (or null) — used outside the beroep step (the
|
protected onDiplomaKeuze(id: string) {
|
||||||
controle summary) where the <app-async> template variable isn't in scope, and
|
|
||||||
inside it too: `<ng-template appAsyncLoaded>`'s own context can't inherit a
|
|
||||||
generic from the sibling [data] input (Angular only infers a structural
|
|
||||||
directive's type parameter from an input on that same node). */
|
|
||||||
protected duoData = computed<DuoLookupDto | null>(() => successOr(this.lookupRd(), null));
|
|
||||||
|
|
||||||
readonly jaNee = JA_NEE;
|
|
||||||
|
|
||||||
protected err = (k: DraftField | 'correspondentie' | 'diploma' | 'documenten') =>
|
|
||||||
this.invullen()?.errors[k] ?? '';
|
|
||||||
protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';
|
|
||||||
protected antwoord = (id: string) => this.draft().antwoorden[id] ?? ''; // runtime guard: missing key → undefined
|
|
||||||
protected set = (key: DraftField, value: string) =>
|
|
||||||
this.dispatch({ tag: 'SetField', key, value });
|
|
||||||
protected setKanaal = (value: string) =>
|
|
||||||
this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });
|
|
||||||
|
|
||||||
/** True while the user is entering a diploma manually (not in the DUO list). */
|
|
||||||
protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');
|
|
||||||
/** The radio selection: a diploma id, or the"not listed" sentinel in manual mode. */
|
|
||||||
protected diplomaKeuze = computed(() =>
|
|
||||||
this.handmatigActief() ? HANDMATIG : (this.draft().diplomaId ?? ''),
|
|
||||||
);
|
|
||||||
|
|
||||||
protected diplomaOptions = (data: DuoLookupDto) => [
|
|
||||||
...data.diplomas.map((d) => ({
|
|
||||||
value: d.id,
|
|
||||||
label: `${d.naam} — ${d.instelling} (${d.jaar})`,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
value: HANDMATIG,
|
|
||||||
label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
protected beroepOptions = (data: DuoLookupDto) =>
|
|
||||||
data.handmatig.beroepen.map((b) => ({ value: b, label: b }));
|
|
||||||
|
|
||||||
/** The policy questions that apply to the current choice (server-decided). */
|
|
||||||
protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {
|
|
||||||
if (this.handmatigActief()) return data.handmatig.policyQuestions;
|
|
||||||
return data.diplomas.find((d) => d.id === this.draft().diplomaId)?.policyQuestions ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Answered policy questions for the controle summary (question text + answer). */
|
|
||||||
protected samenvattingVragen = computed(() => {
|
|
||||||
const data = this.duoData();
|
const data = this.duoData();
|
||||||
const d = this.draft();
|
if (!data) return;
|
||||||
if (!data) return [] as { vraag: string; antwoord: string }[];
|
|
||||||
const alle = [
|
|
||||||
...data.diplomas.flatMap((x) => x.policyQuestions),
|
|
||||||
...data.handmatig.policyQuestions,
|
|
||||||
];
|
|
||||||
return (d.vraagIds ?? []).map((id) => ({
|
|
||||||
vraag: alle.find((q) => q.id === id)?.vraag ?? id,
|
|
||||||
antwoord: d.antwoorden[id] ?? '',
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
protected onDiplomaKeuze(data: DuoLookupDto, id: string) {
|
|
||||||
if (id === HANDMATIG) {
|
if (id === HANDMATIG) {
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
tag: 'KiesHandmatig',
|
tag: 'KiesHandmatig',
|
||||||
@@ -583,6 +229,12 @@ export class RegistratieWizardComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Narrows the beroep step's plain-string `kanaalChange` into the machine's
|
||||||
|
`Correspondentie` union before dispatching. */
|
||||||
|
protected onKanaalChange(value: string) {
|
||||||
|
this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// An explicit seed (stories/tests) wins; otherwise resume from the backend draft
|
// An explicit seed (stories/tests) wins; otherwise resume from the backend draft
|
||||||
// (`?aanvraag=<id>`), or start fresh. Persistence is the draftSync controller's job.
|
// (`?aanvraag=<id>`), or start fresh. Persistence is the draftSync controller's job.
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
# RD-23 — Split `registratie-wizard` into three steps, and move the upload controller
|
||||||
|
|
||||||
|
Status: done
|
||||||
|
Source: PLAN.md 3c, order step 5
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
`registratie-wizard.component.ts` measures ~568 effective lines against a limit of 250 — the
|
||||||
|
largest file in the arc, and more than twice the budget. It carries
|
||||||
|
`/* eslint-disable max-lines */`.
|
||||||
|
|
||||||
|
It is the same shape as RD-22's intake wizard: one `@switch`, three `@case` blocks, three
|
||||||
|
screens in one file. It is harder in one way that PLAN calls out — **moving the upload
|
||||||
|
controller is what gets the parent under 250**, and the controller is a stateful thing, not
|
||||||
|
markup.
|
||||||
|
|
||||||
|
RD-22 already set the `*.step.ts` convention. Follow it.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
|
||||||
|
- `apps/ssp/src/app/herregistratie/ui/intake-wizard/buitenland.step.ts` and `review.step.ts` —
|
||||||
|
**the shape to copy.** RD-22 built them one ticket ago; match their header comments, their
|
||||||
|
`input.required` style and their output naming.
|
||||||
|
- `registratie-wizard.component.ts:100-348` — the three `@case` blocks.
|
||||||
|
- `registratie-wizard.component.ts:411-422` — `createUploadController`, and the `dispatch`
|
||||||
|
callback that decision 4 rewires.
|
||||||
|
- `registratie-wizard.component.ts:568-585` — `onDiplomaKeuze`, which decision 5 reshapes.
|
||||||
|
- `apps/ssp/src/app/registratie/ui/address-fields/address-fields.component.ts:13-18` — the
|
||||||
|
contract the whole family follows.
|
||||||
|
|
||||||
|
## Decisions (pre-made, don't relitigate)
|
||||||
|
|
||||||
|
1. **Three new files beside the parent, named as RD-22 named its own:**
|
||||||
|
|
||||||
|
| File | Class | Selector | Case |
|
||||||
|
| ------------------ | -------------- | ----------------------- | ------------- |
|
||||||
|
| `adres.step.ts` | `AdresStep` | `app-reg-adres-step` | lines 100-175 |
|
||||||
|
| `beroep.step.ts` | `BeroepStep` | `app-reg-beroep-step` | lines 176-285 |
|
||||||
|
| `controle.step.ts` | `ControleStep` | `app-reg-controle-step` | lines 286-348 |
|
||||||
|
|
||||||
|
2. **A step may inject `RegistratieLookupStore` directly. This is the sanctioned exception.**
|
||||||
|
It is `providedIn: 'root'`, so every injection is the same instance, and PLAN names this "the
|
||||||
|
one place the dashboard's axis does apply": the step owns its own async presentation rather
|
||||||
|
than making the parent a pass-through for four lookup signals.
|
||||||
|
|
||||||
|
- `adres` injects it for `adresStatus` (the BRP lookup banner).
|
||||||
|
- `beroep` injects it for the DUO lookup, and owns its own `<app-async>` over it.
|
||||||
|
- `controle` injects it to build `samenvattingVragen`.
|
||||||
|
|
||||||
|
The parent keeps its own injection too — the BRP prefill effect needs it (decision 6).
|
||||||
|
|
||||||
|
3. **Inputs down, narrow outputs up, `dispatch` never passed down:**
|
||||||
|
|
||||||
|
| Step | Inputs | Outputs |
|
||||||
|
| ---------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `adres` | `draft`, `errors` | `fieldChange: { key: DraftField; value: string }`, `kanaalChange: string` |
|
||||||
|
| `beroep` | `draft`, `errors`, `upload` | `uploadMsg: UploadMsg`, `antwoordChange: { vraagId: string; value: string }`, `diplomaChosen: string`, `beroepDeclared: string` |
|
||||||
|
| `controle` | `draft` | `edit: number` |
|
||||||
|
|
||||||
|
Four outputs on `beroep` is correct: they are four distinct user intents, and each maps to one
|
||||||
|
message in the parent. That is not the same thing as handing the step a `dispatch`.
|
||||||
|
|
||||||
|
4. **The upload controller moves into `beroep.step.ts` and emits instead of dispatching.**
|
||||||
|
`createUploadController` takes a `dispatch` callback, so the step builds its own:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
protected uploadCtl = createUploadController({
|
||||||
|
wizardId: 'registratie',
|
||||||
|
getUpload: () => this.upload(),
|
||||||
|
dispatch: (msg) => this.uploadMsg.emit(msg),
|
||||||
|
getCategoryParams: () => ({ … }), // unchanged, reads this.draft()
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The parent maps it back with `(uploadMsg)="dispatch({ tag: 'Upload', msg: $event })"`. This
|
||||||
|
is what collapses five template handlers into one output. `previewUrlFor` moves with the
|
||||||
|
controller — it is `uploadCtl.previewUrlFor` and the child takes it as a function reference.
|
||||||
|
|
||||||
|
5. **`onDiplomaKeuze` stays in the parent, and loses its `data` parameter.** The step emits only
|
||||||
|
the chosen id (`diplomaChosen`). The parent keeps its `duoData` computed and reads it inside
|
||||||
|
the method instead of receiving it as an argument:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
protected onDiplomaKeuze(id: string) {
|
||||||
|
const data = this.duoData();
|
||||||
|
if (!data) return;
|
||||||
|
… // body otherwise unchanged
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Building a `KiesDiploma`/`KiesHandmatig` message needs the DUO payload to map an id to a
|
||||||
|
beroep and its question ids. That is machine-message construction, and it belongs in the
|
||||||
|
container.
|
||||||
|
|
||||||
|
6. **The BRP prefill `effect` stays in the parent**, exactly as written, including its
|
||||||
|
`untracked` call. It writes to the machine, so it belongs where the machine lives. Do not move
|
||||||
|
it into `adres.step.ts`.
|
||||||
|
|
||||||
|
7. **The parent keeps** the shell wiring, the store and its effect map, `draftSync`, the seed
|
||||||
|
constructor, `phase`, `primaryLabel`, `stepTitle`, `stepLabels`, `errorList`, `referentie`,
|
||||||
|
`cursor`, `step`, `draft`, `upload`, `duoData`, `onDiplomaKeuze`, and the `wizardSuccess`
|
||||||
|
block. It gains one computed, as RD-22's parent did:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
protected errors = computed<Errors>(() => this.invullen()?.errors ?? {});
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything else in the list below moves out with the markup that used it: `uploadCtl`,
|
||||||
|
`previewUrlFor`, `kanalen`, `err`, `vraagErr`, `antwoord`, `set`, `setKanaal`,
|
||||||
|
`handmatigActief`, `diplomaKeuze`, `diplomaOptions`, `beroepOptions`, `actieveVragen`,
|
||||||
|
`samenvattingVragen`, `adresSamenvatting`, `adresHerkomstLabel`, `correspondentieLabel`,
|
||||||
|
`diplomaHerkomstLabel`, `adresStatus`, `lookupRd`.
|
||||||
|
|
||||||
|
8. **Delete `/* eslint-disable max-lines */` from the parent.** Mandatory:
|
||||||
|
`reportUnusedDisableDirectives` is `error`, so the two rules pin each other in both
|
||||||
|
directions.
|
||||||
|
|
||||||
|
9. **No stories for the new steps** (PLAN's corollary). `registratie-wizard.stories.ts` already
|
||||||
|
mounts every step by seeding the machine, and the parent's public API does not move.
|
||||||
|
|
||||||
|
10. **Move the markup, do not improve it.** Every `i18n` id, label, placeholder, `fieldId` and
|
||||||
|
`aria` string stays byte-identical. `Errors` is already exported from the machine — RD-20
|
||||||
|
made it a type alias — so no machine change is needed this time.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `apps/ssp/src/app/registratie/ui/registratie-wizard/adres.step.ts` (new)
|
||||||
|
- `apps/ssp/src/app/registratie/ui/registratie-wizard/beroep.step.ts` (new)
|
||||||
|
- `apps/ssp/src/app/registratie/ui/registratie-wizard/controle.step.ts` (new)
|
||||||
|
- `apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts`
|
||||||
|
|
||||||
|
No machine file changes. No shared-library changes.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Write `adres.step.ts` — the smallest, and the one that proves the injection pattern.
|
||||||
|
2. Write `controle.step.ts` — read-only markup plus one output.
|
||||||
|
3. Write `beroep.step.ts` last: it carries the async lookup, the policy questions and the upload
|
||||||
|
controller.
|
||||||
|
4. Replace the `@switch` with the three elements and wire the outputs per decision 3.
|
||||||
|
5. Delete the members listed in decision 7, add the `errors` computed, reshape `onDiplomaKeuze`
|
||||||
|
per decision 5, prune `imports:`.
|
||||||
|
6. Delete the disable (decision 8).
|
||||||
|
7. `git add -A`, then run the acceptance commands.
|
||||||
|
8. Update this ticket's `Status:` to `done` and the README's RD-23 row to `done`.
|
||||||
|
9. Commit all of it together.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
Measured against the tree before handover. Run after `git add -A`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
D=apps/ssp/src/app/registratie/ui/registratie-wizard
|
||||||
|
P=$D/registratie-wizard.component.ts
|
||||||
|
git ls-files "$D/*.step.ts" | wc -l # is 0 -> MUST be 3
|
||||||
|
git grep -c "eslint-disable max-lines" -- $P # is 1 -> MUST be 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The upload controller moved, and the store did not follow the markup down:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -c "uploadCtl" -- $P # is 7 -> MUST be 0
|
||||||
|
git grep -c "createUploadController" -- $D/beroep.step.ts # MUST be 2
|
||||||
|
git grep -c "dispatch" -- "$D/*.step.ts" | awk -F: '{s+=$NF} END {print s+0}' # MUST be 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Two corrections found while running these before handover (recorded here per the README's
|
||||||
|
rule 4 on ticket-writing misses):
|
||||||
|
|
||||||
|
- `createUploadController` is **2**, not 1: `git grep -c` counts matching lines, and an
|
||||||
|
import plus its one call site are always two lines (same shape as `createStore` in
|
||||||
|
`intake-wizard.component.ts`, which is 2, and `createDraftSync` in this same parent, which
|
||||||
|
is 3). A count of 1 is unreachable without an import alias that would exist only to dodge
|
||||||
|
the check.
|
||||||
|
- `dispatch` is **1**, not 0: decision 4's mandated snippet is
|
||||||
|
`dispatch: (msg) => this.uploadMsg.emit(msg),` — the `UploadControllerDeps.dispatch`
|
||||||
|
property name is not the machine's `dispatch`, but it is the same string. Satisfying
|
||||||
|
decision 4 verbatim and satisfying a target of 0 are mutually exclusive.
|
||||||
|
|
||||||
|
The three per-step concerns left the parent:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -c "adresStatus" -- $P # is 3 -> MUST be 0
|
||||||
|
git grep -c "samenvattingVragen" -- $P # is 2 -> MUST be 0
|
||||||
|
git grep -c "previewUrlFor" -- $P # is 3 -> MUST be 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The copy did not drift (decision 10):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -ho "@@[a-zA-Z0-9_.]*" -- $D/ | sort -u | wc -l # is 43 -> MUST still be 43
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run ci --full # exits 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
The `@@` id count is **43** across the whole `registratie-wizard/` directory, so the three new
|
||||||
|
files are included. `ng build --localize` fails on an id that is _added_ without a translation
|
||||||
|
but never on one silently _lost_; the count is the only check that catches a loss.
|
||||||
|
|
||||||
|
**`--full` is required.** The existing story mounts all three steps, and the axe run over it is
|
||||||
|
what proves the projected markup kept its labels, its error wiring and its `aria` strings.
|
||||||
|
|
||||||
|
**Do not add a line-count command.** `npm run lint` is the exact check (decision 8).
|
||||||
|
|
||||||
|
If `dotnet test` fails with `SQLite Error 1: 'no such table: …'`, that is the stale-database
|
||||||
|
trap, not your change. See the README's Troubleshooting section — RD-22 hit it.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- `herregistratie-wizard`. PLAN: do not split it for symmetry.
|
||||||
|
- Changing the upload controller itself, or `upload.machine.ts`.
|
||||||
|
- Moving the BRP prefill effect (decision 6).
|
||||||
|
- Adding stories (decision 9).
|
||||||
|
- Any validation, message or `i18n` change.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **The upload controller is the hard part, and the reason this ticket exists.** Five template
|
||||||
|
handlers become one `uploadMsg` output. Get the `dispatch: (msg) => this.uploadMsg.emit(msg)`
|
||||||
|
wiring right and the rest is markup movement.
|
||||||
|
- **`previewUrlFor` is passed to a child as a function reference**, not called in the template.
|
||||||
|
Keep it an arrow property on the step, or the binding silently loses its `this`.
|
||||||
|
- **`onDiplomaKeuze` must not move into the step** (decision 5). It builds machine messages from
|
||||||
|
the DUO payload.
|
||||||
|
- **Four outputs on `beroep` is the design, not a smell** (decision 3). Do not collapse them into
|
||||||
|
a single message-shaped output — that is passing `dispatch` up under another name, and it
|
||||||
|
moves message construction into the step.
|
||||||
|
- **Deleting the disable is mandatory** (decision 8); its failure message reads like an
|
||||||
|
unrelated error.
|
||||||
|
- **This is the largest single diff in the arc.** Work step by step in the order given, and let
|
||||||
|
the type-checker confirm each before moving on.
|
||||||
@@ -117,7 +117,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
|||||||
| RD-20 | `wizard-errors.ts` + spec, adopted by all 3 wizards | 02 | | done |
|
| RD-20 | `wizard-errors.ts` + spec, adopted by all 3 wizards | 02 | | done |
|
||||||
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | done |
|
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | done |
|
||||||
| RD-22 | `intake-wizard` to 3 step components | 08, 20 | yes | done |
|
| RD-22 | `intake-wizard` to 3 step components | 08, 20 | yes | done |
|
||||||
| RD-23 | `registratie-wizard` to 3 steps + the upload-controller move | 08, 20 | yes | todo |
|
| RD-23 | `registratie-wizard` to 3 steps + the upload-controller move | 08, 20 | yes | done |
|
||||||
| RD-24 | `concepts.page` to 6 sections + `concept-card` + globals + code tokens | 02 | yes | todo |
|
| RD-24 | `concepts.page` to 6 sections + `concept-card` + globals + code tokens | 02 | yes | todo |
|
||||||
| RD-25 | `org-template-editor` to `sample-letter.ts` + labels + 2 children | 02 | yes | todo |
|
| RD-25 | `org-template-editor` to `sample-letter.ts` + labels + 2 children | 02 | yes | todo |
|
||||||
| RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | todo |
|
| RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | todo |
|
||||||
|
|||||||
Reference in New Issue
Block a user