diff --git a/docs/backlog/README.md b/docs/backlog/README.md index ce75b35..26ad530 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -45,7 +45,7 @@ for its existing violations, so every WP ends green. | [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done | | [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done | | [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done | -| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | todo | +| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done | | [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | todo | | [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | todo | | [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | todo | diff --git a/docs/backlog/WP-06-typed-async.md b/docs/backlog/WP-06-typed-async.md index 97ef204..718c255 100644 --- a/docs/backlog/WP-06-typed-async.md +++ b/docs/backlog/WP-06-typed-async.md @@ -1,6 +1,6 @@ # WP-06 — Generic async template contexts: kill `$any()` (18×) -Status: todo +Status: done Phase: 1 — FP/DDD core ## Why @@ -44,19 +44,43 @@ let-p>` consumer must cast. The rest are template union-narrowing workarounds. ## Acceptance criteria -- [ ] `grep -rn '\$any(' src/app` → zero hits. -- [ ] No `as` casts added to compensate in component classes (typed getters are fine). -- [ ] Build green with strict template checking. +- [x] `grep -rn '\$any(' src/app` → zero hits. +- [x] No `as` casts added to compensate in component classes (typed getters are fine). +- [x] Build green with strict template checking. ## Verification -GREEN + `npm run test-storybook:ci`. Smoke: dashboard + registration detail render. +GREEN + `npm run test-storybook:ci` (one unrelated flake on `review-section.stories.ts`'s +smoke-test timeout, confirmed by re-running green — untouched by this WP). Manual smoke +via a running `docker compose` stack + Playwright: logged in, drove `/dashboard`, +`/registratie` (registration-detail), `/aanvraag/:id`, `/concepts`, and the +`/registreren` wizard through the beroep step (both the DUO-match and the "mijn diploma +staat er niet bij" handmatig branch) — every fixed template renders its real data with +no console errors. -## Out of scope +## Deviation from the original plan -Brief page's `` adoption (WP-07 — it depends on this WP's typing). +`AsyncLoadedDirective` + `static ngTemplateContextGuard` **was added** (per the +Decisions block) and is real, working generic typing for `AsyncComponent`'s own +internals. But it does **not**, and structurally **cannot**, remove `$any()` at the ~9 +"root cause" consumer sites (dashboard, registration-detail, aanvraag-detail): Angular +only infers a structural directive's type parameter from an **input bound on that same +node** (see `NgFor`'s `ngForOf`, or `*ngIf="x as y"`'s `ngIf` input) — a generic on a +directive that has no input of its own cannot inherit a type from a sibling input on the +parent `` element, even though the two are nested in the same template. This +is a hard limitation of Angular's template type-checker, not a gap in this +implementation (confirmed against the documented `ngTemplateContextGuard` pattern and by +the compiler continuing to type `let-p` as `unknown` after the generic was added). -## Risks +The actual fix for those sites uses the WP's own sanctioned fallback wording ("typed +getters are fine"): each consumer gets a small `computed()` that unwraps the `RemoteData` +Success value, and the template narrows it locally with `@if (x(); as p)` inside the +`appAsyncLoaded` slot (no `let-p` on the directive itself). `registratie-wizard` reused +its existing `duoData` computed instead of adding a new one. The registration-summary +union-narrowing case used the anticipated `@switch` fix, but needed a `@let status = +reg().status` binding first — `@switch`/`@case` only narrows a stable local, not a +repeated `reg().status` function call. The showcase fake-resource case (`successRes`) +just reads `successRes.value()` directly in the `@for`, skipping `let-v` entirely. -Angular generic-component inference edge cases — the documented fallback keeps the WP -bounded. +`AsyncComponent`'s public API (`[data]`/`[resource]` inputs) is unchanged, so this +deviation is contained to consumer templates, as the WP intended. diff --git a/documentation.json b/documentation.json index 0f38839..3012163 100644 --- a/documentation.json +++ b/documentation.json @@ -11173,12 +11173,12 @@ "directives": [ { "name": "AsyncEmptyDirective", - "id": "directive-AsyncEmptyDirective-8cd95468f2e83b9befe7f37bc6281e635371d590157ae555f51eec4cc9a354ec43e6fbe08d7600ecffbf30ddc2a8631783a98f02fd56547a461aab44ec507259", + "id": "directive-AsyncEmptyDirective-8829fd8ba48f54d52dc1e5e88de58ac991b4ce5057e5c9e5c3406750af3ead689455704d8c75dec9c59329c59ad11c4dc2ce2b03381bf0a2062107d6ebfc18f3", "file": "src/app/shared/ui/async/async.component.ts", "type": "directive", "description": "", "rawdescription": "\n", - "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: unknown }>) {}\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", + "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . Generic so the\n $implicit context is typed as the resource's T instead of unknown — see\n AsyncComponent's contentChild> below, which threads the\n host's own T through the query result type. */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: T }>) {}\n\n static ngTemplateContextGuard(\n _dir: AsyncLoadedDirective,\n _ctx: unknown,\n ): _ctx is { $implicit: T } {\n return true;\n }\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required>(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", "selector": "[appAsyncEmpty]", "providers": [], "hostDirectives": [], @@ -11198,7 +11198,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 20, + "line": 30, "modifierKind": [ 125 ] @@ -11221,7 +11221,7 @@ "deprecationMessage": "" } ], - "line": 19, + "line": 29, "jsdoctags": [ { "name": "tpl", @@ -11239,12 +11239,12 @@ }, { "name": "AsyncErrorDirective", - "id": "directive-AsyncErrorDirective-8cd95468f2e83b9befe7f37bc6281e635371d590157ae555f51eec4cc9a354ec43e6fbe08d7600ecffbf30ddc2a8631783a98f02fd56547a461aab44ec507259", + "id": "directive-AsyncErrorDirective-8829fd8ba48f54d52dc1e5e88de58ac991b4ce5057e5c9e5c3406750af3ead689455704d8c75dec9c59329c59ad11c4dc2ce2b03381bf0a2062107d6ebfc18f3", "file": "src/app/shared/ui/async/async.component.ts", "type": "directive", "description": "", "rawdescription": "\n", - "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: unknown }>) {}\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", + "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . Generic so the\n $implicit context is typed as the resource's T instead of unknown — see\n AsyncComponent's contentChild> below, which threads the\n host's own T through the query result type. */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: T }>) {}\n\n static ngTemplateContextGuard(\n _dir: AsyncLoadedDirective,\n _ctx: unknown,\n ): _ctx is { $implicit: T } {\n return true;\n }\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required>(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", "selector": "[appAsyncError]", "providers": [], "hostDirectives": [], @@ -11264,7 +11264,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 24, + "line": 34, "modifierKind": [ 125 ] @@ -11287,7 +11287,7 @@ "deprecationMessage": "" } ], - "line": 23, + "line": 33, "jsdoctags": [ { "name": "tpl", @@ -11305,12 +11305,12 @@ }, { "name": "AsyncLoadedDirective", - "id": "directive-AsyncLoadedDirective-8cd95468f2e83b9befe7f37bc6281e635371d590157ae555f51eec4cc9a354ec43e6fbe08d7600ecffbf30ddc2a8631783a98f02fd56547a461aab44ec507259", + "id": "directive-AsyncLoadedDirective-8829fd8ba48f54d52dc1e5e88de58ac991b4ce5057e5c9e5c3406750af3ead689455704d8c75dec9c59329c59ad11c4dc2ce2b03381bf0a2062107d6ebfc18f3", "file": "src/app/shared/ui/async/async.component.ts", "type": "directive", "description": "", "rawdescription": "\n", - "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: unknown }>) {}\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", + "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . Generic so the\n $implicit context is typed as the resource's T instead of unknown — see\n AsyncComponent's contentChild> below, which threads the\n host's own T through the query result type. */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: T }>) {}\n\n static ngTemplateContextGuard(\n _dir: AsyncLoadedDirective,\n _ctx: unknown,\n ): _ctx is { $implicit: T } {\n return true;\n }\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required>(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", "selector": "[appAsyncLoaded]", "providers": [], "hostDirectives": [], @@ -11330,13 +11330,70 @@ "indexKey": "", "optional": false, "description": "", - "line": 12, + "line": 15, "modifierKind": [ 125 ] } ], - "methodsClass": [], + "methodsClass": [ + { + "name": "ngTemplateContextGuard", + "args": [ + { + "name": "_dir", + "type": "AsyncLoadedDirective", + "optional": false, + "dotDotDotToken": false, + "deprecated": false, + "deprecationMessage": "" + }, + { + "name": "_ctx", + "type": "unknown", + "optional": false, + "dotDotDotToken": false, + "deprecated": false, + "deprecationMessage": "" + } + ], + "optional": false, + "returnType": "literal type", + "typeParameters": [ + "T" + ], + "line": 17, + "deprecated": false, + "deprecationMessage": "", + "modifierKind": [ + 126 + ], + "jsdoctags": [ + { + "name": "_dir", + "type": "AsyncLoadedDirective", + "optional": false, + "dotDotDotToken": false, + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + }, + { + "name": "_ctx", + "type": "unknown", + "optional": false, + "dotDotDotToken": false, + "deprecated": false, + "deprecationMessage": "", + "tagName": { + "text": "param" + } + } + ] + } + ], "extends": [], "constructorObj": { "name": "constructor", @@ -11353,7 +11410,7 @@ "deprecationMessage": "" } ], - "line": 11, + "line": 14, "jsdoctags": [ { "name": "tpl", @@ -11371,12 +11428,12 @@ }, { "name": "AsyncLoadingDirective", - "id": "directive-AsyncLoadingDirective-8cd95468f2e83b9befe7f37bc6281e635371d590157ae555f51eec4cc9a354ec43e6fbe08d7600ecffbf30ddc2a8631783a98f02fd56547a461aab44ec507259", + "id": "directive-AsyncLoadingDirective-8829fd8ba48f54d52dc1e5e88de58ac991b4ce5057e5c9e5c3406750af3ead689455704d8c75dec9c59329c59ad11c4dc2ce2b03381bf0a2062107d6ebfc18f3", "file": "src/app/shared/ui/async/async.component.ts", "type": "directive", "description": "", "rawdescription": "\n", - "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: unknown }>) {}\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", + "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . Generic so the\n $implicit context is typed as the resource's T instead of unknown — see\n AsyncComponent's contentChild> below, which threads the\n host's own T through the query result type. */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: T }>) {}\n\n static ngTemplateContextGuard(\n _dir: AsyncLoadedDirective,\n _ctx: unknown,\n ): _ctx is { $implicit: T } {\n return true;\n }\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required>(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", "selector": "[appAsyncLoading]", "providers": [], "hostDirectives": [], @@ -11396,7 +11453,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 16, + "line": 26, "modifierKind": [ 125 ] @@ -11419,7 +11476,7 @@ "deprecationMessage": "" } ], - "line": 15, + "line": 25, "jsdoctags": [ { "name": "tpl", @@ -11578,7 +11635,7 @@ }, { "name": "AanvraagDetailPage", - "id": "component-AanvraagDetailPage-53f1a61941e85787f5b695bb526bc9fd66d2027fbd03ef5613cbb5bc5210c51a45398dc717fe634ba8c36521379e428cf4f78eb291e373f4e5e9374e86c78adc", + "id": "component-AanvraagDetailPage-959c5ff317b2e8931ca7883e6e006b667cba68eeb3e1a0f5cd5f595a76b75811cec95993f15cebf6f4bc705ee5f8ab6bdf5f4fc2379d8ec707c257e6d6b8d2c4", "file": "src/app/registratie/ui/aanvraag-detail.page.ts", "encapsulation": [], "entryComponents": [], @@ -11588,13 +11645,29 @@ "selector": "app-aanvraag-detail-page", "styleUrls": [], "styles": [], - "template": "\n \n \n @let a = find($any(list));\n @if (a) {\n \n @for (row of rows(a); track row.key) {\n
\n }\n \n \n De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.\n \n } @else {\n Deze aanvraag is niet gevonden.\n }\n
\n \n \n \n
\n\n", + "template": "\n \n \n @if (applications(); as list) {\n @let a = find(list);\n @if (a) {\n \n @for (row of rows(a); track row.key) {\n
\n }\n \n \n De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.\n \n } @else {\n Deze aanvraag is niet gevonden.\n }\n }\n
\n \n \n \n
\n\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], "inputsClass": [], "outputsClass": [], "propertiesClass": [ + { + "name": "applications", + "defaultValue": "computed(() => {\n const rd = this.store.applications();\n return rd.tag === 'Success' ? rd.value : undefined;\n })", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "indexKey": "", + "optional": false, + "description": "

See DashboardPage's profile for why this narrows via a computed instead of let-.

\n", + "line": 70, + "rawdescription": "\nSee DashboardPage's `profile` for why this narrows via a computed instead of `let-`.", + "modifierKind": [ + 124, + 148 + ] + }, { "name": "find", "defaultValue": "() => {...}", @@ -11604,7 +11677,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 64, + "line": 66, "modifierKind": [ 124 ] @@ -11618,7 +11691,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 62, + "line": 64, "modifierKind": [ 123 ] @@ -11632,7 +11705,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 65, + "line": 67, "modifierKind": [ 124 ] @@ -11646,7 +11719,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 61, + "line": 63, "modifierKind": [ 124 ] @@ -11686,7 +11759,7 @@ "description": "

Page: a single aanvraag ("case"). Stub — it renders the aanvraag's known fields\n(soort, waarvoor, status, referentie, ingediend) in a CIBG Datablock; full case\nhandling is future work. The dashboard "Mijn aanvragen" rows link here.

\n", "rawdescription": "\nPage: a single aanvraag (\"case\"). Stub — it renders the aanvraag's known fields\n(soort, waarvoor, status, referentie, ingediend) in a CIBG Datablock; full case\nhandling is future work. The dashboard \"Mijn aanvragen\" rows link here.", "type": "component", - "sourceCode": "import { Component, inject } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { DataBlockComponent } from '@shared/ui/data-block/data-block.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { ApplicationsStore } from '@registratie/application/applications.store';\nimport { Aanvraag } from '@registratie/domain/aanvraag';\nimport { detailRows } from '@registratie/domain/aanvraag-view';\n\n/** Page: a single aanvraag (\"case\"). Stub — it renders the aanvraag's known fields\n (soort, waarvoor, status, referentie, ingediend) in a CIBG Datablock; full case\n handling is future work. The dashboard \"Mijn aanvragen\" rows link here. */\n@Component({\n selector: 'app-aanvraag-detail-page',\n imports: [\n PageShellComponent,\n SkeletonComponent,\n AlertComponent,\n DataBlockComponent,\n DataRowComponent,\n ...ASYNC,\n ],\n template: `\n \n \n \n @let a = find($any(list));\n @if (a) {\n \n @for (row of rows(a); track row.key) {\n
\n }\n \n \n De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.\n \n } @else {\n Deze aanvraag is niet gevonden.\n }\n
\n \n \n \n
\n \n `,\n})\nexport class AanvraagDetailPage {\n protected store = inject(ApplicationsStore);\n private id = inject(ActivatedRoute).snapshot.paramMap.get('id') ?? '';\n\n protected find = (list: Aanvraag[]): Aanvraag | undefined => list.find((a) => a.id === this.id);\n protected rows = detailRows;\n}\n", + "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { DataBlockComponent } from '@shared/ui/data-block/data-block.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { ApplicationsStore } from '@registratie/application/applications.store';\nimport { Aanvraag } from '@registratie/domain/aanvraag';\nimport { detailRows } from '@registratie/domain/aanvraag-view';\n\n/** Page: a single aanvraag (\"case\"). Stub — it renders the aanvraag's known fields\n (soort, waarvoor, status, referentie, ingediend) in a CIBG Datablock; full case\n handling is future work. The dashboard \"Mijn aanvragen\" rows link here. */\n@Component({\n selector: 'app-aanvraag-detail-page',\n imports: [\n PageShellComponent,\n SkeletonComponent,\n AlertComponent,\n DataBlockComponent,\n DataRowComponent,\n ...ASYNC,\n ],\n template: `\n \n \n \n @if (applications(); as list) {\n @let a = find(list);\n @if (a) {\n \n @for (row of rows(a); track row.key) {\n
\n }\n \n \n De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.\n \n } @else {\n Deze aanvraag is niet gevonden.\n }\n }\n
\n \n \n \n
\n \n `,\n})\nexport class AanvraagDetailPage {\n protected store = inject(ApplicationsStore);\n private id = inject(ActivatedRoute).snapshot.paramMap.get('id') ?? '';\n\n protected find = (list: Aanvraag[]): Aanvraag | undefined => list.find((a) => a.id === this.id);\n protected rows = detailRows;\n\n /** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */\n protected readonly applications = computed(() => {\n const rd = this.store.applications();\n return rd.tag === 'Success' ? rd.value : undefined;\n });\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -12104,7 +12177,7 @@ }, { "name": "AsyncComponent", - "id": "component-AsyncComponent-8cd95468f2e83b9befe7f37bc6281e635371d590157ae555f51eec4cc9a354ec43e6fbe08d7600ecffbf30ddc2a8631783a98f02fd56547a461aab44ec507259", + "id": "component-AsyncComponent-8829fd8ba48f54d52dc1e5e88de58ac991b4ce5057e5c9e5c3406750af3ead689455704d8c75dec9c59329c59ad11c4dc2ce2b03381bf0a2062107d6ebfc18f3", "file": "src/app/shared/ui/async/async.component.ts", "encapsulation": [], "entryComponents": [], @@ -12127,7 +12200,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 82, + "line": 92, "required": false }, { @@ -12138,7 +12211,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 89, + "line": 99, "required": false }, { @@ -12149,7 +12222,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 87, + "line": 97, "required": false }, { @@ -12161,7 +12234,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 83, + "line": 93, "required": false }, { @@ -12172,7 +12245,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 81, + "line": 91, "required": false }, { @@ -12183,7 +12256,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 88, + "line": 98, "required": false } ], @@ -12198,7 +12271,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 93 + "line": 103 }, { "name": "error", @@ -12209,7 +12282,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 114, + "line": 124, "modifierKind": [ 124 ] @@ -12223,18 +12296,18 @@ "indexKey": "", "optional": false, "description": "", - "line": 94 + "line": 104 }, { "name": "loadedTpl", - "defaultValue": "contentChild.required(AsyncLoadedDirective)", + "defaultValue": "contentChild.required>(AsyncLoadedDirective)", "deprecated": false, "deprecationMessage": "", "type": "unknown", "indexKey": "", "optional": false, "description": "", - "line": 91 + "line": 101 }, { "name": "loadingTpl", @@ -12245,7 +12318,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 92 + "line": 102 }, { "name": "rd", @@ -12256,7 +12329,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 97, + "line": 107, "modifierKind": [ 124 ] @@ -12270,7 +12343,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 123 + "line": 133 }, { "name": "value", @@ -12281,7 +12354,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 106, + "line": 116, "modifierKind": [ 124 ] @@ -12313,7 +12386,7 @@ "description": "

Renders exactly ONE of loading / empty / error / loaded for a signal-based\nresource (e.g. httpResource). Built on a RemoteData tagged union (see\ncore/remote-data.ts), so the states are mutually exclusive by construction —\nthe UI can never show two at once ("impossible states"). Unprovided slots\nfall back to sensible defaults.

\n", "rawdescription": "\n\nRenders exactly ONE of loading / empty / error / loaded for a signal-based\nresource (e.g. httpResource). Built on a RemoteData tagged union (see\ncore/remote-data.ts), so the states are mutually exclusive by construction —\nthe UI can never show two at once (\"impossible states\"). Unprovided slots\nfall back to sensible defaults.\n", "type": "component", - "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: unknown }>) {}\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", + "sourceCode": "import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport type { Resource } from '@angular/core';\nimport { SpinnerComponent } from '@shared/ui/spinner/spinner.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';\n\n/* Slot markers. Put on children of . Generic so the\n $implicit context is typed as the resource's T instead of unknown — see\n AsyncComponent's contentChild> below, which threads the\n host's own T through the query result type. */\n@Directive({ selector: '[appAsyncLoaded]' })\nexport class AsyncLoadedDirective {\n constructor(public tpl: TemplateRef<{ $implicit: T }>) {}\n\n static ngTemplateContextGuard(\n _dir: AsyncLoadedDirective,\n _ctx: unknown,\n ): _ctx is { $implicit: T } {\n return true;\n }\n}\n@Directive({ selector: '[appAsyncLoading]' })\nexport class AsyncLoadingDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncEmpty]' })\nexport class AsyncEmptyDirective {\n constructor(public tpl: TemplateRef) {}\n}\n@Directive({ selector: '[appAsyncError]' })\nexport class AsyncErrorDirective {\n constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}\n}\n\n/**\n * Renders exactly ONE of loading / empty / error / loaded for a signal-based\n * resource (e.g. httpResource). Built on a RemoteData tagged union (see\n * core/remote-data.ts), so the states are mutually exclusive by construction —\n * the UI can never show two at once (\"impossible states\"). Unprovided slots\n * fall back to sensible defaults.\n */\n@Component({\n selector: 'app-async',\n imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],\n template: `\n
\n @switch (rd().tag) {\n @case ('Loading') {\n @if (loadingTpl()) {\n \n } @else {\n \n }\n }\n @case ('Failure') {\n @if (errorTpl()) {\n \n } @else {\n {{ errorText() }}\n
\n {{ retryText() }}\n
\n }\n }\n @case ('Empty') {\n @if (emptyTpl()) {\n \n } @else {\n

{{ emptyText() }}

\n }\n }\n @case ('Success') {\n \n }\n }\n
\n `,\n})\nexport class AsyncComponent {\n // Two ways to feed this component:\n // [resource] — a raw httpResource (the common case), or\n // [data] — an already-combined RemoteData (e.g. from a store via map2).\n resource = input>();\n data = input>();\n isEmpty = input<(v: T) => boolean>(() => false);\n\n // Shared/English component: copy lives behind language-agnostic inputs. Defaults are\n // localizable via $localize (source = default locale, currently nl); callers may override.\n errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);\n retryText = input($localize`:@@async.retry:Opnieuw proberen`);\n emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);\n\n loadedTpl = contentChild.required>(AsyncLoadedDirective);\n loadingTpl = contentChild(AsyncLoadingDirective);\n emptyTpl = contentChild(AsyncEmptyDirective);\n errorTpl = contentChild(AsyncErrorDirective);\n\n // Single source of truth: the supplied RemoteData, or the resource projected into one.\n protected rd = computed>(() => {\n const data = this.data();\n if (data) return data;\n const r = this.resource();\n return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };\n });\n\n // value/error are pulled out via the exhaustive fold — only Success carries a\n // value, only Failure carries an error, so these can't lie.\n protected value = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: () => undefined,\n success: (v) => v,\n }),\n );\n protected error = computed(() =>\n foldRemote(this.rd(), {\n loading: () => undefined,\n empty: () => undefined,\n failure: (e) => e,\n success: () => undefined,\n }),\n );\n\n retry = () => {\n const r = this.resource();\n if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {\n (r as { reload: () => void }).reload();\n }\n };\n}\n\n/** Convenience: import this array to get the wrapper + all slot directives. */\nexport const ASYNC = [\n AsyncComponent,\n AsyncLoadedDirective,\n AsyncLoadingDirective,\n AsyncEmptyDirective,\n AsyncErrorDirective,\n] as const;\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -13555,7 +13628,7 @@ }, { "name": "ConceptsPage", - "id": "component-ConceptsPage-d1f7c75c5ffb3cbb001bfac6f7a245a828a312fd9820f07c95286e6900eaa1adfdd9a13a8090bb5e0ce3221dff245491a17d6484ee95e42dd7de4437a437b900", + "id": "component-ConceptsPage-2d18e2bfd9060066bcf38d1b5223659475b1c5505bb537bfbd754c454d5bf5aafbd38917d4e2bb773d19a8d136d2aa595a637c403501ce7bddf5608ca5b73db0", "file": "src/app/showcase/concepts.page.ts", "encapsulation": [], "entryComponents": [], @@ -13567,7 +13640,7 @@ "styles": [ "\n .section {\n margin: 0 0 3rem;\n }\n .lead {\n color: var(--rhc-color-grijs-700);\n max-width: 46rem;\n margin: 0.25rem 0 1.25rem;\n }\n .cols {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));\n gap: 1.5rem;\n align-items: start;\n }\n .card {\n border: 1px solid var(--rhc-color-grijs-200, #e5e5e5);\n border-radius: 10px;\n padding: 1.25rem;\n background: #fff;\n }\n .card--bad {\n border-color: var(--rhc-color-rood-300, #f0b4b4);\n }\n .card--good {\n border-color: var(--rhc-color-groen-300, #b4e0b4);\n }\n .tag {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n font-weight: 700;\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0 0 0.75rem;\n }\n .tag::before {\n content: '';\n width: 0.6rem;\n height: 0.6rem;\n border-radius: 50%;\n }\n .tag.bad {\n color: var(--rhc-color-rood-600, #a30000);\n }\n .tag.bad::before {\n background: var(--rhc-color-rood-500, #d52b1e);\n }\n .tag.good {\n color: var(--rhc-color-groen-700, #277337);\n }\n .tag.good::before {\n background: var(--rhc-color-groen-500, #39870c);\n }\n .tag.plain {\n color: var(--rhc-color-grijs-700);\n }\n .tag.plain::before {\n display: none;\n }\n pre {\n background: #1e2430;\n color: #e6e9ef;\n padding: 1rem;\n border-radius: 8px;\n overflow: auto;\n font-size: 0.82rem;\n line-height: 1.55;\n margin: 0;\n }\n pre .k {\n color: #c792ea;\n }\n pre .s {\n color: #c3e88d;\n }\n pre .c {\n color: #7e8aa0;\n font-style: italic;\n }\n .note {\n font-size: 0.9rem;\n color: var(--rhc-color-grijs-700);\n margin: 0.75rem 0 0;\n }\n /* live state diagram */\n .machine {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin: 0 0 1rem;\n }\n .node {\n padding: 0.4rem 0.8rem;\n border-radius: 999px;\n border: 1px solid var(--rhc-color-grijs-300, #ccc);\n font-size: 0.82rem;\n color: var(--rhc-color-grijs-700);\n transition: all 0.15s;\n }\n .node.on {\n background: var(--rhc-color-hemelblauw-100, #e5f1fb);\n border-color: var(--rhc-color-hemelblauw-500, #007bc7);\n color: var(--rhc-color-hemelblauw-700, #00567d);\n font-weight: 700;\n }\n .steplist {\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem;\n align-items: center;\n margin: 0 0 1rem;\n }\n .pill {\n padding: 0.3rem 0.7rem;\n border-radius: 8px;\n background: var(--rhc-color-grijs-100, #f3f3f3);\n font-size: 0.8rem;\n }\n .pill.extra {\n background: var(--rhc-color-geel-100, #fff6d6);\n border: 1px dashed var(--rhc-color-geel-600, #c79a00);\n }\n .arrow {\n color: var(--rhc-color-grijs-400, #999);\n }\n " ], - "template": "\n

\n Vijf functionele patronen die atomic design makkelijker maakt om te tonen — telkens \"fout\"\n (de oude vorm liet het toe) naast\"goed\" (het type maakt het onmogelijk).\n

\n\n \n
\n 1 · Discriminated unions\n

Laat elke variant precies de gegevens dragen die kloppen — niets meer.

\n
\n
\n

Fout — vlakke interface

\n
\n        

\n Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand.\n

\n
\n
\n

Goed — sum type

\n \n

\n De variant Doorgehaald kent geen herregistratiedatum, dus de rij bestaat\n simpelweg niet.\n

\n
\n
\n
\n\n \n
\n 2 · RemoteData fold\n

\n Eén waarde met vier elkaar uitsluitende toestanden in plaats van drie losse booleans.\n

\n
\n
\n

Vier toestanden, één molecuul

\n

Loading

\n {{ v }}\n

Empty

\n {{ v }}\n

Failure

\n {{ v }}\n

Success

\n
    \n @for (i of v; track i) {\n
  • {{ i }}
  • \n }\n
\n
\n
\n

De exhaustieve fold

\n
\n        

\n Een nieuwe variant toevoegen breekt de compile via assertNever tot je hem\n afhandelt.\n

\n
\n
\n
\n\n \n
\n 3 · Parse, don't validate\n

Na het parsen onthoudt het type dat de waarde geldig is.

\n
\n
\n

Smart constructor → Result

\n \n
\n
\n @if (parsed().ok) {\n

ok

\n
Postcode =\"{{ $any(parsed()).value }}\"
\n

\n Een gevalideerde Postcode is een ander type dan een ruwe string.\n

\n } @else {\n

err

\n
{{ $any(parsed()).error }}
\n }\n
\n
\n
\n\n \n
\n 4 · Form als state machine\n

\n Eén tagged union stuurt de UI. Speel met de wizard — de gemarkeerde toestand is de\n huidige.\n

\n
\n
\n

Fout — losse booleans

\n
\n        

\n Niets verhindert\"submitting\" mét validatiefouten of een successcherm met errors.\n

\n
\n
\n

Goed — één tagged union

\n
\n @for (n of ['Editing', 'Submitting', 'Submitted', 'Failed']; track n) {\n {{ n }}\n }\n
\n \n
\n
\n
\n\n \n
\n 5 · Vragenlijst met vaste stappen —\"vragen tonen, niet stappen toevoegen\"\n

\n Het aantal stappen ligt vast (STEPS); vervolgvragen verschijnen\n binnen een stap op basis van eerdere antwoorden. Antwoord\"ja\" op buitenland of\n vul weinig uren in, en er komt een extra vraag bij in dezelfde stap — de voortgang\"van N\"\n blijft gelijk.\n

\n
\n
\n

Vaste stappen

\n
\n @for (s of iw.steps; track s; let last = $last) {\n {{ s }}\n @if (!last) {\n \n }\n }\n
\n

\n De stappen zijn altijd dezelfde; alleen de vragen binnen een stap verschijnen\n of verdwijnen.\n

\n
\n
\n

De wizard

\n \n
\n
\n
\n
\n", + "template": "\n

\n Vijf functionele patronen die atomic design makkelijker maakt om te tonen — telkens \"fout\"\n (de oude vorm liet het toe) naast\"goed\" (het type maakt het onmogelijk).\n

\n\n \n
\n 1 · Discriminated unions\n

Laat elke variant precies de gegevens dragen die kloppen — niets meer.

\n
\n
\n

Fout — vlakke interface

\n
\n        

\n Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand.\n

\n
\n
\n

Goed — sum type

\n \n

\n De variant Doorgehaald kent geen herregistratiedatum, dus de rij bestaat\n simpelweg niet.\n

\n
\n
\n
\n\n \n
\n 2 · RemoteData fold\n

\n Eén waarde met vier elkaar uitsluitende toestanden in plaats van drie losse booleans.\n

\n
\n
\n

Vier toestanden, één molecuul

\n

Loading

\n {{ v }}\n

Empty

\n {{ v }}\n

Failure

\n {{ v }}\n

Success

\n
    \n @for (i of successRes.value(); track i) {\n
  • {{ i }}
  • \n }\n
\n
\n
\n

De exhaustieve fold

\n
\n        

\n Een nieuwe variant toevoegen breekt de compile via assertNever tot je hem\n afhandelt.\n

\n
\n
\n
\n\n \n
\n 3 · Parse, don't validate\n

Na het parsen onthoudt het type dat de waarde geldig is.

\n
\n
\n

Smart constructor → Result

\n \n
\n @let r = parsed();\n
\n @if (r.ok) {\n

ok

\n
Postcode =\"{{ r.value }}\"
\n

\n Een gevalideerde Postcode is een ander type dan een ruwe string.\n

\n } @else {\n

err

\n
{{ r.error }}
\n }\n
\n
\n
\n\n \n
\n 4 · Form als state machine\n

\n Eén tagged union stuurt de UI. Speel met de wizard — de gemarkeerde toestand is de\n huidige.\n

\n
\n
\n

Fout — losse booleans

\n
\n        

\n Niets verhindert\"submitting\" mét validatiefouten of een successcherm met errors.\n

\n
\n
\n

Goed — één tagged union

\n
\n @for (n of ['Editing', 'Submitting', 'Submitted', 'Failed']; track n) {\n {{ n }}\n }\n
\n \n
\n
\n
\n\n \n
\n 5 · Vragenlijst met vaste stappen —\"vragen tonen, niet stappen toevoegen\"\n

\n Het aantal stappen ligt vast (STEPS); vervolgvragen verschijnen\n binnen een stap op basis van eerdere antwoorden. Antwoord\"ja\" op buitenland of\n vul weinig uren in, en er komt een extra vraag bij in dezelfde stap — de voortgang\"van N\"\n blijft gelijk.\n

\n
\n
\n

Vaste stappen

\n
\n @for (s of iw.steps; track s; let last = $last) {\n {{ s }}\n @if (!last) {\n \n }\n }\n
\n

\n De stappen zijn altijd dezelfde; alleen de vragen binnen een stap verschijnen\n of verdwijnen.\n

\n
\n
\n

De wizard

\n \n
\n
\n
\n
\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], @@ -13583,7 +13656,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 342 + "line": 343 }, { "name": "emptyRes", @@ -13594,7 +13667,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 352 + "line": 353 }, { "name": "errorRes", @@ -13605,7 +13678,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 353 + "line": 354 }, { "name": "foldCode", @@ -13616,7 +13689,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 364 + "line": 365 }, { "name": "isEmpty", @@ -13627,7 +13700,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 340 + "line": 341 }, { "name": "loadingRes", @@ -13638,7 +13711,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 351 + "line": 352 }, { "name": "machineBad", @@ -13649,7 +13722,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 371 + "line": 372 }, { "name": "parsed", @@ -13660,7 +13733,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 357 + "line": 358 }, { "name": "raw", @@ -13671,7 +13744,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 356 + "line": 357 }, { "name": "successRes", @@ -13682,7 +13755,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 354 + "line": 355 }, { "name": "unionBad", @@ -13693,7 +13766,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 359 + "line": 360 } ], "methodsClass": [], @@ -13742,7 +13815,7 @@ "description": "

Teaching showcase: each section pairs the impossible-state-permitting"before"\nwith the"after" where the type system rules it out. Composition-only.

\n", "rawdescription": "\nTeaching showcase: each section pairs the impossible-state-permitting\"before\"\nwith the\"after\" where the type system rules it out. Composition-only.", "type": "component", - "sourceCode": "import { Component, computed, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport type { Resource } from '@angular/core';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';\nimport { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component';\nimport { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';\nimport { Registration } from '@registratie/domain/registration';\nimport { parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** Minimal fake Resource so can be driven through every state without HTTP. */\nfunction fakeResource(status: string, value?: T, error?: Error): Resource {\n return {\n value: () => value as T,\n status: () => status,\n error: () => error,\n hasValue: () => value !== undefined,\n reload: () => {},\n } as unknown as Resource;\n}\n\n/** Teaching showcase: each section pairs the impossible-state-permitting\"before\"\n with the\"after\" where the type system rules it out. Composition-only. */\n@Component({\n selector: 'app-concepts-page',\n imports: [\n FormsModule,\n PageShellComponent,\n HeadingComponent,\n TextInputComponent,\n ...ASYNC,\n SkeletonComponent,\n RegistrationSummaryComponent,\n HerregistratieWizardComponent,\n IntakeWizardComponent,\n ],\n styles: [\n `\n .section {\n margin: 0 0 3rem;\n }\n .lead {\n color: var(--rhc-color-grijs-700);\n max-width: 46rem;\n margin: 0.25rem 0 1.25rem;\n }\n .cols {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));\n gap: 1.5rem;\n align-items: start;\n }\n .card {\n border: 1px solid var(--rhc-color-grijs-200, #e5e5e5);\n border-radius: 10px;\n padding: 1.25rem;\n background: #fff;\n }\n .card--bad {\n border-color: var(--rhc-color-rood-300, #f0b4b4);\n }\n .card--good {\n border-color: var(--rhc-color-groen-300, #b4e0b4);\n }\n .tag {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n font-weight: 700;\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0 0 0.75rem;\n }\n .tag::before {\n content: '';\n width: 0.6rem;\n height: 0.6rem;\n border-radius: 50%;\n }\n .tag.bad {\n color: var(--rhc-color-rood-600, #a30000);\n }\n .tag.bad::before {\n background: var(--rhc-color-rood-500, #d52b1e);\n }\n .tag.good {\n color: var(--rhc-color-groen-700, #277337);\n }\n .tag.good::before {\n background: var(--rhc-color-groen-500, #39870c);\n }\n .tag.plain {\n color: var(--rhc-color-grijs-700);\n }\n .tag.plain::before {\n display: none;\n }\n pre {\n background: #1e2430;\n color: #e6e9ef;\n padding: 1rem;\n border-radius: 8px;\n overflow: auto;\n font-size: 0.82rem;\n line-height: 1.55;\n margin: 0;\n }\n pre .k {\n color: #c792ea;\n }\n pre .s {\n color: #c3e88d;\n }\n pre .c {\n color: #7e8aa0;\n font-style: italic;\n }\n .note {\n font-size: 0.9rem;\n color: var(--rhc-color-grijs-700);\n margin: 0.75rem 0 0;\n }\n /* live state diagram */\n .machine {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin: 0 0 1rem;\n }\n .node {\n padding: 0.4rem 0.8rem;\n border-radius: 999px;\n border: 1px solid var(--rhc-color-grijs-300, #ccc);\n font-size: 0.82rem;\n color: var(--rhc-color-grijs-700);\n transition: all 0.15s;\n }\n .node.on {\n background: var(--rhc-color-hemelblauw-100, #e5f1fb);\n border-color: var(--rhc-color-hemelblauw-500, #007bc7);\n color: var(--rhc-color-hemelblauw-700, #00567d);\n font-weight: 700;\n }\n .steplist {\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem;\n align-items: center;\n margin: 0 0 1rem;\n }\n .pill {\n padding: 0.3rem 0.7rem;\n border-radius: 8px;\n background: var(--rhc-color-grijs-100, #f3f3f3);\n font-size: 0.8rem;\n }\n .pill.extra {\n background: var(--rhc-color-geel-100, #fff6d6);\n border: 1px dashed var(--rhc-color-geel-600, #c79a00);\n }\n .arrow {\n color: var(--rhc-color-grijs-400, #999);\n }\n `,\n ],\n template: `\n \n

\n Vijf functionele patronen die atomic design makkelijker maakt om te tonen — telkens \"fout\"\n (de oude vorm liet het toe) naast\"goed\" (het type maakt het onmogelijk).\n

\n\n \n
\n 1 · Discriminated unions\n

Laat elke variant precies de gegevens dragen die kloppen — niets meer.

\n
\n
\n

Fout — vlakke interface

\n
\n            

\n Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand.\n

\n
\n
\n

Goed — sum type

\n \n

\n De variant Doorgehaald kent geen herregistratiedatum, dus de rij bestaat\n simpelweg niet.\n

\n
\n
\n
\n\n \n
\n 2 · RemoteData fold\n

\n Eén waarde met vier elkaar uitsluitende toestanden in plaats van drie losse booleans.\n

\n
\n
\n

Vier toestanden, één molecuul

\n

Loading

\n {{ v }}\n

Empty

\n {{ v }}\n

Failure

\n {{ v }}\n

Success

\n
    \n @for (i of v; track i) {\n
  • {{ i }}
  • \n }\n
\n
\n
\n

De exhaustieve fold

\n
\n            

\n Een nieuwe variant toevoegen breekt de compile via assertNever tot je hem\n afhandelt.\n

\n
\n
\n
\n\n \n
\n 3 · Parse, don't validate\n

Na het parsen onthoudt het type dat de waarde geldig is.

\n
\n
\n

Smart constructor → Result

\n \n
\n
\n @if (parsed().ok) {\n

ok

\n
Postcode =\"{{ $any(parsed()).value }}\"
\n

\n Een gevalideerde Postcode is een ander type dan een ruwe string.\n

\n } @else {\n

err

\n
{{ $any(parsed()).error }}
\n }\n
\n
\n
\n\n \n
\n 4 · Form als state machine\n

\n Eén tagged union stuurt de UI. Speel met de wizard — de gemarkeerde toestand is de\n huidige.\n

\n
\n
\n

Fout — losse booleans

\n
\n            

\n Niets verhindert\"submitting\" mét validatiefouten of een successcherm met errors.\n

\n
\n
\n

Goed — één tagged union

\n
\n @for (n of ['Editing', 'Submitting', 'Submitted', 'Failed']; track n) {\n {{ n }}\n }\n
\n \n
\n
\n
\n\n \n
\n 5 · Vragenlijst met vaste stappen —\"vragen tonen, niet stappen toevoegen\"\n

\n Het aantal stappen ligt vast (STEPS); vervolgvragen verschijnen\n binnen een stap op basis van eerdere antwoorden. Antwoord\"ja\" op buitenland of\n vul weinig uren in, en er komt een extra vraag bij in dezelfde stap — de voortgang\"van N\"\n blijft gelijk.\n

\n
\n
\n

Vaste stappen

\n
\n @for (s of iw.steps; track s; let last = $last) {\n {{ s }}\n @if (!last) {\n \n }\n }\n
\n

\n De stappen zijn altijd dezelfde; alleen de vragen binnen een stap verschijnen\n of verdwijnen.\n

\n
\n
\n

De wizard

\n \n
\n
\n
\n
\n `,\n})\nexport class ConceptsPage {\n isEmpty = (v: string[]) => !v || v.length === 0;\n\n doorgehaald: Registration = {\n bigNummer: '19012345601',\n naam: 'Dr. A. (Anna) de Vries',\n beroep: 'Arts',\n registratiedatum: '2012-09-01',\n geboortedatum: '1985-03-14',\n status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' },\n };\n\n loadingRes = fakeResource('loading');\n emptyRes = fakeResource('resolved', []);\n errorRes = fakeResource('error', undefined, new Error('Demo'));\n successRes = fakeResource('resolved', ['Huisartsgeneeskunde', 'Spoedeisende hulp']);\n\n raw = signal('');\n parsed = computed(() => parsePostcode(this.raw()));\n\n unionBad = `interface Registration {\n status: 'Geregistreerd' | 'Doorgehaald';\n herregistratieDatum: string; // altijd aanwezig 😬\n}`;\n\n foldCode = `foldRemote(rd, {\n loading: () => spinner,\n empty: () => 'geen data',\n failure: (e) => alert(e),\n success: (v) => render(v),\n}); // mist er één → compile-fout`;\n\n machineBad = `submitting = signal(false);\nsubmitted = signal(false);\nerrors = signal<...>({});\n// submitting === true && errors.size > 0 ? 🤷`;\n}\n", + "sourceCode": "import { Component, computed, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport type { Resource } from '@angular/core';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';\nimport { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component';\nimport { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';\nimport { Registration } from '@registratie/domain/registration';\nimport { parsePostcode } from '@registratie/domain/value-objects/postcode';\n\n/** Minimal fake Resource so can be driven through every state without HTTP. */\nfunction fakeResource(status: string, value?: T, error?: Error): Resource {\n return {\n value: () => value as T,\n status: () => status,\n error: () => error,\n hasValue: () => value !== undefined,\n reload: () => {},\n } as unknown as Resource;\n}\n\n/** Teaching showcase: each section pairs the impossible-state-permitting\"before\"\n with the\"after\" where the type system rules it out. Composition-only. */\n@Component({\n selector: 'app-concepts-page',\n imports: [\n FormsModule,\n PageShellComponent,\n HeadingComponent,\n TextInputComponent,\n ...ASYNC,\n SkeletonComponent,\n RegistrationSummaryComponent,\n HerregistratieWizardComponent,\n IntakeWizardComponent,\n ],\n styles: [\n `\n .section {\n margin: 0 0 3rem;\n }\n .lead {\n color: var(--rhc-color-grijs-700);\n max-width: 46rem;\n margin: 0.25rem 0 1.25rem;\n }\n .cols {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));\n gap: 1.5rem;\n align-items: start;\n }\n .card {\n border: 1px solid var(--rhc-color-grijs-200, #e5e5e5);\n border-radius: 10px;\n padding: 1.25rem;\n background: #fff;\n }\n .card--bad {\n border-color: var(--rhc-color-rood-300, #f0b4b4);\n }\n .card--good {\n border-color: var(--rhc-color-groen-300, #b4e0b4);\n }\n .tag {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n font-weight: 700;\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0 0 0.75rem;\n }\n .tag::before {\n content: '';\n width: 0.6rem;\n height: 0.6rem;\n border-radius: 50%;\n }\n .tag.bad {\n color: var(--rhc-color-rood-600, #a30000);\n }\n .tag.bad::before {\n background: var(--rhc-color-rood-500, #d52b1e);\n }\n .tag.good {\n color: var(--rhc-color-groen-700, #277337);\n }\n .tag.good::before {\n background: var(--rhc-color-groen-500, #39870c);\n }\n .tag.plain {\n color: var(--rhc-color-grijs-700);\n }\n .tag.plain::before {\n display: none;\n }\n pre {\n background: #1e2430;\n color: #e6e9ef;\n padding: 1rem;\n border-radius: 8px;\n overflow: auto;\n font-size: 0.82rem;\n line-height: 1.55;\n margin: 0;\n }\n pre .k {\n color: #c792ea;\n }\n pre .s {\n color: #c3e88d;\n }\n pre .c {\n color: #7e8aa0;\n font-style: italic;\n }\n .note {\n font-size: 0.9rem;\n color: var(--rhc-color-grijs-700);\n margin: 0.75rem 0 0;\n }\n /* live state diagram */\n .machine {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin: 0 0 1rem;\n }\n .node {\n padding: 0.4rem 0.8rem;\n border-radius: 999px;\n border: 1px solid var(--rhc-color-grijs-300, #ccc);\n font-size: 0.82rem;\n color: var(--rhc-color-grijs-700);\n transition: all 0.15s;\n }\n .node.on {\n background: var(--rhc-color-hemelblauw-100, #e5f1fb);\n border-color: var(--rhc-color-hemelblauw-500, #007bc7);\n color: var(--rhc-color-hemelblauw-700, #00567d);\n font-weight: 700;\n }\n .steplist {\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem;\n align-items: center;\n margin: 0 0 1rem;\n }\n .pill {\n padding: 0.3rem 0.7rem;\n border-radius: 8px;\n background: var(--rhc-color-grijs-100, #f3f3f3);\n font-size: 0.8rem;\n }\n .pill.extra {\n background: var(--rhc-color-geel-100, #fff6d6);\n border: 1px dashed var(--rhc-color-geel-600, #c79a00);\n }\n .arrow {\n color: var(--rhc-color-grijs-400, #999);\n }\n `,\n ],\n template: `\n \n

\n Vijf functionele patronen die atomic design makkelijker maakt om te tonen — telkens \"fout\"\n (de oude vorm liet het toe) naast\"goed\" (het type maakt het onmogelijk).\n

\n\n \n
\n 1 · Discriminated unions\n

Laat elke variant precies de gegevens dragen die kloppen — niets meer.

\n
\n
\n

Fout — vlakke interface

\n
\n            

\n Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand.\n

\n
\n
\n

Goed — sum type

\n \n

\n De variant Doorgehaald kent geen herregistratiedatum, dus de rij bestaat\n simpelweg niet.\n

\n
\n
\n
\n\n \n
\n 2 · RemoteData fold\n

\n Eén waarde met vier elkaar uitsluitende toestanden in plaats van drie losse booleans.\n

\n
\n
\n

Vier toestanden, één molecuul

\n

Loading

\n {{ v }}\n

Empty

\n {{ v }}\n

Failure

\n {{ v }}\n

Success

\n
    \n @for (i of successRes.value(); track i) {\n
  • {{ i }}
  • \n }\n
\n
\n
\n

De exhaustieve fold

\n
\n            

\n Een nieuwe variant toevoegen breekt de compile via assertNever tot je hem\n afhandelt.\n

\n
\n
\n
\n\n \n
\n 3 · Parse, don't validate\n

Na het parsen onthoudt het type dat de waarde geldig is.

\n
\n
\n

Smart constructor → Result

\n \n
\n @let r = parsed();\n
\n @if (r.ok) {\n

ok

\n
Postcode =\"{{ r.value }}\"
\n

\n Een gevalideerde Postcode is een ander type dan een ruwe string.\n

\n } @else {\n

err

\n
{{ r.error }}
\n }\n
\n
\n
\n\n \n
\n 4 · Form als state machine\n

\n Eén tagged union stuurt de UI. Speel met de wizard — de gemarkeerde toestand is de\n huidige.\n

\n
\n
\n

Fout — losse booleans

\n
\n            

\n Niets verhindert\"submitting\" mét validatiefouten of een successcherm met errors.\n

\n
\n
\n

Goed — één tagged union

\n
\n @for (n of ['Editing', 'Submitting', 'Submitted', 'Failed']; track n) {\n {{ n }}\n }\n
\n \n
\n
\n
\n\n \n
\n 5 · Vragenlijst met vaste stappen —\"vragen tonen, niet stappen toevoegen\"\n

\n Het aantal stappen ligt vast (STEPS); vervolgvragen verschijnen\n binnen een stap op basis van eerdere antwoorden. Antwoord\"ja\" op buitenland of\n vul weinig uren in, en er komt een extra vraag bij in dezelfde stap — de voortgang\"van N\"\n blijft gelijk.\n

\n
\n
\n

Vaste stappen

\n
\n @for (s of iw.steps; track s; let last = $last) {\n {{ s }}\n @if (!last) {\n \n }\n }\n
\n

\n De stappen zijn altijd dezelfde; alleen de vragen binnen een stap verschijnen\n of verdwijnen.\n

\n
\n
\n

De wizard

\n \n
\n
\n
\n
\n `,\n})\nexport class ConceptsPage {\n isEmpty = (v: string[]) => !v || v.length === 0;\n\n doorgehaald: Registration = {\n bigNummer: '19012345601',\n naam: 'Dr. A. (Anna) de Vries',\n beroep: 'Arts',\n registratiedatum: '2012-09-01',\n geboortedatum: '1985-03-14',\n status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' },\n };\n\n loadingRes = fakeResource('loading');\n emptyRes = fakeResource('resolved', []);\n errorRes = fakeResource('error', undefined, new Error('Demo'));\n successRes = fakeResource('resolved', ['Huisartsgeneeskunde', 'Spoedeisende hulp']);\n\n raw = signal('');\n parsed = computed(() => parsePostcode(this.raw()));\n\n unionBad = `interface Registration {\n status: 'Geregistreerd' | 'Doorgehaald';\n herregistratieDatum: string; // altijd aanwezig 😬\n}`;\n\n foldCode = `foldRemote(rd, {\n loading: () => spinner,\n empty: () => 'geen data',\n failure: (e) => alert(e),\n success: (v) => render(v),\n}); // mist er één → compile-fout`;\n\n machineBad = `submitting = signal(false);\nsubmitted = signal(false);\nerrors = signal<...>({});\n// submitting === true && errors.size > 0 ? 🤷`;\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "\n .section {\n margin: 0 0 3rem;\n }\n .lead {\n color: var(--rhc-color-grijs-700);\n max-width: 46rem;\n margin: 0.25rem 0 1.25rem;\n }\n .cols {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));\n gap: 1.5rem;\n align-items: start;\n }\n .card {\n border: 1px solid var(--rhc-color-grijs-200, #e5e5e5);\n border-radius: 10px;\n padding: 1.25rem;\n background: #fff;\n }\n .card--bad {\n border-color: var(--rhc-color-rood-300, #f0b4b4);\n }\n .card--good {\n border-color: var(--rhc-color-groen-300, #b4e0b4);\n }\n .tag {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n font-weight: 700;\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin: 0 0 0.75rem;\n }\n .tag::before {\n content: '';\n width: 0.6rem;\n height: 0.6rem;\n border-radius: 50%;\n }\n .tag.bad {\n color: var(--rhc-color-rood-600, #a30000);\n }\n .tag.bad::before {\n background: var(--rhc-color-rood-500, #d52b1e);\n }\n .tag.good {\n color: var(--rhc-color-groen-700, #277337);\n }\n .tag.good::before {\n background: var(--rhc-color-groen-500, #39870c);\n }\n .tag.plain {\n color: var(--rhc-color-grijs-700);\n }\n .tag.plain::before {\n display: none;\n }\n pre {\n background: #1e2430;\n color: #e6e9ef;\n padding: 1rem;\n border-radius: 8px;\n overflow: auto;\n font-size: 0.82rem;\n line-height: 1.55;\n margin: 0;\n }\n pre .k {\n color: #c792ea;\n }\n pre .s {\n color: #c3e88d;\n }\n pre .c {\n color: #7e8aa0;\n font-style: italic;\n }\n .note {\n font-size: 0.9rem;\n color: var(--rhc-color-grijs-700);\n margin: 0.75rem 0 0;\n }\n /* live state diagram */\n .machine {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin: 0 0 1rem;\n }\n .node {\n padding: 0.4rem 0.8rem;\n border-radius: 999px;\n border: 1px solid var(--rhc-color-grijs-300, #ccc);\n font-size: 0.82rem;\n color: var(--rhc-color-grijs-700);\n transition: all 0.15s;\n }\n .node.on {\n background: var(--rhc-color-hemelblauw-100, #e5f1fb);\n border-color: var(--rhc-color-hemelblauw-500, #007bc7);\n color: var(--rhc-color-hemelblauw-700, #00567d);\n font-weight: 700;\n }\n .steplist {\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem;\n align-items: center;\n margin: 0 0 1rem;\n }\n .pill {\n padding: 0.3rem 0.7rem;\n border-radius: 8px;\n background: var(--rhc-color-grijs-100, #f3f3f3);\n font-size: 0.8rem;\n }\n .pill.extra {\n background: var(--rhc-color-geel-100, #fff6d6);\n border: 1px dashed var(--rhc-color-geel-600, #c79a00);\n }\n .arrow {\n color: var(--rhc-color-grijs-400, #999);\n }\n \n", @@ -13808,7 +13881,7 @@ }, { "name": "DashboardPage", - "id": "component-DashboardPage-14d24a4a5f651727466c0cf030f4fb56ebd3048c3a201a8353e8d8c9adff866e6913b66a39b802f498e4a787750c03df6b184ed836fcc5a3fb60e52f206cc534", + "id": "component-DashboardPage-be48058917ef2c7a5187b3a097fb45a8bcdb1827f53d3785b917eb34cc020265b36f8cabe1e2f2bc397c1ece61e8bd947b483e5691bbede87211135d2ec7c2e8", "file": "src/app/registratie/ui/dashboard.page.ts", "encapsulation": [], "entryComponents": [], @@ -13818,13 +13891,28 @@ "selector": "app-dashboard-page", "styleUrls": [], "styles": [], - "template": "\n
\n @if (aanvragen().length) {\n
\n @for (a of concepten(); track a.id) {\n \n }\n @if (ingediend().length) {\n Mijn aanvragen\n \n @for (a of ingediend(); track a.id) {\n @let row = submittedRow(a);\n \n }\n \n }\n
\n }\n\n @if (store.pendingHerregistratie()) {\n Uw herregistratie-aanvraag is in behandeling.\n }\n\n \n \n @let tasks = tasksFor($any(p).registration);\n\n
\n @if (tasks.length) {\n \n } @else {\n Wat moet ik regelen\n

\n U heeft op dit moment niets openstaan.\n

\n }\n
\n\n
\n Mijn registratie\n
\n \n
\n \n
\n \n \n \n \n
\n \n \n \n
\n\n
\n Specialismen en aantekeningen\n
\n \n \n \n \n \n \n \n \n

\n U heeft nog geen specialismen of aantekeningen.\n

\n
\n
\n
\n
\n\n
\n Wat wilt u doen?\n \n @for (a of acties; track a.to) {\n \n }\n \n
\n \n\n", + "template": "\n
\n @if (aanvragen().length) {\n
\n @for (a of concepten(); track a.id) {\n \n }\n @if (ingediend().length) {\n Mijn aanvragen\n \n @for (a of ingediend(); track a.id) {\n @let row = submittedRow(a);\n \n }\n \n }\n
\n }\n\n @if (store.pendingHerregistratie()) {\n Uw herregistratie-aanvraag is in behandeling.\n }\n\n \n \n @if (profile(); as p) {\n @let tasks = tasksFor(p.registration);\n\n
\n @if (tasks.length) {\n \n } @else {\n Wat moet ik regelen\n

\n U heeft op dit moment niets openstaan.\n

\n }\n
\n\n
\n Mijn registratie\n
\n \n
\n \n
\n \n \n \n \n }\n
\n \n \n \n
\n\n
\n Specialismen en aantekeningen\n
\n \n \n @if (aantekeningen(); as r) {\n \n }\n \n \n \n \n \n

\n U heeft nog geen specialismen of aantekeningen.\n

\n
\n
\n
\n
\n\n
\n Wat wilt u doen?\n \n @for (a of acties; track a.to) {\n \n }\n \n
\n \n\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], "inputsClass": [], "outputsClass": [], "propertiesClass": [ + { + "name": "aantekeningen", + "defaultValue": "computed(() => {\n const rd = this.store.aantekeningen();\n return rd.tag === 'Success' ? rd.value : undefined;\n })", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "indexKey": "", + "optional": false, + "description": "", + "line": 254, + "modifierKind": [ + 124, + 148 + ] + }, { "name": "aanvragen", "defaultValue": "computed(() => {\n const rd = this.apps.applications();\n if (rd.tag !== 'Success') return [];\n const order: Record = {\n Concept: 0,\n InBehandeling: 1,\n Goedgekeurd: 2,\n Afgewezen: 2,\n };\n return rd.value.slice().sort((a, b) => order[a.status.tag] - order[b.status.tag]);\n })", @@ -13834,7 +13922,7 @@ "indexKey": "", "optional": false, "description": "

The user's applications, sorted Concept → In behandeling → resolved. Empty →\nthe"Mijn aanvragen" section is hidden (see template).

\n", - "line": 204, + "line": 208, "rawdescription": "\nThe user's applications, sorted Concept → In behandeling → resolved. Empty →\nthe\"Mijn aanvragen\" section is hidden (see template).", "modifierKind": [ 124 @@ -13849,7 +13937,7 @@ "indexKey": "", "optional": false, "description": "

Primary transactional actions, as an "aanvragen" list (see CIBG's\ncomponenten/aanvragen). The core portal sections live in the header nav now;\nthe teaching pages (concepts/brief) are only reachable from here.

\n", - "line": 245, + "line": 262, "rawdescription": "\nPrimary transactional actions, as an \"aanvragen\" list (see CIBG's\ncomponenten/aanvragen). The core portal sections live in the header nav now;\nthe teaching pages (concepts/brief) are only reachable from here.", "modifierKind": [ 124, @@ -13865,7 +13953,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 190, + "line": 194, "modifierKind": [ 123 ] @@ -13879,7 +13967,7 @@ "indexKey": "", "optional": false, "description": "

A Concept ("lopende aanvraag") renders as a melding above the list; the rest\nas keuzelijst items — the two shapes need different HTML contexts.

\n", - "line": 217, + "line": 221, "rawdescription": "\nA Concept (\"lopende aanvraag\") renders as a melding above the list; the rest\nas keuzelijst items — the two shapes need different HTML contexts.", "modifierKind": [ 124 @@ -13894,7 +13982,7 @@ "indexKey": "", "optional": false, "description": "

Server-computed eligibility (rendered, not recomputed).

\n", - "line": 233, + "line": 237, "rawdescription": "\nServer-computed eligibility (rendered, not recomputed).", "modifierKind": [ 123, @@ -13910,11 +13998,27 @@ "indexKey": "", "optional": false, "description": "", - "line": 218, + "line": 222, "modifierKind": [ 124 ] }, + { + "name": "profile", + "defaultValue": "computed(() => {\n const rd = this.store.profile();\n return rd.tag === 'Success' ? rd.value : undefined;\n })", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "indexKey": "", + "optional": false, + "description": "

Typed narrowing for the <app-async> loaded slot — <ng-template>'s own\ncontext can't inherit a generic from a sibling host input (Angular only infers\na structural directive's type parameter from an input on that same node), so\nthe Success value is unwrapped here instead of through let-.

\n", + "line": 250, + "rawdescription": "\nTyped narrowing for the `` loaded slot — ``'s own\ncontext can't inherit a generic from a sibling host input (Angular only infers\na structural directive's type parameter from an input on that same node), so\nthe Success value is unwrapped here instead of through `let-`.", + "modifierKind": [ + 124, + 148 + ] + }, { "name": "resumeRoutes", "defaultValue": "{\n registratie: '/registreren',\n herregistratie: '/herregistratie',\n intake: '/intake',\n }", @@ -13924,7 +14028,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 220, + "line": 224, "modifierKind": [ 123, 148 @@ -13939,7 +14043,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 191, + "line": 195, "modifierKind": [ 123 ] @@ -13953,7 +14057,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 189, + "line": 193, "modifierKind": [ 124 ] @@ -13967,7 +14071,7 @@ "indexKey": "", "optional": false, "description": "

Pure view mapping for a submitted aanvraag → CIBG aanvragen-row fields.

\n", - "line": 194, + "line": 198, "rawdescription": "\nPure view mapping for a submitted aanvraag → CIBG aanvragen-row fields.", "modifierKind": [ 124 @@ -13990,7 +14094,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 228, + "line": 232, "deprecated": false, "deprecationMessage": "", "modifierKind": [ @@ -14025,7 +14129,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 225, + "line": 229, "deprecated": false, "deprecationMessage": "", "modifierKind": [ @@ -14060,7 +14164,7 @@ "optional": false, "returnType": "any", "typeParameters": [], - "line": 238, + "line": 242, "deprecated": false, "deprecationMessage": "", "modifierKind": [ @@ -14142,7 +14246,7 @@ "description": "

Page:"Mijn overzicht" — the portal home, following the NL Design System\n"Mijn omgeving" pattern (side nav +"Wat moet ik regelen" +"Mijn zaken").

\n", "rawdescription": "\nPage:\"Mijn overzicht\" — the portal home, following the NL Design System\n\"Mijn omgeving\" pattern (side nav +\"Wat moet ik regelen\" +\"Mijn zaken\").", "type": "component", - "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { DataBlockComponent } from '@shared/ui/data-block/data-block.component';\nimport { TaskListComponent } from '@shared/ui/task-list/task-list.component';\nimport { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';\nimport { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';\nimport { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';\nimport { AanvraagBlockComponent } from '@registratie/ui/aanvraag-block/aanvraag-block.component';\nimport { BigProfileStore } from '@registratie/application/big-profile.store';\nimport { ApplicationsStore } from '@registratie/application/applications.store';\nimport { Registration } from '@registratie/domain/registration';\nimport { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';\nimport { submittedRow } from '@registratie/domain/aanvraag-view';\nimport { tasksFromProfile } from '@registratie/domain/tasks';\n\n/** Page:\"Mijn overzicht\" — the portal home, following the NL Design System\n\"Mijn omgeving\" pattern (side nav +\"Wat moet ik regelen\" +\"Mijn zaken\"). */\n@Component({\n selector: 'app-dashboard-page',\n imports: [\n PageShellComponent,\n HeadingComponent,\n AlertComponent,\n SkeletonComponent,\n DataRowComponent,\n DataBlockComponent,\n TaskListComponent,\n ApplicationListComponent,\n ApplicationLinkComponent,\n ...ASYNC,\n RegistrationSummaryComponent,\n RegistrationTableComponent,\n AanvraagBlockComponent,\n ],\n template: `\n \n
\n @if (aanvragen().length) {\n
\n @for (a of concepten(); track a.id) {\n \n }\n @if (ingediend().length) {\n Mijn aanvragen\n \n @for (a of ingediend(); track a.id) {\n @let row = submittedRow(a);\n \n }\n \n }\n
\n }\n\n @if (store.pendingHerregistratie()) {\n Uw herregistratie-aanvraag is in behandeling.\n }\n\n \n \n @let tasks = tasksFor($any(p).registration);\n\n
\n @if (tasks.length) {\n \n } @else {\n Wat moet ik regelen\n

\n U heeft op dit moment niets openstaan.\n

\n }\n
\n\n
\n Mijn registratie\n
\n \n
\n \n
\n \n \n \n \n
\n \n \n \n
\n\n
\n Specialismen en aantekeningen\n
\n \n \n \n \n \n \n \n \n

\n U heeft nog geen specialismen of aantekeningen.\n

\n
\n
\n
\n
\n\n
\n Wat wilt u doen?\n \n @for (a of acties; track a.to) {\n \n }\n \n
\n \n \n `,\n})\nexport class DashboardPage {\n protected store = inject(BigProfileStore);\n private apps = inject(ApplicationsStore);\n private router = inject(Router);\n\n /** Pure view mapping for a submitted aanvraag → CIBG aanvragen-row fields. */\n protected submittedRow = submittedRow;\n\n constructor() {\n // Re-fetch on each visit so server-computed auto-approval transitions show up\n // (Concept → In behandeling → Goedgekeurd after the processing window).\n this.apps.reload();\n }\n\n /** The user's applications, sorted Concept → In behandeling → resolved. Empty →\n the\"Mijn aanvragen\" section is hidden (see template). */\n protected aanvragen = computed(() => {\n const rd = this.apps.applications();\n if (rd.tag !== 'Success') return [];\n const order: Record = {\n Concept: 0,\n InBehandeling: 1,\n Goedgekeurd: 2,\n Afgewezen: 2,\n };\n return rd.value.slice().sort((a, b) => order[a.status.tag] - order[b.status.tag]);\n });\n /** A Concept (\"lopende aanvraag\") renders as a melding above the list; the rest\n as keuzelijst items — the two shapes need different HTML contexts. */\n protected concepten = computed(() => this.aanvragen().filter((a) => a.status.tag === 'Concept'));\n protected ingediend = computed(() => this.aanvragen().filter((a) => a.status.tag !== 'Concept'));\n\n private readonly resumeRoutes: Record = {\n registratie: '/registreren',\n herregistratie: '/herregistratie',\n intake: '/intake',\n };\n protected resume(a: Aanvraag) {\n void this.router.navigate([this.resumeRoutes[a.type]], { queryParams: { aanvraag: a.id } });\n }\n protected cancelAanvraag(a: Aanvraag) {\n void this.apps.cancel(a.id);\n }\n\n /** Server-computed eligibility (rendered, not recomputed). */\n private readonly eligible = computed(() => {\n const d = this.store.decisions();\n return d.tag === 'Success' && d.value.eligibleForHerregistratie;\n });\n\n protected tasksFor(reg: Registration) {\n return tasksFromProfile(reg, this.eligible());\n }\n\n /** Primary transactional actions, as an \"aanvragen\" list (see CIBG's\n componenten/aanvragen). The core portal sections live in the header nav now;\n the teaching pages (concepts/brief) are only reachable from here. */\n protected readonly acties = [\n {\n to: '/registreren',\n titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,\n tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,\n actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,\n },\n {\n to: '/herregistratie',\n titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,\n tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,\n actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,\n },\n {\n to: '/intake',\n titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,\n tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,\n actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,\n },\n {\n to: '/registratie',\n titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,\n tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,\n actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,\n },\n {\n to: '/concepts',\n titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,\n tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,\n actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,\n },\n {\n to: '/brief',\n titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,\n tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,\n actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,\n },\n ];\n}\n", + "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { HeadingComponent } from '@shared/ui/heading/heading.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { DataBlockComponent } from '@shared/ui/data-block/data-block.component';\nimport { TaskListComponent } from '@shared/ui/task-list/task-list.component';\nimport { ApplicationListComponent } from '@shared/ui/application-list/application-list.component';\nimport { ApplicationLinkComponent } from '@shared/ui/application-link/application-link.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';\nimport { RegistrationTableComponent } from '@registratie/ui/registration-table/registration-table.component';\nimport { AanvraagBlockComponent } from '@registratie/ui/aanvraag-block/aanvraag-block.component';\nimport { BigProfileStore } from '@registratie/application/big-profile.store';\nimport { ApplicationsStore } from '@registratie/application/applications.store';\nimport { Registration } from '@registratie/domain/registration';\nimport { Aanvraag, AanvraagType } from '@registratie/domain/aanvraag';\nimport { submittedRow } from '@registratie/domain/aanvraag-view';\nimport { tasksFromProfile } from '@registratie/domain/tasks';\n\n/** Page:\"Mijn overzicht\" — the portal home, following the NL Design System\n\"Mijn omgeving\" pattern (side nav +\"Wat moet ik regelen\" +\"Mijn zaken\"). */\n@Component({\n selector: 'app-dashboard-page',\n imports: [\n PageShellComponent,\n HeadingComponent,\n AlertComponent,\n SkeletonComponent,\n DataRowComponent,\n DataBlockComponent,\n TaskListComponent,\n ApplicationListComponent,\n ApplicationLinkComponent,\n ...ASYNC,\n RegistrationSummaryComponent,\n RegistrationTableComponent,\n AanvraagBlockComponent,\n ],\n template: `\n \n
\n @if (aanvragen().length) {\n
\n @for (a of concepten(); track a.id) {\n \n }\n @if (ingediend().length) {\n Mijn aanvragen\n \n @for (a of ingediend(); track a.id) {\n @let row = submittedRow(a);\n \n }\n \n }\n
\n }\n\n @if (store.pendingHerregistratie()) {\n Uw herregistratie-aanvraag is in behandeling.\n }\n\n \n \n @if (profile(); as p) {\n @let tasks = tasksFor(p.registration);\n\n
\n @if (tasks.length) {\n \n } @else {\n Wat moet ik regelen\n

\n U heeft op dit moment niets openstaan.\n

\n }\n
\n\n
\n Mijn registratie\n
\n \n
\n \n
\n \n \n \n \n }\n
\n \n \n \n
\n\n
\n Specialismen en aantekeningen\n
\n \n \n @if (aantekeningen(); as r) {\n \n }\n \n \n \n \n \n

\n U heeft nog geen specialismen of aantekeningen.\n

\n
\n
\n
\n
\n\n
\n Wat wilt u doen?\n \n @for (a of acties; track a.to) {\n \n }\n \n
\n \n \n `,\n})\nexport class DashboardPage {\n protected store = inject(BigProfileStore);\n private apps = inject(ApplicationsStore);\n private router = inject(Router);\n\n /** Pure view mapping for a submitted aanvraag → CIBG aanvragen-row fields. */\n protected submittedRow = submittedRow;\n\n constructor() {\n // Re-fetch on each visit so server-computed auto-approval transitions show up\n // (Concept → In behandeling → Goedgekeurd after the processing window).\n this.apps.reload();\n }\n\n /** The user's applications, sorted Concept → In behandeling → resolved. Empty →\n the\"Mijn aanvragen\" section is hidden (see template). */\n protected aanvragen = computed(() => {\n const rd = this.apps.applications();\n if (rd.tag !== 'Success') return [];\n const order: Record = {\n Concept: 0,\n InBehandeling: 1,\n Goedgekeurd: 2,\n Afgewezen: 2,\n };\n return rd.value.slice().sort((a, b) => order[a.status.tag] - order[b.status.tag]);\n });\n /** A Concept (\"lopende aanvraag\") renders as a melding above the list; the rest\n as keuzelijst items — the two shapes need different HTML contexts. */\n protected concepten = computed(() => this.aanvragen().filter((a) => a.status.tag === 'Concept'));\n protected ingediend = computed(() => this.aanvragen().filter((a) => a.status.tag !== 'Concept'));\n\n private readonly resumeRoutes: Record = {\n registratie: '/registreren',\n herregistratie: '/herregistratie',\n intake: '/intake',\n };\n protected resume(a: Aanvraag) {\n void this.router.navigate([this.resumeRoutes[a.type]], { queryParams: { aanvraag: a.id } });\n }\n protected cancelAanvraag(a: Aanvraag) {\n void this.apps.cancel(a.id);\n }\n\n /** Server-computed eligibility (rendered, not recomputed). */\n private readonly eligible = computed(() => {\n const d = this.store.decisions();\n return d.tag === 'Success' && d.value.eligibleForHerregistratie;\n });\n\n protected tasksFor(reg: Registration) {\n return tasksFromProfile(reg, this.eligible());\n }\n\n /** Typed narrowing for the `` loaded slot — ``'s own\n context can't inherit a generic from a sibling host input (Angular only infers\n a structural directive's type parameter from an input on that same node), so\n the Success value is unwrapped here instead of through `let-`. */\n protected readonly profile = computed(() => {\n const rd = this.store.profile();\n return rd.tag === 'Success' ? rd.value : undefined;\n });\n protected readonly aantekeningen = computed(() => {\n const rd = this.store.aantekeningen();\n return rd.tag === 'Success' ? rd.value : undefined;\n });\n\n /** Primary transactional actions, as an \"aanvragen\" list (see CIBG's\n componenten/aanvragen). The core portal sections live in the header nav now;\n the teaching pages (concepts/brief) are only reachable from here. */\n protected readonly acties = [\n {\n to: '/registreren',\n titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`,\n tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`,\n actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving`,\n },\n {\n to: '/herregistratie',\n titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`,\n tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`,\n actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan`,\n },\n {\n to: '/intake',\n titel: $localize`:@@dashboard.actie.intake.titel:Herregistratie-intake`,\n tekst: $localize`:@@dashboard.actie.intake.tekst:Vragenlijst met vertakkingen.`,\n actie: $localize`:@@dashboard.actie.intake.actie:Start intake`,\n },\n {\n to: '/registratie',\n titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`,\n tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`,\n actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens`,\n },\n {\n to: '/concepts',\n titel: $localize`:@@dashboard.actie.concepten.titel:Functionele patronen`,\n tekst: $localize`:@@dashboard.actie.concepten.tekst:Bekijk de FP/TEA-bouwstenen van deze POC.`,\n actie: $localize`:@@dashboard.actie.concepten.actie:Bekijk patronen`,\n },\n {\n to: '/brief',\n titel: $localize`:@@dashboard.actie.brief.titel:Brief opstellen`,\n tekst: $localize`:@@dashboard.actie.brief.tekst:Stel een brief samen uit vaste en vrije onderdelen.`,\n actie: $localize`:@@dashboard.actie.brief.actie:Start brief`,\n },\n ];\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -14152,7 +14256,7 @@ "deprecated": false, "deprecationMessage": "", "args": [], - "line": 194 + "line": 198 }, "extends": [] }, @@ -18944,7 +19048,7 @@ }, { "name": "RegistratieWizardComponent", - "id": "component-RegistratieWizardComponent-a997f6b706dd0276e72f7add4aed0ba23eacf8373c15aee4ddf53cce1b6e399713a624cbbe5f3214ab044665400805a38c99a592d63418ba36f5b66c4e0e17fa", + "id": "component-RegistratieWizardComponent-48f9a037e6359dfb3bfd1f5765c14c7b4303b557b6603d55b7947e902296f7151cd5616f4e86a458d8978e3c38810fb8210dde4b6847e22c32494770ad27f83f", "file": "src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts", "encapsulation": [], "entryComponents": [], @@ -18954,7 +19058,7 @@ "selector": "app-registratie-wizard", "styleUrls": [], "styles": [], - "template": " 0\"\n [errors]=\"errorList()\"\n [errorMessage]=\"errorMessage()\"\n i18n-submittingLabel=\"@@regWizard.submitting\"\n submittingLabel=\"Uw registratie wordt verwerkt…\"\n (primary)=\"onPrimary()\"\n (back)=\"dispatch({ tag: 'Back' })\"\n (cancel)=\"restart()\"\n (retry)=\"onRetry()\"\n (goToStep)=\"dispatch({ tag: 'GaNaarStap', cursor: $event })\"\n>\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig\n in.\n }\n }\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw\n beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig\n beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen($any(data)); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n
\n \n \n \n
\n\n \n @if (err('documenten')) {\n {{ err('documenten') }}\n }\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n
\n }\n \n }\n }\n\n
\n \n

\n Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n

\n
\n Nieuwe registratie starten\n
\n \n
\n\n", + "template": " 0\"\n [errors]=\"errorList()\"\n [errorMessage]=\"errorMessage()\"\n i18n-submittingLabel=\"@@regWizard.submitting\"\n submittingLabel=\"Uw registratie wordt verwerkt…\"\n (primary)=\"onPrimary()\"\n (back)=\"dispatch({ tag: 'Back' })\"\n (cancel)=\"restart()\"\n (retry)=\"onRetry()\"\n (goToStep)=\"dispatch({ tag: 'GaNaarStap', cursor: $event })\"\n>\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig\n in.\n }\n }\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n @if (duoData(); as data) {\n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies\n uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna\n handmatig beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen(data); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n }\n
\n \n \n \n
\n\n \n @if (err('documenten')) {\n {{ err('documenten') }}\n }\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n
\n }\n \n }\n }\n\n
\n \n

\n Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n

\n
\n Nieuwe registratie starten\n
\n \n
\n\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], @@ -18968,7 +19072,7 @@ "indexKey": "", "optional": false, "description": "

Optional seed so Storybook / tests can mount any state directly.

\n", - "line": 364, + "line": 366, "rawdescription": "\nOptional seed so Storybook / tests can mount any state directly.", "required": false } @@ -18984,7 +19088,7 @@ "indexKey": "", "optional": false, "description": "

The policy questions that apply to the current choice (server-decided).

\n", - "line": 527, + "line": 532, "rawdescription": "\nThe policy questions that apply to the current choice (server-decided).", "modifierKind": [ 124 @@ -18999,7 +19103,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 460, + "line": 462, "modifierKind": [ 124 ] @@ -19013,7 +19117,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 453, + "line": 455, "modifierKind": [ 124 ] @@ -19027,7 +19131,7 @@ "indexKey": "", "optional": false, "description": "

BRP lookup outcome (laden/gevonden/geen/fout) and the parsed DUO lookup, both\nserved by the application facade — the wizard renders, it does not fetch/parse.

\n", - "line": 484, + "line": 486, "rawdescription": "\nBRP lookup outcome (laden/gevonden/geen/fout) and the parsed DUO lookup, both\nserved by the application facade — the wizard renders, it does not fetch/parse.", "modifierKind": [ 124 @@ -19042,7 +19146,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 499, + "line": 504, "modifierKind": [ 124 ] @@ -19056,7 +19160,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 523, + "line": 528, "modifierKind": [ 124 ] @@ -19070,7 +19174,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 467, + "line": 469, "modifierKind": [ 124 ] @@ -19084,7 +19188,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 381, + "line": 383, "modifierKind": [ 124 ] @@ -19098,7 +19202,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 474, + "line": 476, "modifierKind": [ 124 ] @@ -19112,7 +19216,7 @@ "indexKey": "", "optional": false, "description": "

The radio selection: a diploma id, or the"not listed" sentinel in manual mode.

\n", - "line": 508, + "line": 513, "rawdescription": "\nThe radio selection: a diploma id, or the\"not listed\" sentinel in manual mode.", "modifierKind": [ 124 @@ -19127,7 +19231,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 512, + "line": 517, "modifierKind": [ 124 ] @@ -19141,7 +19245,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 378, + "line": 380, "modifierKind": [ 148 ] @@ -19155,7 +19259,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 382, + "line": 384, "modifierKind": [ 124 ] @@ -19169,7 +19273,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 398, + "line": 400, "modifierKind": [ 123 ] @@ -19182,11 +19286,11 @@ "type": "unknown", "indexKey": "", "optional": false, - "description": "

Parsed lookup as a plain value (or null) — used outside the beroep step (the\ncontrole summary) where the template variable isn't in scope.

\n", - "line": 489, - "rawdescription": "\nParsed lookup as a plain value (or null) — used outside the beroep step (the\ncontrole summary) where the template variable isn't in scope.", + "description": "

Parsed lookup as a plain value (or null) — used outside the beroep step (the\ncontrole summary) where the template variable isn't in scope, and\ninside it too: <ng-template appAsyncLoaded>'s own context can't inherit a\ngeneric from the sibling [data] input (Angular only infers a structural\ndirective's type parameter from an input on that same node).

\n", + "line": 494, + "rawdescription": "\nParsed lookup as a plain value (or null) — used outside the beroep step (the\ncontrole summary) where the template variable isn't in scope, and\ninside it too: ``'s own context can't inherit a\ngeneric from the sibling [data] input (Angular only infers a structural\ndirective's type parameter from an input on that same node).", "modifierKind": [ - 123 + 124 ] }, { @@ -19198,7 +19302,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 496, + "line": 501, "modifierKind": [ 124 ] @@ -19212,7 +19316,7 @@ "indexKey": "", "optional": false, "description": "

Current step's errors (incl. per-question), flattened for the error summary.

\n", - "line": 442, + "line": 444, "rawdescription": "\nCurrent step's errors (incl. per-question), flattened for the error summary.", "modifierKind": [ 124 @@ -19227,7 +19331,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 424, + "line": 426, "modifierKind": [ 124 ] @@ -19241,7 +19345,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 416, + "line": 418, "modifierKind": [ 124 ] @@ -19255,7 +19359,7 @@ "indexKey": "", "optional": false, "description": "

True while the user is entering a diploma manually (not in the DUO list).

\n", - "line": 506, + "line": 511, "rawdescription": "\nTrue while the user is entering a diploma manually (not in the DUO list).", "modifierKind": [ 124 @@ -19270,7 +19374,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 380, + "line": 382, "modifierKind": [ 123 ] @@ -19284,7 +19388,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 494, + "line": 499, "modifierKind": [ 148 ] @@ -19298,7 +19402,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 366, + "line": 368, "modifierKind": [ 148 ] @@ -19312,7 +19416,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 354, + "line": 356, "modifierKind": [ 123 ] @@ -19326,7 +19430,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 485, + "line": 487, "modifierKind": [ 124 ] @@ -19340,7 +19444,7 @@ "indexKey": "", "optional": false, "description": "

Preview/download link for a completed upload; the dev-simulation demo-* ids\nhave no stored bytes, so they get no link.

\n", - "line": 360, + "line": 362, "rawdescription": "\nPreview/download link for a completed upload; the dev-simulation `demo-*` ids\nhave no stored bytes, so they get no link.", "modifierKind": [ 124 @@ -19355,7 +19459,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 419, + "line": 421, "modifierKind": [ 124 ] @@ -19369,7 +19473,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 415, + "line": 417, "modifierKind": [ 124 ] @@ -19383,7 +19487,7 @@ "indexKey": "", "optional": false, "description": "

Answered policy questions for the controle summary (question text + answer).

\n", - "line": 533, + "line": 538, "rawdescription": "\nAnswered policy questions for the controle summary (question text + answer).", "modifierKind": [ 124 @@ -19398,7 +19502,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 500, + "line": 505, "modifierKind": [ 124 ] @@ -19412,7 +19516,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 502, + "line": 507, "modifierKind": [ 124 ] @@ -19426,7 +19530,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 429, + "line": 431, "modifierKind": [ 124 ] @@ -19440,7 +19544,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 377, + "line": 379, "modifierKind": [ 148 ] @@ -19454,7 +19558,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 411, + "line": 413, "modifierKind": [ 124 ] @@ -19468,7 +19572,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 367, + "line": 369, "modifierKind": [ 148 ] @@ -19482,7 +19586,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 412, + "line": 414, "modifierKind": [ 124 ] @@ -19496,7 +19600,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 372, + "line": 374, "modifierKind": [ 123 ] @@ -19510,7 +19614,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 356, + "line": 358, "modifierKind": [ 123 ] @@ -19524,7 +19628,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 383, + "line": 385, "modifierKind": [ 124 ] @@ -19538,7 +19642,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 355, + "line": 357, "modifierKind": [ 123 ] @@ -19552,7 +19656,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 384, + "line": 386, "modifierKind": [ 124 ] @@ -19566,7 +19670,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 498, + "line": 503, "modifierKind": [ 124 ] @@ -19596,7 +19700,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 547, + "line": 552, "deprecated": false, "deprecationMessage": "", "modifierKind": [ @@ -19633,7 +19737,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 594, + "line": 599, "deprecated": false, "deprecationMessage": "" }, @@ -19643,7 +19747,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 601, + "line": 606, "deprecated": false, "deprecationMessage": "" }, @@ -19653,7 +19757,7 @@ "optional": false, "returnType": "void", "typeParameters": [], - "line": 608, + "line": 613, "deprecated": false, "deprecationMessage": "", "rawdescription": "\nReset the wizard to a fresh start. Reload the BRP lookup so the address\nre-prefills, keeping the form and the\"vooraf ingevuld\" note consistent.", @@ -19665,7 +19769,7 @@ "optional": false, "returnType": "any", "typeParameters": [], - "line": 616, + "line": 621, "deprecated": false, "deprecationMessage": "", "rawdescription": "\nThe effect: when we enter Indienen, submit through the aanvraag lifecycle\n(duo → auto-approve, handmatig → manual), then dispatch the outcome.", @@ -19741,7 +19845,7 @@ "description": "

Organism: the BIG-registration wizard. All state lives in one signal driven by\nthe pure reduce (registratie-wizard.machine.ts). The BRP address prefills the\ndraft via an effect; the DUO diploma list renders through ; choosing\na diploma reveals its server-derived beroep. The draft is persisted to the\nbackend as a Concept aanvraag (createDraftSync) so a reload — or a"Verder gaan"\nfrom the dashboard via ?aanvraag=<id> — resumes progress. Built from existing\natoms/molecules.

\n", "rawdescription": "\nOrganism: the BIG-registration wizard. All state lives in one signal driven by\nthe pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the\ndraft via an effect; the DUO diploma list renders through ; choosing\na diploma reveals its server-derived beroep. The draft is persisted to the\nbackend as a Concept aanvraag (createDraftSync) so a reload — or a\"Verder gaan\"\nfrom the dashboard via `?aanvraag=` — resumes progress. Built from existing\natoms/molecules.", "type": "component", - "sourceCode": "import { Component, computed, effect, inject, input, untracked } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';\nimport { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';\nimport {\n WizardShellComponent,\n WizardError,\n WizardStatus,\n naarStapLabel,\n} from '@shared/layout/wizard-shell/wizard-shell.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';\nimport { createStore } from '@shared/application/store';\nimport { whenTag } from '@shared/kernel/fp';\nimport { RemoteData } from '@shared/application/remote-data';\nimport { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';\nimport { DuoLookupDto, PolicyQuestionDto } from '@registratie/contracts/duo-diplomas.dto';\nimport {\n RegistratieState,\n RegistratieMsg,\n Draft,\n DraftField,\n Correspondentie,\n StepId,\n initial,\n reduce,\n hasProgress,\n STEPS,\n} from '@registratie/domain/registratie-wizard.machine';\nimport { createDraftSync } from '@registratie/application/draft-sync';\nimport { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';\nimport { createUploadController } from '@shared/upload/upload-controller';\nimport { UploadAdapter } from '@shared/upload/upload.adapter';\nimport { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine';\n\nconst KANALEN = [\n { value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },\n { value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },\n];\nconst HANDMATIG = '__handmatig__'; // sentinel option:\"my diploma isn't listed\"\n/** The server-owned geldigheidsvraag whose\"ja\" answer requires a Dutch-taalvaardigheid\n upload (proof of the confirmed B2 level). Stable id shared with the backend. */\nconst NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';\n\n/** Organism: the BIG-registration wizard. All state lives in one signal driven by\n the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the\n draft via an effect; the DUO diploma list renders through ; choosing\n a diploma reveals its server-derived beroep. The draft is persisted to the\n backend as a Concept aanvraag (createDraftSync) so a reload — or a\"Verder gaan\"\n from the dashboard via `?aanvraag=` — resumes progress. Built from existing\n atoms/molecules. */\n@Component({\n selector: 'app-registratie-wizard',\n imports: [\n FormsModule,\n FormFieldComponent,\n TextInputComponent,\n RadioGroupComponent,\n ButtonComponent,\n AlertComponent,\n SkeletonComponent,\n DataRowComponent,\n ReviewSectionComponent,\n ConfirmationComponent,\n WizardShellComponent,\n AddressFieldsComponent,\n DocumentUploadComponent,\n ...ASYNC,\n ],\n template: `\n 0\"\n [errors]=\"errorList()\"\n [errorMessage]=\"errorMessage()\"\n i18n-submittingLabel=\"@@regWizard.submitting\"\n submittingLabel=\"Uw registratie wordt verwerkt…\"\n (primary)=\"onPrimary()\"\n (back)=\"dispatch({ tag: 'Back' })\"\n (cancel)=\"restart()\"\n (retry)=\"onRetry()\"\n (goToStep)=\"dispatch({ tag: 'GaNaarStap', cursor: $event })\"\n >\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig\n in.\n }\n }\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw\n beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig\n beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen($any(data)); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n
\n \n \n \n
\n\n \n @if (err('documenten')) {\n {{ err('documenten') }}\n }\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n
\n }\n \n }\n }\n\n
\n \n

\n Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n

\n
\n Nieuwe registratie starten\n
\n \n
\n \n `,\n})\nexport class RegistratieWizardComponent {\n private lookup = inject(RegistratieLookupStore);\n private uploadAdapter = inject(UploadAdapter);\n private store = createStore(initial, reduce);\n\n /** Preview/download link for a completed upload; the dev-simulation `demo-*` ids\n have no stored bytes, so they get no link. */\n protected previewUrlFor = (documentId: string): string | undefined =>\n documentId.startsWith('demo-') ? undefined : this.uploadAdapter.contentUrl(documentId);\n\n /** Optional seed so Storybook / tests can mount any state directly. */\n seed = input(initial);\n\n readonly kanalen = KANALEN;\n readonly stepLabels = [\n $localize`:@@regWizard.step.adres:Adres`,\n $localize`:@@regWizard.step.beroep:Beroep`,\n $localize`:@@regWizard.step.controle:Controle`,\n ]; // short labels for the stepper\n private stepTitles = [\n $localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`,\n $localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`,\n $localize`:@@regWizard.title.controle:Controleren en indienen`,\n ];\n readonly state = this.store.model;\n readonly dispatch = this.store.dispatch;\n\n private invullen = computed(() => whenTag(this.state(), 'Invullen'));\n protected cursor = computed(() => this.invullen()?.cursor ?? 0);\n protected draft = computed(() => this.invullen()?.draft ?? { antwoorden: {} });\n protected upload = computed(() => this.invullen()?.upload ?? initialUpload);\n protected uploadCtl = createUploadController({\n wizardId: 'registratie',\n getUpload: () => this.upload(),\n dispatch: (msg) => this.dispatch({ tag: 'Upload', msg }),\n // Required documents depend on answers (server decides): a diploma upload only for a\n // manual diploma; a Dutch-taalvaardigheid upload only once the applicant confirms\n // (\"ja\") the B2 language requirement.\n getCategoryParams: () => ({\n diplomaHerkomst: this.draft().diplomaHerkomst,\n taalvaardigheid: this.draft().antwoorden[NL_TAALVAARDIGHEID_VRAAG],\n }),\n });\n // Backend draft-sync (replaces sessionStorage): create a Concept once the user has\n // made progress, then debounced-sync the whole machine snapshot; resume by `?aanvraag`.\n private draftSync = createDraftSync({\n type: 'registratie',\n snapshot: () => {\n const s = this.state();\n if (s.tag !== 'Invullen' || !hasProgress(s)) return null;\n const documentIds = deliveryRefs(s.upload)\n .filter((r) => r.channel === 'digital' && r.documentId)\n .map((r) => r.documentId!);\n return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds };\n },\n onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as RegistratieState }),\n enabled: () => this.seed() === initial,\n });\n protected step = computed(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);\n protected stepTitle = computed(\n () => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)],\n );\n protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');\n protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? '');\n\n // --- Presentational wiring for the shared wizard shell ---------------------\n protected primaryLabel = computed(() => {\n if (this.step() === 'controle') return $localize`:@@regWizard.indienen:Registratie indienen`;\n const next = this.cursor() + 1;\n return naarStapLabel(next + 1, this.stepLabels[next]);\n });\n protected errorMessage = computed(\n () =>\n $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` +\n ` ${this.failedError()}`,\n );\n protected shellStatus = computed(() => {\n switch (this.state().tag) {\n case 'Invullen':\n return 'editing';\n case 'Indienen':\n return 'submitting';\n case 'Ingediend':\n return 'submitted';\n case 'Mislukt':\n return 'failed';\n }\n });\n /** Current step's errors (incl. per-question), flattened for the error summary. */\n protected errorList = computed(() => {\n const e = this.invullen()?.errors ?? {};\n const out: WizardError[] = [];\n for (const [k, v] of Object.entries(e)) {\n if (k !== 'antwoorden' && typeof v === 'string' && v) out.push({ id: k, message: v });\n }\n for (const [qid, msg] of Object.entries(e.antwoorden ?? {})) {\n if (msg) out.push({ id: 'vraag-' + qid, message: msg });\n }\n return out;\n });\n protected adresSamenvatting = computed(() => {\n const d = this.draft();\n return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')]\n .filter(Boolean)\n .join(', ');\n });\n // Readable labels for the controle summary (instead of raw enum values).\n protected adresHerkomstLabel = computed(\n () =>\n ({\n brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`,\n handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd`,\n })[this.draft().adresHerkomst ?? 'handmatig'],\n );\n protected correspondentieLabel = computed(\n () =>\n ({\n email: $localize`:@@regWizard.corr.email:Per e-mail`,\n post: $localize`:@@regWizard.corr.post:Per post`,\n })[this.draft().correspondentie ?? 'post'],\n );\n protected diplomaHerkomstLabel = computed(\n () =>\n ({\n duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`,\n handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)`,\n })[this.draft().diplomaHerkomst ?? 'handmatig'],\n );\n\n /** BRP lookup outcome (laden/gevonden/geen/fout) and the parsed DUO lookup, both\n served by the application facade — the wizard renders, it does not fetch/parse. */\n protected adresStatus = this.lookup.adresStatus;\n protected lookupRd: () => RemoteData = this.lookup.duoLookup;\n\n /** Parsed lookup as a plain value (or null) — used outside the beroep step (the\n controle summary) where the template variable isn't in scope. */\n private duoData = computed(() => {\n const rd = this.lookupRd();\n return rd.tag === 'Success' ? rd.value : null;\n });\n\n readonly jaNee = JA_NEE;\n\n protected err = (k: DraftField | 'correspondentie' | 'diploma' | 'documenten') =>\n this.invullen()?.errors[k] ?? '';\n protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';\n protected antwoord = (id: string) => this.draft().antwoorden[id] ?? ''; // runtime guard: missing key → undefined\n protected set = (key: DraftField, value: string) =>\n this.dispatch({ tag: 'SetField', key, value });\n protected setKanaal = (value: string) =>\n this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });\n\n /** True while the user is entering a diploma manually (not in the DUO list). */\n protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');\n /** The radio selection: a diploma id, or the\"not listed\" sentinel in manual mode. */\n protected diplomaKeuze = computed(() =>\n this.handmatigActief() ? HANDMATIG : (this.draft().diplomaId ?? ''),\n );\n\n protected diplomaOptions = (data: DuoLookupDto) => [\n ...data.diplomas.map((d) => ({\n value: d.id,\n label: `${d.naam} — ${d.instelling} (${d.jaar})`,\n })),\n {\n value: HANDMATIG,\n label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij`,\n },\n ];\n\n protected beroepOptions = (data: DuoLookupDto) =>\n data.handmatig.beroepen.map((b) => ({ value: b, label: b }));\n\n /** The policy questions that apply to the current choice (server-decided). */\n protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {\n if (this.handmatigActief()) return data.handmatig.policyQuestions;\n return data.diplomas.find((d) => d.id === this.draft().diplomaId)?.policyQuestions ?? [];\n };\n\n /** Answered policy questions for the controle summary (question text + answer). */\n protected samenvattingVragen = computed(() => {\n const data = this.duoData();\n const d = this.draft();\n if (!data) return [] as { vraag: string; antwoord: string }[];\n const alle = [\n ...data.diplomas.flatMap((x) => x.policyQuestions),\n ...data.handmatig.policyQuestions,\n ];\n return (d.vraagIds ?? []).map((id) => ({\n vraag: alle.find((q) => q.id === id)?.vraag ?? id,\n antwoord: d.antwoorden[id] ?? '',\n }));\n });\n\n protected onDiplomaKeuze(data: DuoLookupDto, id: string) {\n if (id === HANDMATIG) {\n this.dispatch({\n tag: 'KiesHandmatig',\n vraagIds: data.handmatig.policyQuestions.map((q) => q.id),\n });\n return;\n }\n const d = data.diplomas.find((x) => x.id === id);\n if (d)\n this.dispatch({\n tag: 'KiesDiploma',\n diplomaId: d.id,\n beroep: d.beroep,\n vraagIds: d.policyQuestions.map((q) => q.id),\n });\n }\n\n constructor() {\n // An explicit seed (stories/tests) wins; otherwise resume from the backend draft\n // (`?aanvraag=`), or start fresh. Persistence is the draftSync controller's job.\n const seeded = this.seed();\n queueMicrotask(() =>\n seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),\n );\n // Prefill the address from the BRP lookup as it arrives. Track only the facade's\n // parsed prefill signal; untrack the dispatch (it reads the state signal, which\n // would otherwise make this effect loop on its own write). Don't clobber\n // edits/restored data. A null prefill (loading/error/geen adres) leaves manual entry.\n effect(() => {\n const a = this.lookup.prefillAdres();\n if (!a) return;\n untracked(() => {\n const s = this.state();\n if (s.tag !== 'Invullen' || s.draft.straat) return;\n this.dispatch({\n tag: 'PrefillAdres',\n straat: a.straat,\n postcode: a.postcode,\n woonplaats: a.woonplaats,\n });\n });\n });\n // A11y: focus management (step heading on step change, error summary on a\n // failed submit) now lives in the shared WizardShellComponent.\n }\n\n onPrimary() {\n const s = this.state();\n if (s.tag !== 'Invullen') return;\n this.dispatch(this.step() === 'controle' ? { tag: 'Submit' } : { tag: 'Next' });\n this.runIfIndienen();\n }\n\n onRetry() {\n this.dispatch({ tag: 'Retry' });\n this.runIfIndienen();\n }\n\n /** Reset the wizard to a fresh start. Reload the BRP lookup so the address\n re-prefills, keeping the form and the\"vooraf ingevuld\" note consistent. */\n restart() {\n this.draftSync.reset(); // discard the current Concept; a fresh one starts on next progress\n this.dispatch({ tag: 'Seed', state: initial });\n this.lookup.reloadAdres();\n }\n\n /** The effect: when we enter Indienen, submit through the aanvraag lifecycle\n (duo → auto-approve, handmatig → manual), then dispatch the outcome. */\n private async runIfIndienen() {\n const s = this.state();\n if (s.tag !== 'Indienen') return;\n const r = await this.draftSync.submit({\n diplomaHerkomst: s.data.diplomaHerkomst,\n documents: s.data.documents,\n });\n if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value.referentie ?? '' });\n else this.dispatch({ tag: 'SubmitFailed', error: r.error });\n }\n}\n", + "sourceCode": "import { Component, computed, effect, inject, input, untracked } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { FormFieldComponent } from '@shared/ui/form-field/form-field.component';\nimport { TextInputComponent } from '@shared/ui/text-input/text-input.component';\nimport { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';\nimport { ButtonComponent } from '@shared/ui/button/button.component';\nimport { AlertComponent } from '@shared/ui/alert/alert.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { ReviewSectionComponent } from '@shared/ui/review-section/review-section.component';\nimport { ConfirmationComponent } from '@shared/ui/confirmation/confirmation.component';\nimport {\n WizardShellComponent,\n WizardError,\n WizardStatus,\n naarStapLabel,\n} from '@shared/layout/wizard-shell/wizard-shell.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component';\nimport { createStore } from '@shared/application/store';\nimport { whenTag } from '@shared/kernel/fp';\nimport { RemoteData } from '@shared/application/remote-data';\nimport { RegistratieLookupStore } from '@registratie/application/registratie-lookup.store';\nimport { DuoLookupDto, PolicyQuestionDto } from '@registratie/contracts/duo-diplomas.dto';\nimport {\n RegistratieState,\n RegistratieMsg,\n Draft,\n DraftField,\n Correspondentie,\n StepId,\n initial,\n reduce,\n hasProgress,\n STEPS,\n} from '@registratie/domain/registratie-wizard.machine';\nimport { createDraftSync } from '@registratie/application/draft-sync';\nimport { DocumentUploadComponent } from '@shared/ui/upload/document-upload/document-upload.component';\nimport { createUploadController } from '@shared/upload/upload-controller';\nimport { UploadAdapter } from '@shared/upload/upload.adapter';\nimport { UploadState, initialUpload, deliveryRefs } from '@shared/upload/upload.machine';\n\nconst KANALEN = [\n { value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },\n { value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },\n];\nconst HANDMATIG = '__handmatig__'; // sentinel option:\"my diploma isn't listed\"\n/** The server-owned geldigheidsvraag whose\"ja\" answer requires a Dutch-taalvaardigheid\n upload (proof of the confirmed B2 level). Stable id shared with the backend. */\nconst NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';\n\n/** Organism: the BIG-registration wizard. All state lives in one signal driven by\n the pure `reduce` (registratie-wizard.machine.ts). The BRP address prefills the\n draft via an effect; the DUO diploma list renders through ; choosing\n a diploma reveals its server-derived beroep. The draft is persisted to the\n backend as a Concept aanvraag (createDraftSync) so a reload — or a\"Verder gaan\"\n from the dashboard via `?aanvraag=` — resumes progress. Built from existing\n atoms/molecules. */\n@Component({\n selector: 'app-registratie-wizard',\n imports: [\n FormsModule,\n FormFieldComponent,\n TextInputComponent,\n RadioGroupComponent,\n ButtonComponent,\n AlertComponent,\n SkeletonComponent,\n DataRowComponent,\n ReviewSectionComponent,\n ConfirmationComponent,\n WizardShellComponent,\n AddressFieldsComponent,\n DocumentUploadComponent,\n ...ASYNC,\n ],\n template: `\n 0\"\n [errors]=\"errorList()\"\n [errorMessage]=\"errorMessage()\"\n i18n-submittingLabel=\"@@regWizard.submitting\"\n submittingLabel=\"Uw registratie wordt verwerkt…\"\n (primary)=\"onPrimary()\"\n (back)=\"dispatch({ tag: 'Back' })\"\n (cancel)=\"restart()\"\n (retry)=\"onRetry()\"\n (goToStep)=\"dispatch({ tag: 'GaNaarStap', cursor: $event })\"\n >\n @switch (step()) {\n @case ('adres') {\n @if (adresStatus() === 'laden') {\n \n } @else {\n @switch (adresStatus()) {\n @case ('gevonden') {\n Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.\n }\n @case ('geen') {\n We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.\n }\n @case ('fout') {\n We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig\n in.\n }\n }\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n \n \n }\n }\n }\n @case ('beroep') {\n \n \n @if (duoData(); as data) {\n \n \n \n\n @if (handmatigActief()) {\n Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies\n uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna\n handmatig beoordeeld.\n \n \n \n } @else if (draft().beroep) {\n
\n \n
\n }\n\n @for (q of actieveVragen(data); track q.id) {\n \n @if (q.type === 'ja-nee') {\n \n } @else {\n \n }\n \n }\n }\n
\n \n \n \n
\n\n \n @if (err('documenten')) {\n {{ err('documenten') }}\n }\n }\n @case ('controle') {\n Controleer uw gegevens en dien de registratie in.\n \n \n \n \n @if (draft().correspondentie === 'email') {\n \n }\n \n \n \n \n @for (item of samenvattingVragen(); track item.vraag) {\n
\n }\n \n }\n }\n\n
\n \n

\n Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.\n

\n
\n Nieuwe registratie starten\n
\n \n
\n \n `,\n})\nexport class RegistratieWizardComponent {\n private lookup = inject(RegistratieLookupStore);\n private uploadAdapter = inject(UploadAdapter);\n private store = createStore(initial, reduce);\n\n /** Preview/download link for a completed upload; the dev-simulation `demo-*` ids\n have no stored bytes, so they get no link. */\n protected previewUrlFor = (documentId: string): string | undefined =>\n documentId.startsWith('demo-') ? undefined : this.uploadAdapter.contentUrl(documentId);\n\n /** Optional seed so Storybook / tests can mount any state directly. */\n seed = input(initial);\n\n readonly kanalen = KANALEN;\n readonly stepLabels = [\n $localize`:@@regWizard.step.adres:Adres`,\n $localize`:@@regWizard.step.beroep:Beroep`,\n $localize`:@@regWizard.step.controle:Controle`,\n ]; // short labels for the stepper\n private stepTitles = [\n $localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`,\n $localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`,\n $localize`:@@regWizard.title.controle:Controleren en indienen`,\n ];\n readonly state = this.store.model;\n readonly dispatch = this.store.dispatch;\n\n private invullen = computed(() => whenTag(this.state(), 'Invullen'));\n protected cursor = computed(() => this.invullen()?.cursor ?? 0);\n protected draft = computed(() => this.invullen()?.draft ?? { antwoorden: {} });\n protected upload = computed(() => this.invullen()?.upload ?? initialUpload);\n protected uploadCtl = createUploadController({\n wizardId: 'registratie',\n getUpload: () => this.upload(),\n dispatch: (msg) => this.dispatch({ tag: 'Upload', msg }),\n // Required documents depend on answers (server decides): a diploma upload only for a\n // manual diploma; a Dutch-taalvaardigheid upload only once the applicant confirms\n // (\"ja\") the B2 language requirement.\n getCategoryParams: () => ({\n diplomaHerkomst: this.draft().diplomaHerkomst,\n taalvaardigheid: this.draft().antwoorden[NL_TAALVAARDIGHEID_VRAAG],\n }),\n });\n // Backend draft-sync (replaces sessionStorage): create a Concept once the user has\n // made progress, then debounced-sync the whole machine snapshot; resume by `?aanvraag`.\n private draftSync = createDraftSync({\n type: 'registratie',\n snapshot: () => {\n const s = this.state();\n if (s.tag !== 'Invullen' || !hasProgress(s)) return null;\n const documentIds = deliveryRefs(s.upload)\n .filter((r) => r.channel === 'digital' && r.documentId)\n .map((r) => r.documentId!);\n return { draft: s, stepIndex: s.cursor, stepCount: STEPS.length, documentIds };\n },\n onResume: (draft) => this.dispatch({ tag: 'Seed', state: draft as RegistratieState }),\n enabled: () => this.seed() === initial,\n });\n protected step = computed(() => STEPS[Math.min(this.cursor(), STEPS.length - 1)]);\n protected stepTitle = computed(\n () => this.stepTitles[Math.min(this.cursor(), this.stepTitles.length - 1)],\n );\n protected referentie = computed(() => whenTag(this.state(), 'Ingediend')?.referentie ?? '');\n protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? '');\n\n // --- Presentational wiring for the shared wizard shell ---------------------\n protected primaryLabel = computed(() => {\n if (this.step() === 'controle') return $localize`:@@regWizard.indienen:Registratie indienen`;\n const next = this.cursor() + 1;\n return naarStapLabel(next + 1, this.stepLabels[next]);\n });\n protected errorMessage = computed(\n () =>\n $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` +\n ` ${this.failedError()}`,\n );\n protected shellStatus = computed(() => {\n switch (this.state().tag) {\n case 'Invullen':\n return 'editing';\n case 'Indienen':\n return 'submitting';\n case 'Ingediend':\n return 'submitted';\n case 'Mislukt':\n return 'failed';\n }\n });\n /** Current step's errors (incl. per-question), flattened for the error summary. */\n protected errorList = computed(() => {\n const e = this.invullen()?.errors ?? {};\n const out: WizardError[] = [];\n for (const [k, v] of Object.entries(e)) {\n if (k !== 'antwoorden' && typeof v === 'string' && v) out.push({ id: k, message: v });\n }\n for (const [qid, msg] of Object.entries(e.antwoorden ?? {})) {\n if (msg) out.push({ id: 'vraag-' + qid, message: msg });\n }\n return out;\n });\n protected adresSamenvatting = computed(() => {\n const d = this.draft();\n return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')]\n .filter(Boolean)\n .join(', ');\n });\n // Readable labels for the controle summary (instead of raw enum values).\n protected adresHerkomstLabel = computed(\n () =>\n ({\n brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`,\n handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd`,\n })[this.draft().adresHerkomst ?? 'handmatig'],\n );\n protected correspondentieLabel = computed(\n () =>\n ({\n email: $localize`:@@regWizard.corr.email:Per e-mail`,\n post: $localize`:@@regWizard.corr.post:Per post`,\n })[this.draft().correspondentie ?? 'post'],\n );\n protected diplomaHerkomstLabel = computed(\n () =>\n ({\n duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`,\n handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)`,\n })[this.draft().diplomaHerkomst ?? 'handmatig'],\n );\n\n /** BRP lookup outcome (laden/gevonden/geen/fout) and the parsed DUO lookup, both\n served by the application facade — the wizard renders, it does not fetch/parse. */\n protected adresStatus = this.lookup.adresStatus;\n protected lookupRd: () => RemoteData = this.lookup.duoLookup;\n\n /** Parsed lookup as a plain value (or null) — used outside the beroep step (the\n controle summary) where the template variable isn't in scope, and\n inside it too: ``'s own context can't inherit a\n generic from the sibling [data] input (Angular only infers a structural\n directive's type parameter from an input on that same node). */\n protected duoData = computed(() => {\n const rd = this.lookupRd();\n return rd.tag === 'Success' ? rd.value : null;\n });\n\n readonly jaNee = JA_NEE;\n\n protected err = (k: DraftField | 'correspondentie' | 'diploma' | 'documenten') =>\n this.invullen()?.errors[k] ?? '';\n protected vraagErr = (id: string) => this.invullen()?.errors.antwoorden?.[id] ?? '';\n protected antwoord = (id: string) => this.draft().antwoorden[id] ?? ''; // runtime guard: missing key → undefined\n protected set = (key: DraftField, value: string) =>\n this.dispatch({ tag: 'SetField', key, value });\n protected setKanaal = (value: string) =>\n this.dispatch({ tag: 'SetCorrespondentie', value: value as Correspondentie });\n\n /** True while the user is entering a diploma manually (not in the DUO list). */\n protected handmatigActief = computed(() => this.draft().diplomaHerkomst === 'handmatig');\n /** The radio selection: a diploma id, or the\"not listed\" sentinel in manual mode. */\n protected diplomaKeuze = computed(() =>\n this.handmatigActief() ? HANDMATIG : (this.draft().diplomaId ?? ''),\n );\n\n protected diplomaOptions = (data: DuoLookupDto) => [\n ...data.diplomas.map((d) => ({\n value: d.id,\n label: `${d.naam} — ${d.instelling} (${d.jaar})`,\n })),\n {\n value: HANDMATIG,\n label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij`,\n },\n ];\n\n protected beroepOptions = (data: DuoLookupDto) =>\n data.handmatig.beroepen.map((b) => ({ value: b, label: b }));\n\n /** The policy questions that apply to the current choice (server-decided). */\n protected actieveVragen = (data: DuoLookupDto): PolicyQuestionDto[] => {\n if (this.handmatigActief()) return data.handmatig.policyQuestions;\n return data.diplomas.find((d) => d.id === this.draft().diplomaId)?.policyQuestions ?? [];\n };\n\n /** Answered policy questions for the controle summary (question text + answer). */\n protected samenvattingVragen = computed(() => {\n const data = this.duoData();\n const d = this.draft();\n if (!data) return [] as { vraag: string; antwoord: string }[];\n const alle = [\n ...data.diplomas.flatMap((x) => x.policyQuestions),\n ...data.handmatig.policyQuestions,\n ];\n return (d.vraagIds ?? []).map((id) => ({\n vraag: alle.find((q) => q.id === id)?.vraag ?? id,\n antwoord: d.antwoorden[id] ?? '',\n }));\n });\n\n protected onDiplomaKeuze(data: DuoLookupDto, id: string) {\n if (id === HANDMATIG) {\n this.dispatch({\n tag: 'KiesHandmatig',\n vraagIds: data.handmatig.policyQuestions.map((q) => q.id),\n });\n return;\n }\n const d = data.diplomas.find((x) => x.id === id);\n if (d)\n this.dispatch({\n tag: 'KiesDiploma',\n diplomaId: d.id,\n beroep: d.beroep,\n vraagIds: d.policyQuestions.map((q) => q.id),\n });\n }\n\n constructor() {\n // An explicit seed (stories/tests) wins; otherwise resume from the backend draft\n // (`?aanvraag=`), or start fresh. Persistence is the draftSync controller's job.\n const seeded = this.seed();\n queueMicrotask(() =>\n seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),\n );\n // Prefill the address from the BRP lookup as it arrives. Track only the facade's\n // parsed prefill signal; untrack the dispatch (it reads the state signal, which\n // would otherwise make this effect loop on its own write). Don't clobber\n // edits/restored data. A null prefill (loading/error/geen adres) leaves manual entry.\n effect(() => {\n const a = this.lookup.prefillAdres();\n if (!a) return;\n untracked(() => {\n const s = this.state();\n if (s.tag !== 'Invullen' || s.draft.straat) return;\n this.dispatch({\n tag: 'PrefillAdres',\n straat: a.straat,\n postcode: a.postcode,\n woonplaats: a.woonplaats,\n });\n });\n });\n // A11y: focus management (step heading on step change, error summary on a\n // failed submit) now lives in the shared WizardShellComponent.\n }\n\n onPrimary() {\n const s = this.state();\n if (s.tag !== 'Invullen') return;\n this.dispatch(this.step() === 'controle' ? { tag: 'Submit' } : { tag: 'Next' });\n this.runIfIndienen();\n }\n\n onRetry() {\n this.dispatch({ tag: 'Retry' });\n this.runIfIndienen();\n }\n\n /** Reset the wizard to a fresh start. Reload the BRP lookup so the address\n re-prefills, keeping the form and the\"vooraf ingevuld\" note consistent. */\n restart() {\n this.draftSync.reset(); // discard the current Concept; a fresh one starts on next progress\n this.dispatch({ tag: 'Seed', state: initial });\n this.lookup.reloadAdres();\n }\n\n /** The effect: when we enter Indienen, submit through the aanvraag lifecycle\n (duo → auto-approve, handmatig → manual), then dispatch the outcome. */\n private async runIfIndienen() {\n const s = this.state();\n if (s.tag !== 'Indienen') return;\n const r = await this.draftSync.submit({\n diplomaHerkomst: s.data.diplomaHerkomst,\n documents: s.data.documents,\n });\n if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value.referentie ?? '' });\n else this.dispatch({ tag: 'SubmitFailed', error: r.error });\n }\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -19751,13 +19855,13 @@ "deprecated": false, "deprecationMessage": "", "args": [], - "line": 563 + "line": 568 }, "extends": [] }, { "name": "RegistrationDetailPage", - "id": "component-RegistrationDetailPage-58b413b4fb05310368df8942dc5dc9c582414eb2d9adb63a4d3e8cdff8cc651bd4c7f8a7db5630e19acfae54cc2376a8351c900e352933d95c4b1e002057972d", + "id": "component-RegistrationDetailPage-fa758679b2815bcdbfd271235c8fbc980bae745618f5fa5ecb9eed3ad2a4d00cf611d38e7653a978672f1b9deb20a63aa74d1bfe05531dfc6a1a8812da486fff", "file": "src/app/registratie/ui/registration-detail.page.ts", "encapsulation": [], "entryComponents": [], @@ -19767,13 +19871,29 @@ "selector": "app-registration-detail-page", "styleUrls": [], "styles": [], - "template": "\n \n \n \n \n \n \n \n \n\n
\n \n
\n\n", + "template": "\n \n \n @if (profile(); as p) {\n \n }\n \n \n \n \n \n\n
\n \n
\n\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], "inputsClass": [], "outputsClass": [], "propertiesClass": [ + { + "name": "profile", + "defaultValue": "computed(() => {\n const rd = this.store.profile();\n return rd.tag === 'Success' ? rd.value : undefined;\n })", + "deprecated": false, + "deprecationMessage": "", + "type": "unknown", + "indexKey": "", + "optional": false, + "description": "

See DashboardPage's profile for why this narrows via a computed instead of let-.

\n", + "line": 45, + "rawdescription": "\nSee DashboardPage's `profile` for why this narrows via a computed instead of `let-`.", + "modifierKind": [ + 124, + 148 + ] + }, { "name": "store", "defaultValue": "inject(BigProfileStore)", @@ -19783,7 +19903,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 40, + "line": 42, "modifierKind": [ 124 ] @@ -19819,7 +19939,7 @@ "description": "", "rawdescription": "\n", "type": "component", - "sourceCode": "import { Component, inject } from '@angular/core';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';\nimport { ChangeRequestFormComponent } from '@registratie/ui/change-request-form/change-request-form.component';\nimport { BigProfileStore } from '@registratie/application/big-profile.store';\n\n@Component({\n selector: 'app-registration-detail-page',\n imports: [\n PageShellComponent,\n SkeletonComponent,\n ...ASYNC,\n RegistrationSummaryComponent,\n ChangeRequestFormComponent,\n ],\n template: `\n \n \n \n \n \n \n \n \n \n\n
\n \n
\n \n `,\n})\nexport class RegistrationDetailPage {\n protected store = inject(BigProfileStore);\n}\n", + "sourceCode": "import { Component, computed, inject } from '@angular/core';\nimport { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';\nimport { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';\nimport { ASYNC } from '@shared/ui/async/async.component';\nimport { RegistrationSummaryComponent } from '@registratie/ui/registration-summary/registration-summary.component';\nimport { ChangeRequestFormComponent } from '@registratie/ui/change-request-form/change-request-form.component';\nimport { BigProfileStore } from '@registratie/application/big-profile.store';\n\n@Component({\n selector: 'app-registration-detail-page',\n imports: [\n PageShellComponent,\n SkeletonComponent,\n ...ASYNC,\n RegistrationSummaryComponent,\n ChangeRequestFormComponent,\n ],\n template: `\n \n \n \n @if (profile(); as p) {\n \n }\n \n \n \n \n \n\n
\n \n
\n \n `,\n})\nexport class RegistrationDetailPage {\n protected store = inject(BigProfileStore);\n\n /** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */\n protected readonly profile = computed(() => {\n const rd = this.store.profile();\n return rd.tag === 'Success' ? rd.value : undefined;\n });\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -19827,7 +19947,7 @@ }, { "name": "RegistrationSummaryComponent", - "id": "component-RegistrationSummaryComponent-bf649fbf16bc69cb3eb35ddffa7bca987da2702682b3f75593fe0b5c23b270708f7b6788e74171b32af62b9ad5d7ea228a94958689e57c6902ca7cb8cc895069", + "id": "component-RegistrationSummaryComponent-b57ccdd7541bc3f78b1ac846eda04ba8e69f28cef5a1a650f342473ac04ae4406adb7e923018bb4003c51c42dcc45ce201c04952cd62c97a60547ea92842c1dc", "file": "src/app/registratie/ui/registration-summary/registration-summary.component.ts", "encapsulation": [], "entryComponents": [], @@ -19837,7 +19957,7 @@ "selector": "app-registration-summary", "styleUrls": [], "styles": [], - "template": "\n \n
\n
\n
\n \n
\n \n \n @switch (reg().status.tag) {\n @case ('Geregistreerd') {\n \n }\n @case ('Geschorst') {\n \n \n }\n @case ('Doorgehaald') {\n \n \n }\n }\n
\n", + "template": "\n \n
\n
\n
\n \n
\n \n \n @let status = reg().status;\n @switch (status.tag) {\n @case ('Geregistreerd') {\n \n }\n @case ('Geschorst') {\n \n
\n }\n @case ('Doorgehaald') {\n \n
\n }\n }\n
\n", "templateUrl": [], "viewProviders": [], "hostDirectives": [], @@ -19850,7 +19970,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 76, + "line": 69, "required": true } ], @@ -19865,7 +19985,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 78, + "line": 71, "modifierKind": [ 124 ] @@ -19879,7 +19999,7 @@ "indexKey": "", "optional": false, "description": "", - "line": 77, + "line": 70, "modifierKind": [ 124 ] @@ -19912,7 +20032,7 @@ "description": "

Organism: registration summary in a CIBG Datablock. Composes data-row rows +\nstatus-badge atom (no card wrapper — the datablock carries its own surface).

\n", "rawdescription": "\nOrganism: registration summary in a CIBG Datablock. Composes data-row rows +\nstatus-badge atom (no card wrapper — the datablock carries its own surface).", "type": "component", - "sourceCode": "import { Component, input } from '@angular/core';\nimport { DatePipe } from '@angular/common';\nimport { Registration } from '@registratie/domain/registration';\nimport { statusColor, statusLabel } from '@registratie/domain/registration.policy';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';\nimport { DataBlockComponent } from '@shared/ui/data-block/data-block.component';\n\n/** Organism: registration summary in a CIBG Datablock. Composes data-row rows +\n status-badge atom (no card wrapper — the datablock carries its own surface). */\n@Component({\n selector: 'app-registration-summary',\n imports: [DatePipe, DataRowComponent, StatusBadgeComponent, DataBlockComponent],\n template: `\n \n \n
\n
\n
\n \n
\n \n \n @switch (reg().status.tag) {\n @case ('Geregistreerd') {\n \n }\n @case ('Geschorst') {\n \n \n }\n @case ('Doorgehaald') {\n \n \n }\n }\n
\n `,\n})\nexport class RegistrationSummaryComponent {\n reg = input.required();\n protected label = () => statusLabel(this.reg().status.tag);\n protected color = () => statusColor(this.reg().status.tag);\n}\n", + "sourceCode": "import { Component, input } from '@angular/core';\nimport { DatePipe } from '@angular/common';\nimport { Registration } from '@registratie/domain/registration';\nimport { statusColor, statusLabel } from '@registratie/domain/registration.policy';\nimport { DataRowComponent } from '@shared/ui/data-row/data-row.component';\nimport { StatusBadgeComponent } from '@shared/ui/status-badge/status-badge.component';\nimport { DataBlockComponent } from '@shared/ui/data-block/data-block.component';\n\n/** Organism: registration summary in a CIBG Datablock. Composes data-row rows +\n status-badge atom (no card wrapper — the datablock carries its own surface). */\n@Component({\n selector: 'app-registration-summary',\n imports: [DatePipe, DataRowComponent, StatusBadgeComponent, DataBlockComponent],\n template: `\n \n \n
\n
\n
\n \n
\n \n \n @let status = reg().status;\n @switch (status.tag) {\n @case ('Geregistreerd') {\n \n }\n @case ('Geschorst') {\n \n
\n }\n @case ('Doorgehaald') {\n \n
\n }\n }\n
\n `,\n})\nexport class RegistrationSummaryComponent {\n reg = input.required();\n protected label = () => statusLabel(this.reg().status.tag);\n protected color = () => statusColor(this.reg().status.tag);\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -40304,9 +40424,9 @@ "type": "component", "linktype": "component", "name": "AanvraagDetailPage", - "coveragePercent": 20, - "coverageCount": "1/5", - "status": "low" + "coveragePercent": 33, + "coverageCount": "2/6", + "status": "medium" }, { "filePath": "src/app/registratie/ui/address-fields/address-fields.component.ts", @@ -40350,8 +40470,8 @@ "type": "component", "linktype": "component", "name": "DashboardPage", - "coveragePercent": 40, - "coverageCount": "6/15", + "coveragePercent": 41, + "coverageCount": "7/17", "status": "medium" }, { @@ -40407,9 +40527,9 @@ "type": "component", "linktype": "component", "name": "RegistrationDetailPage", - "coveragePercent": 0, - "coverageCount": "0/2", - "status": "low" + "coveragePercent": 33, + "coverageCount": "1/3", + "status": "medium" }, { "filePath": "src/app/registratie/ui/registration-summary/registration-summary.component.ts", @@ -41526,7 +41646,7 @@ "linktype": "directive", "name": "AsyncLoadedDirective", "coveragePercent": 0, - "coverageCount": "0/3", + "coverageCount": "0/4", "status": "low" }, { diff --git a/src/app/registratie/ui/aanvraag-detail.page.ts b/src/app/registratie/ui/aanvraag-detail.page.ts index 8433ec9..59c8efe 100644 --- a/src/app/registratie/ui/aanvraag-detail.page.ts +++ b/src/app/registratie/ui/aanvraag-detail.page.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, computed, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component'; import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component'; @@ -30,24 +30,26 @@ import { detailRows } from '@registratie/domain/aanvraag-view'; backLink="/dashboard" > - - @let a = find($any(list)); - @if (a) { - - @for (row of rows(a); track row.key) { -
- } -
- - De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC. - - } @else { - Deze aanvraag is niet gevonden. + + @if (applications(); as list) { + @let a = find(list); + @if (a) { + + @for (row of rows(a); track row.key) { +
+ } +
+ + De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC. + + } @else { + Deze aanvraag is niet gevonden. + } }
@@ -63,4 +65,10 @@ 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 applications = computed(() => { + const rd = this.store.applications(); + return rd.tag === 'Success' ? rd.value : undefined; + }); } diff --git a/src/app/registratie/ui/dashboard.page.ts b/src/app/registratie/ui/dashboard.page.ts index 63a4e8b..2ba9741 100644 --- a/src/app/registratie/ui/dashboard.page.ts +++ b/src/app/registratie/ui/dashboard.page.ts @@ -87,59 +87,61 @@ import { tasksFromProfile } from '@registratie/domain/tasks'; } - - @let tasks = tasksFor($any(p).registration); + + @if (profile(); as p) { + @let tasks = tasksFor(p.registration); -
- @if (tasks.length) { - - } @else { - Wat moet ik regelen + @if (tasks.length) { + + } @else { + Wat moet ik regelen +

+ U heeft op dit moment niets openstaan. +

+ } +
+ +
+ Mijn registratie -

- U heeft op dit moment niets openstaan. -

- } -
- -
- Mijn registratie -
- -
- -
-
-
-
-
+
+ +
+ +
+
+
+
+ + }
@@ -152,8 +154,10 @@ import { tasksFromProfile } from '@registratie/domain/tasks'; >
- - + + @if (aantekeningen(); as r) { + + } @@ -239,6 +243,19 @@ export class DashboardPage { return tasksFromProfile(reg, this.eligible()); } + /** Typed narrowing for the `` loaded slot — ``'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. */ diff --git a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts index 7fcc350..72f1d92 100644 --- a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts +++ b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts @@ -169,83 +169,85 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid'; } @case ('beroep') { - - - - - - @if (handmatigActief()) { - Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw - beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig - beoordeeld. + + @if (duoData(); as data) { - } @else if (draft().beroep) { -
-
-
- } - @for (q of actieveVragen($any(data)); track q.id) { - - @if (q.type === 'ja-nee') { + @if (handmatigActief()) { + Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies + uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna + handmatig beoordeeld. + - } @else { - - } - + + } @else if (draft().beroep) { +
+
+
+ } + + @for (q of actieveVragen(data); track q.id) { + + @if (q.type === 'ja-nee') { + + } @else { + + } + + } }
@@ -485,8 +487,11 @@ export class RegistratieWizardComponent { protected lookupRd: () => RemoteData = this.lookup.duoLookup; /** Parsed lookup as a plain value (or null) — used outside the beroep step (the - controle summary) where the template variable isn't in scope. */ - private duoData = computed(() => { + controle summary) where the template variable isn't in scope, and + inside it too: ``'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(() => { const rd = this.lookupRd(); return rd.tag === 'Success' ? rd.value : null; }); diff --git a/src/app/registratie/ui/registration-detail.page.ts b/src/app/registratie/ui/registration-detail.page.ts index 0875b94..4ca85e0 100644 --- a/src/app/registratie/ui/registration-detail.page.ts +++ b/src/app/registratie/ui/registration-detail.page.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, computed, inject } from '@angular/core'; 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'; @@ -22,8 +22,10 @@ import { BigProfileStore } from '@registratie/application/big-profile.store'; backLink="/dashboard" > - - + + @if (profile(); as p) { + + } @@ -38,4 +40,10 @@ 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; + }); } diff --git a/src/app/registratie/ui/registration-summary/registration-summary.component.ts b/src/app/registratie/ui/registration-summary/registration-summary.component.ts index d8a431c..11d78d7 100644 --- a/src/app/registratie/ui/registration-summary/registration-summary.component.ts +++ b/src/app/registratie/ui/registration-summary/registration-summary.component.ts @@ -30,14 +30,17 @@ import { DataBlockComponent } from '@shared/ui/data-block/data-block.component'; key="Registratiedatum" [value]="reg().registratiedatum | date: 'longDate'" >
- - @switch (reg().status.tag) { + + @let status = reg().status; + @switch (status.tag) { @case ('Geregistreerd') {
} @case ('Geschorst') { @@ -45,28 +48,18 @@ import { DataBlockComponent } from '@shared/ui/data-block/data-block.component'; app-data-row i18n-key="@@summary.geschorstTot" key="Geschorst tot" - [value]="$any(reg().status).geschorstTot | date: 'longDate'" - > -
+
} @case ('Doorgehaald') {
-
+
} } diff --git a/src/app/shared/ui/async/async.component.ts b/src/app/shared/ui/async/async.component.ts index 301e4ff..2a319bb 100644 --- a/src/app/shared/ui/async/async.component.ts +++ b/src/app/shared/ui/async/async.component.ts @@ -6,10 +6,20 @@ import { AlertComponent } from '@shared/ui/alert/alert.component'; import { ButtonComponent } from '@shared/ui/button/button.component'; import { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data'; -/* Slot markers. Put on children of . */ +/* Slot markers. Put on children of . Generic so the + $implicit context is typed as the resource's T instead of unknown — see + AsyncComponent's contentChild> below, which threads the + host's own T through the query result type. */ @Directive({ selector: '[appAsyncLoaded]' }) -export class AsyncLoadedDirective { - constructor(public tpl: TemplateRef<{ $implicit: unknown }>) {} +export class AsyncLoadedDirective { + constructor(public tpl: TemplateRef<{ $implicit: T }>) {} + + static ngTemplateContextGuard( + _dir: AsyncLoadedDirective, + _ctx: unknown, + ): _ctx is { $implicit: T } { + return true; + } } @Directive({ selector: '[appAsyncLoading]' }) export class AsyncLoadingDirective { @@ -88,7 +98,7 @@ export class AsyncComponent { retryText = input($localize`:@@async.retry:Opnieuw proberen`); emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`); - loadedTpl = contentChild.required(AsyncLoadedDirective); + loadedTpl = contentChild.required>(AsyncLoadedDirective); loadingTpl = contentChild(AsyncLoadingDirective); emptyTpl = contentChild(AsyncEmptyDirective); errorTpl = contentChild(AsyncErrorDirective); diff --git a/src/app/showcase/concepts.page.ts b/src/app/showcase/concepts.page.ts index b910aa0..2ad55ba 100644 --- a/src/app/showcase/concepts.page.ts +++ b/src/app/showcase/concepts.page.ts @@ -223,9 +223,9 @@ function fakeResource(status: string, value?: T, error?: Error): Resource >

Success

    - @for (i of v; track i) { + @for (i of successRes.value(); track i) {
  • {{ i }}
  • }
(status: string, value?: T, error?: Error): Resource placeholder="Typ een postcode, bijv. 1234 AB" /> -
- @if (parsed().ok) { + @let r = parsed(); +
+ @if (r.ok) {

ok

-
Postcode ="{{ $any(parsed()).value }}"
+
Postcode ="{{ r.value }}"

Een gevalideerde Postcode is een ander type dan een ruwe string.

} @else {

err

-
{{ $any(parsed()).error }}
+
{{ r.error }}
}