feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
|||||||
|
// Dependency-cruiser (WP-38, generalized for the WP-67 monorepo split): the single
|
||||||
|
// declarative source for the bounded-context + atomic-layer boundaries. Each app
|
||||||
|
// (apps/ssp, apps/behandelportal) is cruised SEPARATELY against its own tsconfig.json
|
||||||
|
// (see .dependency-cruiser.<app>.js) — a single merged tsconfig can't resolve both
|
||||||
|
// apps' `@auth/*` alias at once, since each points at a different physical directory.
|
||||||
|
// This file is the shared rule *factory*; it never runs standalone.
|
||||||
|
//
|
||||||
|
// libs/shared and libs/beheer are cross-app libraries, not per-app feature contexts:
|
||||||
|
// any app context may import either; neither may import an app's feature context;
|
||||||
|
// libs/shared may not import libs/beheer (shared stays the base, beheer a peer leaf).
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string[] | null>} contextAllowed context name -> the OTHER
|
||||||
|
* app-local contexts it may additionally import (besides itself + libs/shared|beheer).
|
||||||
|
* `null` = unrestricted (showcase, the sanctioned teaching page).
|
||||||
|
* @param {string} appName the apps/<appName> directory this config cruises.
|
||||||
|
* @param {string} tsConfigFileName this app's own tsconfig.json (resolves its aliases).
|
||||||
|
*/
|
||||||
|
module.exports = function buildConfig(contextAllowed, appName, tsConfigFileName) {
|
||||||
|
const FEATURES = Object.keys(contextAllowed).join('|');
|
||||||
|
const appRoot = `apps/${appName}/src/app`;
|
||||||
|
|
||||||
|
const contextRule = (from) => {
|
||||||
|
const allowed = contextAllowed[from];
|
||||||
|
if (allowed === null) return null;
|
||||||
|
const forbidden = Object.keys(contextAllowed)
|
||||||
|
.filter((name) => name !== from && !allowed.includes(name))
|
||||||
|
.join('|');
|
||||||
|
return {
|
||||||
|
name: `${appName}-${from}-scope`,
|
||||||
|
comment: `${from} may depend only on its allowed contexts (+ libs/shared|beheer). See CLAUDE.md §1.`,
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: `^${appRoot}/${from}/` },
|
||||||
|
to: { path: `^${appRoot}/(${forbidden})/` },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Atomic-layer rules apply uniformly across this app's tree AND both libraries.
|
||||||
|
const anyRoot = `(${appRoot}|libs/shared/src|libs/beheer/src)`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
forbidden: [
|
||||||
|
// --- Bounded-context direction (the "dependencies point inward" spine) ---
|
||||||
|
{
|
||||||
|
name: 'shared-no-features',
|
||||||
|
comment: 'libs/shared is the base — it must not import any app feature context.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: '^libs/shared/src/' },
|
||||||
|
to: { path: `^${appRoot}/(${FEATURES})/` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'beheer-no-features',
|
||||||
|
comment: 'libs/beheer is a cross-app library — it must not import any app feature context.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: '^libs/beheer/src/' },
|
||||||
|
to: { path: `^${appRoot}/(${FEATURES})/` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shared-no-beheer',
|
||||||
|
comment: 'libs/shared stays the base — it must not depend on the beheer library.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: '^libs/shared/src/' },
|
||||||
|
to: { path: '^libs/beheer/src/' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${appName}-no-other-app`,
|
||||||
|
comment: "An app may not import another app's source directly.",
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: `^${appRoot}/` },
|
||||||
|
to: { path: '^apps/(?!' + appName + '/)' },
|
||||||
|
},
|
||||||
|
...Object.keys(contextAllowed).map(contextRule).filter(Boolean),
|
||||||
|
|
||||||
|
// --- Atomic-layer rules (dependencies point inward: ui → application → domain) ---
|
||||||
|
{
|
||||||
|
name: 'domain-is-pure',
|
||||||
|
comment: 'domain/ is framework-free business logic — no Angular.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: `^${anyRoot}/.*/domain/` },
|
||||||
|
to: { path: 'node_modules/@angular/' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contracts-import-nothing',
|
||||||
|
comment: 'contracts/ are pure wire DTO shapes — they import nothing (ADR-0001).',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: `^${anyRoot}/.*/contracts/` },
|
||||||
|
to: { pathNot: '/contracts/', path: `^(${anyRoot}/|node_modules/@angular/)` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ui-not-infrastructure',
|
||||||
|
comment:
|
||||||
|
'ui/ + layout/ reach data through an application store/command, never infrastructure directly (type-only DTO imports allowed).',
|
||||||
|
severity: 'error',
|
||||||
|
from: {
|
||||||
|
path: `^${anyRoot}/.*(/ui/|/layout/)`,
|
||||||
|
pathNot: '\\.stories\\.ts$|\\.spec\\.ts$',
|
||||||
|
},
|
||||||
|
to: { path: '/infrastructure/', dependencyTypesNot: ['type-only'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiclient-infrastructure-only',
|
||||||
|
comment:
|
||||||
|
'The generated ApiClient is a value only inside infrastructure/ (+ shared/upload); elsewhere type-only.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { pathNot: '/infrastructure/|^libs/shared/src/upload/' },
|
||||||
|
to: {
|
||||||
|
path: '^libs/shared/src/infrastructure/api-client\\.ts$',
|
||||||
|
dependencyTypesNot: ['type-only'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Hygiene (cheap wins a graph makes obvious) ---
|
||||||
|
{
|
||||||
|
name: 'no-circular',
|
||||||
|
comment: 'No cyclic dependencies.',
|
||||||
|
severity: 'error',
|
||||||
|
from: {},
|
||||||
|
to: { circular: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
options: {
|
||||||
|
doNotFollow: { path: 'node_modules' },
|
||||||
|
tsConfig: { fileName: tsConfigFileName },
|
||||||
|
tsPreCompilationDeps: true, // needed so `type-only` imports are distinguished
|
||||||
|
enhancedResolveOptions: {
|
||||||
|
exportsFields: ['exports'],
|
||||||
|
conditionNames: ['import', 'require', 'node', 'default'],
|
||||||
|
},
|
||||||
|
reporterOptions: {
|
||||||
|
archi: { collapsePattern: '^(apps/[^/]+/src/app|libs/[^/]+/src)/[^/]+' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// behandelportal's own context boundaries.
|
||||||
|
module.exports = require('./.dependency-cruiser.base.js')(
|
||||||
|
{
|
||||||
|
auth: [],
|
||||||
|
behandeling: [],
|
||||||
|
},
|
||||||
|
'behandelportal',
|
||||||
|
'apps/behandelportal/tsconfig.json',
|
||||||
|
);
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
// Dependency-cruiser (WP-38): the single declarative source for the app's bounded-context
|
|
||||||
// + atomic-layer boundaries — and the graph you can SEE (`npm run dep:graph`). Replaces the
|
|
||||||
// hand-duplicated `no-restricted-imports` blocks that had to be copied per context (and that
|
|
||||||
// left `herregistratie` without one). ESLint keeps only the rules dep-cruiser can't express
|
|
||||||
// (no-explicit-any, template a11y).
|
|
||||||
//
|
|
||||||
// Contexts: shared (base) · auth · registratie · herregistratie · brief · beheer · showcase.
|
|
||||||
// Allowed cross-context edges: everyone → shared; herregistratie → registratie; showcase → *
|
|
||||||
// (the sanctioned teaching page). Nobody imports showcase.
|
|
||||||
|
|
||||||
// Single source of truth for bounded-context boundaries: each entry maps a context name to the
|
|
||||||
// OTHER contexts it may additionally import (besides itself + shared). `showcase` maps to `null`
|
|
||||||
// — sanctioned to import every context (the teaching page); nothing else may import it. Add a
|
|
||||||
// context here — nowhere else — when scaffolding one (see `gen:context`, WP-44); FEATURES and
|
|
||||||
// every contextRule below are derived from this object.
|
|
||||||
const CONTEXT_ALLOWED = {
|
|
||||||
auth: [],
|
|
||||||
registratie: [],
|
|
||||||
herregistratie: ['registratie'], // the one sanctioned cross-feature edge
|
|
||||||
brief: [],
|
|
||||||
beheer: [],
|
|
||||||
showcase: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FEATURES = Object.keys(CONTEXT_ALLOWED).join('|');
|
|
||||||
|
|
||||||
/** A context may import shared + itself + its allowed list; forbidden = every other context. */
|
|
||||||
const contextRule = (from) => {
|
|
||||||
const allowed = CONTEXT_ALLOWED[from];
|
|
||||||
if (allowed === null) return null; // unrestricted (showcase) — no rule to generate
|
|
||||||
const forbidden = Object.keys(CONTEXT_ALLOWED)
|
|
||||||
.filter((name) => name !== from && !allowed.includes(name))
|
|
||||||
.join('|');
|
|
||||||
return {
|
|
||||||
name: `${from}-scope`,
|
|
||||||
comment: `${from} may depend only on its allowed contexts (+ shared). See CLAUDE.md §1.`,
|
|
||||||
severity: 'error',
|
|
||||||
from: { path: `^src/app/${from}/` },
|
|
||||||
to: { path: `^src/app/(${forbidden})/` },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
forbidden: [
|
|
||||||
// --- Bounded-context direction (the "dependencies point inward" spine) ---
|
|
||||||
{
|
|
||||||
name: 'shared-no-features',
|
|
||||||
comment: 'shared/ is the base — it must not import any feature context.',
|
|
||||||
severity: 'error',
|
|
||||||
from: { path: '^src/app/shared/', pathNot: '^src/app/shared/ui/debug-state/' },
|
|
||||||
to: { path: `^src/app/(${FEATURES})/` },
|
|
||||||
},
|
|
||||||
...Object.keys(CONTEXT_ALLOWED).map(contextRule).filter(Boolean),
|
|
||||||
// showcase/ is exempt (reads every context by design); nothing imports it — covered by the
|
|
||||||
// rules above each forbidding `→ showcase`.
|
|
||||||
|
|
||||||
// --- Atomic-layer rules (dependencies point inward: ui → application → domain) ---
|
|
||||||
{
|
|
||||||
name: 'domain-is-pure',
|
|
||||||
comment: 'domain/ is framework-free business logic — no Angular.',
|
|
||||||
severity: 'error',
|
|
||||||
from: { path: '/domain/' },
|
|
||||||
to: { path: 'node_modules/@angular/' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'contracts-import-nothing',
|
|
||||||
comment: 'contracts/ are pure wire DTO shapes — they import nothing (ADR-0001).',
|
|
||||||
severity: 'error',
|
|
||||||
from: { path: '/contracts/' },
|
|
||||||
to: { pathNot: '/contracts/', path: '^(src/app/|node_modules/@angular/)' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ui-not-infrastructure',
|
|
||||||
comment:
|
|
||||||
'ui/ + layout/ reach data through an application store/command, never infrastructure directly (type-only DTO imports allowed).',
|
|
||||||
severity: 'error',
|
|
||||||
from: {
|
|
||||||
path: '(/ui/|/layout/)',
|
|
||||||
pathNot: '\\.stories\\.ts$|\\.spec\\.ts$|^src/app/shared/ui/debug-state/',
|
|
||||||
},
|
|
||||||
to: { path: '/infrastructure/', dependencyTypesNot: ['type-only'] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'apiclient-infrastructure-only',
|
|
||||||
comment:
|
|
||||||
'The generated ApiClient is a value only inside infrastructure/ (+ shared/upload); elsewhere type-only.',
|
|
||||||
severity: 'error',
|
|
||||||
from: { pathNot: '/infrastructure/|^src/app/shared/upload/' },
|
|
||||||
to: {
|
|
||||||
path: '^src/app/shared/infrastructure/api-client\\.ts$',
|
|
||||||
dependencyTypesNot: ['type-only'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Hygiene (cheap wins a graph makes obvious) ---
|
|
||||||
{
|
|
||||||
name: 'no-circular',
|
|
||||||
comment: 'No cyclic dependencies.',
|
|
||||||
severity: 'error',
|
|
||||||
from: {},
|
|
||||||
to: { circular: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
options: {
|
|
||||||
doNotFollow: { path: 'node_modules' },
|
|
||||||
tsConfig: { fileName: 'tsconfig.json' }, // resolves @shared/@registratie/… path aliases
|
|
||||||
tsPreCompilationDeps: true, // needed so `type-only` imports are distinguished
|
|
||||||
enhancedResolveOptions: {
|
|
||||||
exportsFields: ['exports'],
|
|
||||||
conditionNames: ['import', 'require', 'node', 'default'],
|
|
||||||
},
|
|
||||||
reporterOptions: {
|
|
||||||
// Context-level architecture graph for `npm run dep:graph` (mermaid — no graphviz needed).
|
|
||||||
archi: { collapsePattern: '^src/app/[^/]+/[^/]+' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// The SSP's own context boundaries. Add a context here — nowhere else — when
|
||||||
|
// scaffolding one (see `gen:context`, WP-44).
|
||||||
|
module.exports = require('./.dependency-cruiser.base.js')(
|
||||||
|
{
|
||||||
|
auth: [],
|
||||||
|
registratie: [],
|
||||||
|
herregistratie: ['registratie'], // the one sanctioned cross-feature edge
|
||||||
|
brief: [],
|
||||||
|
showcase: null, // unrestricted — the sanctioned teaching page; nothing imports it
|
||||||
|
},
|
||||||
|
'ssp',
|
||||||
|
'apps/ssp/tsconfig.json',
|
||||||
|
);
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
.git
|
.git
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
storybook-static
|
storybook-static*
|
||||||
backend/**/bin
|
backend/**/bin
|
||||||
backend/**/obj
|
backend/**/obj
|
||||||
backend/**/bigregister.db*
|
backend/**/bigregister.db*
|
||||||
|
|||||||
+13
-11
@@ -35,16 +35,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
frontend:
|
frontend:
|
||||||
- 'src/**'
|
- 'apps/**'
|
||||||
|
- 'libs/**'
|
||||||
- 'public/**'
|
- 'public/**'
|
||||||
- 'e2e/**'
|
- 'e2e/**'
|
||||||
- 'scripts/**'
|
- 'scripts/**'
|
||||||
- 'angular.json'
|
- 'angular.json'
|
||||||
- 'package*.json'
|
- 'package*.json'
|
||||||
- 'tsconfig*.json'
|
- 'tsconfig*.json'
|
||||||
- '.storybook/**'
|
- '.storybook*/**'
|
||||||
- 'eslint.config.mjs'
|
- 'eslint.config.mjs'
|
||||||
- '.dependency-cruiser.js'
|
- '.dependency-cruiser*.js'
|
||||||
- '.github/workflows/**'
|
- '.github/workflows/**'
|
||||||
backend:
|
backend:
|
||||||
- 'backend/**'
|
- 'backend/**'
|
||||||
@@ -104,16 +105,17 @@ jobs:
|
|||||||
- run: npm run dep:check
|
- run: npm run dep:check
|
||||||
if: needs.changes.outputs.frontend == 'true'
|
if: needs.changes.outputs.frontend == 'true'
|
||||||
# Showcase snippets must match their real source regions (WP-39, no drift).
|
# Showcase snippets must match their real source regions (WP-39, no drift).
|
||||||
- run: npm run gen:snippets && git diff --exit-code src/app/showcase/snippets.generated.ts
|
- run: npm run gen:snippets && git diff --exit-code apps/ssp/src/app/showcase/snippets.generated.ts
|
||||||
if: needs.changes.outputs.frontend == 'true'
|
if: needs.changes.outputs.frontend == 'true'
|
||||||
# Runs the full suite AND reports coverage (WP-46, report-only — no thresholds, so
|
# Runs the full suite (both apps + both shared libraries, WP-67) AND reports coverage
|
||||||
# it can't fail on coverage; it still fails on a failing test, like `npm test` did).
|
# (WP-46, report-only — no thresholds, so it can't fail on coverage; it still fails on
|
||||||
|
# a failing test, like `npm test` did).
|
||||||
- run: npm run test:coverage
|
- run: npm run test:coverage
|
||||||
if: needs.changes.outputs.frontend == 'true'
|
if: needs.changes.outputs.frontend == 'true'
|
||||||
# --localize builds every configured locale (nl + en, angular.json's i18n
|
# --localize builds every configured locale (nl + en, angular.json's i18n block) in one
|
||||||
# block) in one pass; i18nMissingTranslation:"error" (angular.json) fails
|
# pass per app; i18nMissingTranslation:"error" (angular.json) fails this step if either
|
||||||
# this step if messages.en.xlf is missing a unit the source (WP-20) gains.
|
# app's messages.en.xlf is missing a unit its source (WP-20) or libs/shared gains.
|
||||||
- run: npx ng build --localize
|
- run: npx ng build ssp --localize && npx ng build behandelportal --localize
|
||||||
if: needs.changes.outputs.frontend == 'true'
|
if: needs.changes.outputs.frontend == 'true'
|
||||||
# The shipped bundle must stay clean; dev-only advisories are excluded.
|
# The shipped bundle must stay clean; dev-only advisories are excluded.
|
||||||
- run: npm audit --omit=dev
|
- run: npm audit --omit=dev
|
||||||
@@ -300,5 +302,5 @@ jobs:
|
|||||||
if: (needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true') && steps.node-modules-cache.outputs.cache-hit != 'true'
|
if: (needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true') && steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||||
- run: npm run gen:api
|
- run: npm run gen:api
|
||||||
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
|
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
|
||||||
- run: git diff --exit-code src/app/shared/infrastructure/api-client.ts backend/swagger.json
|
- run: git diff --exit-code libs/shared/src/infrastructure/api-client.ts backend/swagger.json
|
||||||
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
|
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
|
||||||
|
|||||||
+2
-1
@@ -44,7 +44,8 @@ __screenshots__/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
*storybook.log
|
*storybook.log
|
||||||
storybook-static
|
storybook-static*
|
||||||
|
documentation.json
|
||||||
|
|
||||||
# Playwright e2e
|
# Playwright e2e
|
||||||
/test-results
|
/test-results
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# Build output & caches
|
# Build output & caches
|
||||||
dist/
|
dist/
|
||||||
storybook-static/
|
storybook-static*/
|
||||||
coverage/
|
coverage/
|
||||||
.angular/
|
.angular/
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ package-lock.json
|
|||||||
|
|
||||||
# Generated — owned by their generators, not prettier
|
# Generated — owned by their generators, not prettier
|
||||||
documentation.json
|
documentation.json
|
||||||
src/app/shared/infrastructure/api-client.ts
|
libs/shared/src/infrastructure/api-client.ts
|
||||||
src/app/showcase/snippets.generated.ts
|
apps/ssp/src/app/showcase/snippets.generated.ts
|
||||||
|
|
||||||
# Vendored design system (CIBG Huisstijl)
|
# Vendored design system (CIBG Huisstijl)
|
||||||
public/cibg-huisstijl/
|
public/cibg-huisstijl/
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/angular';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
|
// WP-67: see .storybook-ssp/main.ts's comment for why this is a separate config dir.
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: [
|
||||||
|
'../apps/behandelportal/src/**/*.mdx',
|
||||||
|
'../libs/shared/**/*.mdx',
|
||||||
|
'../libs/beheer/**/*.mdx',
|
||||||
|
'../apps/behandelportal/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||||
|
'../libs/shared/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||||
|
'../libs/beheer/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||||
|
],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-a11y',
|
||||||
|
{
|
||||||
|
name: '@storybook/addon-docs',
|
||||||
|
options: { mdxPluginOptions: { mdxCompileOptions: { remarkPlugins: [remarkGfm] } } },
|
||||||
|
},
|
||||||
|
'@storybook/addon-onboarding',
|
||||||
|
],
|
||||||
|
framework: '@storybook/angular',
|
||||||
|
staticDirs: ['../public'],
|
||||||
|
webpackFinal: async (config) => {
|
||||||
|
config.optimization = { ...config.optimization, nodeEnv: false };
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Preview } from '@storybook/angular';
|
||||||
|
import { componentWrapperDecorator } from '@storybook/angular';
|
||||||
|
import { setCompodocJson } from '@storybook/addon-docs/angular';
|
||||||
|
import docJson from './documentation.json';
|
||||||
|
setCompodocJson(docJson);
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') document.body.classList.add('brand--cibg');
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
decorators: [componentWrapperDecorator((story) => `<div style="padding:1.5rem">${story}</div>`)],
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
a11y: {
|
||||||
|
config: { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] } },
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Sidebar tells the DDD seam: reusable design system, then domain contexts. The
|
||||||
|
// "Foundations" MDX pages live in libs/shared/docs and apply to either app's Storybook.
|
||||||
|
options: {
|
||||||
|
storySort: {
|
||||||
|
order: [
|
||||||
|
'Foundations',
|
||||||
|
[
|
||||||
|
'Overview',
|
||||||
|
'Learning Path',
|
||||||
|
'Domain-Driven Design',
|
||||||
|
'Atomic Design',
|
||||||
|
'FP in the UI',
|
||||||
|
'State Machines (TEA)',
|
||||||
|
'RemoteData & Async',
|
||||||
|
"Parse, don't validate",
|
||||||
|
'Design Tokens',
|
||||||
|
'CIBG Gap Register',
|
||||||
|
'Accessibility',
|
||||||
|
'Testing strategy',
|
||||||
|
'BDD',
|
||||||
|
'Internationalization',
|
||||||
|
],
|
||||||
|
'Design System',
|
||||||
|
['Atoms', 'Molecules', 'Organisms', 'Templates', 'Devtools'],
|
||||||
|
'Domein',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// This tsconfig is used by Compodoc to generate the documentation for the project.
|
||||||
|
// If Compodoc is not used, this file can be deleted.
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": [
|
||||||
|
"../apps/behandelportal/src/test.ts",
|
||||||
|
"../apps/behandelportal/src/**/*.spec.ts",
|
||||||
|
"../apps/behandelportal/src/**/*.stories.ts",
|
||||||
|
"../libs/shared/**/*.spec.ts",
|
||||||
|
"../libs/shared/**/*.stories.ts",
|
||||||
|
"../libs/beheer/**/*.spec.ts",
|
||||||
|
"../libs/beheer/**/*.stories.ts"
|
||||||
|
],
|
||||||
|
"include": ["../apps/behandelportal/src/**/*", "../libs/shared/**/*", "../libs/beheer/**/*"],
|
||||||
|
"files": ["./typings.d.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "../apps/behandelportal/tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node", "@angular/localize"],
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"exclude": ["../apps/behandelportal/src/test.ts", "../apps/behandelportal/src/**/*.spec.ts"],
|
||||||
|
"include": [
|
||||||
|
"../apps/behandelportal/src/**/*.stories.*",
|
||||||
|
"../libs/shared/**/*.stories.*",
|
||||||
|
"../libs/beheer/**/*.stories.*",
|
||||||
|
"./preview.ts"
|
||||||
|
],
|
||||||
|
"files": ["./typings.d.ts"]
|
||||||
|
}
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
import type { StorybookConfig } from '@storybook/angular';
|
import type { StorybookConfig } from '@storybook/angular';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
|
// WP-67: one config dir per app — a single merged tsconfig can't resolve both apps' @auth/*
|
||||||
|
// alias at once (each points at a different physical directory), so this Storybook instance
|
||||||
|
// only ever compiles under the ssp project's own browserTarget/tsconfig (see angular.json's
|
||||||
|
// ssp:storybook target) and only globs ssp's own stories + the shared libraries'.
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
stories: [
|
||||||
|
'../apps/ssp/src/**/*.mdx',
|
||||||
|
'../libs/shared/**/*.mdx',
|
||||||
|
'../libs/beheer/**/*.mdx',
|
||||||
|
'../apps/ssp/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||||
|
'../libs/shared/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||||
|
'../libs/beheer/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||||
|
],
|
||||||
addons: [
|
addons: [
|
||||||
'@storybook/addon-a11y',
|
'@storybook/addon-a11y',
|
||||||
// remark-gfm so GFM pipe tables in *.mdx docs actually render (addon-docs
|
// remark-gfm so GFM pipe tables in *.mdx docs actually render (addon-docs
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- CIBG Huisstijl (customized Bootstrap 5.2), served from the vendored public/ staticDir.
|
||||||
|
Mirrors src/index.html so stories match the app. System font per styles.scss. -->
|
||||||
|
<link rel="stylesheet" href="cibg-huisstijl/css/huisstijl.min.css" />
|
||||||
|
<!-- Letter-rendering contract (WP-24), mirrors index.html. -->
|
||||||
|
<link rel="stylesheet" href="letter.css" />
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Preview } from '@storybook/angular';
|
import type { Preview } from '@storybook/angular';
|
||||||
import { componentWrapperDecorator } from '@storybook/angular';
|
import { componentWrapperDecorator } from '@storybook/angular';
|
||||||
import { setCompodocJson } from '@storybook/addon-docs/angular';
|
import { setCompodocJson } from '@storybook/addon-docs/angular';
|
||||||
import docJson from '../documentation.json';
|
import docJson from './documentation.json';
|
||||||
setCompodocJson(docJson);
|
setCompodocJson(docJson);
|
||||||
|
|
||||||
// Activate CIBG's official palette in the story iframe. The override is `body.brand--cibg`,
|
// Activate CIBG's official palette in the story iframe. The override is `body.brand--cibg`,
|
||||||
@@ -26,7 +26,7 @@ const preview: Preview = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Sidebar tells the DDD seam: reusable design system, then domain contexts.
|
// Sidebar tells the DDD seam: reusable design system, then domain contexts.
|
||||||
// See src/docs/layers.mdx.
|
// See libs/shared/docs/layers.mdx.
|
||||||
options: {
|
options: {
|
||||||
storySort: {
|
storySort: {
|
||||||
order: [
|
order: [
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { TestRunnerConfig } from '@storybook/test-runner';
|
||||||
|
import { getStoryContext } from '@storybook/test-runner';
|
||||||
|
import { injectAxe, checkA11y } from 'axe-playwright';
|
||||||
|
|
||||||
|
const config: TestRunnerConfig = {
|
||||||
|
async preVisit(page) {
|
||||||
|
await injectAxe(page);
|
||||||
|
},
|
||||||
|
async postVisit(page, context) {
|
||||||
|
const storyContext = await getStoryContext(page, context);
|
||||||
|
if (storyContext.parameters?.a11y?.disable) return;
|
||||||
|
|
||||||
|
await checkA11y(page, '#storybook-root', {
|
||||||
|
axeOptions: storyContext.parameters?.a11y?.config,
|
||||||
|
detailedReport: true,
|
||||||
|
detailedReportOptions: { html: true },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -3,8 +3,16 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
// Exclude all files that are not needed for documentation generation.
|
// Exclude all files that are not needed for documentation generation.
|
||||||
"exclude": ["../src/test.ts", "../src/**/*.spec.ts", "../src/**/*.stories.ts"],
|
"exclude": [
|
||||||
|
"../apps/ssp/src/test.ts",
|
||||||
|
"../apps/ssp/src/**/*.spec.ts",
|
||||||
|
"../apps/ssp/src/**/*.stories.ts",
|
||||||
|
"../libs/shared/**/*.spec.ts",
|
||||||
|
"../libs/shared/**/*.stories.ts",
|
||||||
|
"../libs/beheer/**/*.spec.ts",
|
||||||
|
"../libs/beheer/**/*.stories.ts"
|
||||||
|
],
|
||||||
// Please make sure to include all files from which Compodoc should generate documentation.
|
// Please make sure to include all files from which Compodoc should generate documentation.
|
||||||
"include": ["../src/**/*"],
|
"include": ["../apps/ssp/src/**/*", "../libs/shared/**/*", "../libs/beheer/**/*"],
|
||||||
"files": ["./typings.d.ts"]
|
"files": ["./typings.d.ts"]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "../apps/ssp/tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node", "@angular/localize"],
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"exclude": ["../apps/ssp/src/test.ts", "../apps/ssp/src/**/*.spec.ts"],
|
||||||
|
"include": [
|
||||||
|
"../apps/ssp/src/**/*.stories.*",
|
||||||
|
"../libs/shared/**/*.stories.*",
|
||||||
|
"../libs/beheer/**/*.stories.*",
|
||||||
|
"./preview.ts"
|
||||||
|
],
|
||||||
|
"files": ["./typings.d.ts"]
|
||||||
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.md' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.app.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["node", "@angular/localize"],
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"moduleResolution": "bundler"
|
|
||||||
},
|
|
||||||
"exclude": ["../src/test.ts", "../src/**/*.spec.ts"],
|
|
||||||
"include": ["../src/**/*.stories.*", "./preview.ts"],
|
|
||||||
"files": ["./typings.d.ts"]
|
|
||||||
}
|
|
||||||
@@ -14,20 +14,34 @@ typed client. The FE renders the backend's decisions. Reference data mimicking
|
|||||||
BRP/DUO (`Data/SeedData.cs`) is in-memory; applications, documents and the brief
|
BRP/DUO (`Data/SeedData.cs`) is in-memory; applications, documents and the brief
|
||||||
persist to a SQLite file via EF Core (WP-22) — `docs/project/backlog/WP-22-durable-persistence.md`.
|
persist to a SQLite file via EF Core (WP-22) — `docs/project/backlog/WP-22-durable-persistence.md`.
|
||||||
|
|
||||||
|
**Monorepo (WP-67):** two Angular projects share one backend + one shared library —
|
||||||
|
`apps/ssp` (Zorgverlener self-service, this doc's main subject) and `apps/behandelportal`
|
||||||
|
(Behandelaar backoffice, ADR-0002). Both import `libs/shared` (design system + kernel +
|
||||||
|
generated API client) and `libs/beheer` (the admin/stamdata context, used identically by
|
||||||
|
both). `backend/` is unowned by either — a genuinely shared dependency.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start # ng serve (proxies /api → backend) → http://localhost:4200
|
npm start # ng serve ssp (proxies /api → backend) → http://localhost:4200
|
||||||
npm test # vitest
|
npm run start:behandelportal # ng serve behandelportal → http://localhost:4201
|
||||||
npm run lint # eslint — enforces `any`-free code + import/layer boundaries
|
npm test # vitest — both apps + both shared libraries (ssp, behandelportal, shared, beheer)
|
||||||
npm run build # ng build (must stay green)
|
npm run lint # eslint — enforces `any`-free code + import/layer boundaries
|
||||||
npm run storybook # component library by atomic layer
|
npm run build # ng build ssp && ng build behandelportal (must stay green)
|
||||||
npm run gen:api # regenerate the typed client from the backend OpenAPI doc
|
npm run storybook # ssp's component library by atomic layer
|
||||||
npm run ci # run the CI gate locally BEFORE pushing (mirrors ci.yml); `npm run ci --full` adds storybook-a11y
|
npm run storybook:behandelportal # behandelportal's own instance (see "Monorepo" note below)
|
||||||
docker compose up # run FE + backend together (Swagger at :5000/swagger)
|
npm run gen:api # regenerate the ONE typed client (libs/shared) from the backend OpenAPI doc
|
||||||
cd backend && dotnet test # backend rule + endpoint tests
|
npm run ci # run the CI gate locally BEFORE pushing (mirrors ci.yml); `npm run ci --full` adds storybook-a11y
|
||||||
|
docker compose up # run both FE apps + backend together (Swagger at :5000/swagger)
|
||||||
|
cd backend && dotnet test # backend rule + endpoint tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Two Storybook instances, not one:** `apps/ssp` and `apps/behandelportal` each have their own
|
||||||
|
`auth` context at the same `@auth/*` alias pointing at different physical directories — a single
|
||||||
|
merged tsconfig can't resolve both at once, so `.storybook-ssp/` and `.storybook-behandelportal/`
|
||||||
|
are separate config dirs (`npm run storybook[:behandelportal]` / `build-storybook[:behandelportal]`),
|
||||||
|
each globbing its own app's stories + both shared libraries'.
|
||||||
|
|
||||||
**Run `npm run ci` before every push** (`scripts/ci-local.sh`) — it runs the same jobs
|
**Run `npm run ci` before every push** (`scripts/ci-local.sh`) — it runs the same jobs
|
||||||
Gitea CI does (lint, format:check, check:tokens, test, `ng build --localize`, audit, backend
|
Gitea CI does (lint, format:check, check:tokens, test, `ng build --localize`, audit, backend
|
||||||
format+test, api-client drift), so a red build is caught locally. Two ways to make it
|
format+test, api-client drift), so a red build is caught locally. Two ways to make it
|
||||||
@@ -65,9 +79,17 @@ session protocol is the worked example of this in practice.
|
|||||||
|
|
||||||
### 1. DDD: contexts then layers, dependencies point inward
|
### 1. DDD: contexts then layers, dependencies point inward
|
||||||
|
|
||||||
`src/app/<context>/<layer>/`. Contexts: `shared`, `auth`, `registratie`,
|
`apps/<app>/src/app/<context>/<layer>/` for an app-local context; `libs/<lib>/src/<layer>/`
|
||||||
`herregistratie`, `brief` (letter-composition teaching slice), `showcase` (teaching
|
for a cross-app library (WP-67). Two apps today: `apps/ssp` (Zorgverlener self-service —
|
||||||
page, not a feature; **sanctioned** to read every context — nothing imports it).
|
contexts `auth`, `registratie`, `herregistratie`, `brief` (letter-composition teaching
|
||||||
|
slice), `showcase` (teaching page, not a feature; **sanctioned** to read every context in
|
||||||
|
its own app — nothing imports it)) and `apps/behandelportal` (Behandelaar backoffice,
|
||||||
|
ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
|
||||||
|
design system + kernel + generated API client — no business logic) and `libs/beheer` (the
|
||||||
|
admin/stamdata context, identical for both apps today — WP-67 folded a silently-diverging
|
||||||
|
duplicate copy back into one). `auth` is deliberately **not** shared even though today it's
|
||||||
|
near-identical in both apps — ADR-0002 models Zorgverlener/Medewerker as different
|
||||||
|
`Principal` variants with different login flows; the two copies are expected to diverge.
|
||||||
|
|
||||||
| Layer | Job | Angular allowed? |
|
| Layer | Job | Angular allowed? |
|
||||||
| ----------------- | ----------------------------------------- | -------------------------------- |
|
| ----------------- | ----------------------------------------- | -------------------------------- |
|
||||||
@@ -77,17 +99,28 @@ page, not a feature; **sanctioned** to read every context — nothing imports it
|
|||||||
| `contracts/` | wire DTOs (the FE⇄BE seam) | no |
|
| `contracts/` | wire DTOs (the FE⇄BE seam) | no |
|
||||||
| `ui/` | how it looks (components, pages) | yes |
|
| `ui/` | how it looks (components, pages) | yes |
|
||||||
|
|
||||||
**Dependencies only point inward**: `ui → application → domain`; everyone may use
|
**Dependencies only point inward**: `ui → application → domain`; every context in either
|
||||||
`shared`; never the reverse. `ui`/`layout` never import `infrastructure` directly
|
app may use `libs/shared` and `libs/beheer`; never the reverse (`libs/shared` may not
|
||||||
(reach data through an application store/command) — lint-enforced. Cross-context only
|
depend on `libs/beheer` either — it stays the base). `ui`/`layout` never import
|
||||||
`herregistratie → registratie → shared`, `auth → shared`, `brief → shared`. Imports use
|
`infrastructure` directly (reach data through an application store/command) —
|
||||||
aliases as direction statements: `@shared/* @auth/* @registratie/* @herregistratie/*
|
lint-enforced (per app, since each app is cruised against its own tsconfig — WP-67's
|
||||||
@brief/*`. `domain/` imports nothing from Angular.
|
`.dependency-cruiser.base.js` + one thin `.dependency-cruiser.<app>.js` per app). An app
|
||||||
|
may not import the other app's source directly. Cross-context only
|
||||||
|
`herregistratie → registratie → libs/shared|beheer`, `auth → libs/shared|beheer`,
|
||||||
|
`brief → libs/shared|beheer` (ssp); `behandeling → libs/shared|beheer`, `auth →
|
||||||
|
libs/shared|beheer` (behandelportal). Imports use aliases as direction statements:
|
||||||
|
`@shared/* @beheer/* @auth/* @registratie/* @herregistratie/* @brief/*` (ssp) —
|
||||||
|
`@shared/* @beheer/* @auth/* @behandeling/*` (behandelportal); each app's own
|
||||||
|
`tsconfig.json` declares its full map (the root `tsconfig.json` intentionally has no
|
||||||
|
`paths` — see its comment). `domain/` imports nothing from Angular.
|
||||||
|
|
||||||
### 2. Atomic design: folder = layer
|
### 2. Atomic design: folder = layer
|
||||||
|
|
||||||
`shared/ui` atoms → molecules → organisms; `shared/layout` templates (`shell`,
|
`libs/shared/ui` atoms → molecules → organisms; `libs/shared/layout` templates (`shell`,
|
||||||
`page-shell`); context `ui/` pages. Each level only uses levels below. A new page
|
`page-shell`); each app's own context `ui/` pages. Each level only uses levels below,
|
||||||
|
and a shared component takes nav/copy as `input()`s or an injection token (e.g.
|
||||||
|
`HEADER_NAV_ITEMS`/`HEADER_ADMIN_LINKS`, `DEBUG_PANEL` in `shell.component.ts`) rather
|
||||||
|
than hardcoding one app's content — the two apps' primary nav genuinely differs. A new page
|
||||||
should be **composition of existing blocks** — adding building blocks is the
|
should be **composition of existing blocks** — adding building blocks is the
|
||||||
exception, not the default. Atoms are thin wrappers over CIBG Huisstijl (Bootstrap 5.2)
|
exception, not the default. Atoms are thin wrappers over CIBG Huisstijl (Bootstrap 5.2)
|
||||||
CSS classes (`btn`, `form-control`, `card`, …); we own only a small typed `input()` API,
|
CSS classes (`btn`, `form-control`, `card`, …); we own only a small typed `input()` API,
|
||||||
@@ -97,11 +130,11 @@ small hand-rolled surface built from the token bridge; see ADR-0003.)
|
|||||||
### 3. State: make illegal states unrepresentable
|
### 3. State: make illegal states unrepresentable
|
||||||
|
|
||||||
Default reflex — **if you're about to add a second/third boolean to track state,
|
Default reflex — **if you're about to add a second/third boolean to track state,
|
||||||
model a discriminated union instead.** Three tools, all in `shared/application`:
|
model a discriminated union instead.** Three tools, all in `libs/shared/src/application`:
|
||||||
|
|
||||||
- **`RemoteData<E,T>`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`.
|
- **`RemoteData<E,T>`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`.
|
||||||
Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success).
|
Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success).
|
||||||
Render it via the `<app-async>` molecule (`shared/ui/async`) — one of four
|
Render it via the `<app-async>` molecule (`libs/shared/src/ui/async`) — one of four
|
||||||
templates, mutually exclusive by construction. Default loading spinner/skeleton
|
templates, mutually exclusive by construction. Default loading spinner/skeleton
|
||||||
is delay-gated (~250ms) so fast connections don't flash.
|
is delay-gated (~250ms) so fast connections don't flash.
|
||||||
- **Elm-style store** (`store.ts` → `createStore(initial, reduce)`) — all state in
|
- **Elm-style store** (`store.ts` → `createStore(initial, reduce)`) — all state in
|
||||||
@@ -116,8 +149,9 @@ model a discriminated union instead.** Three tools, all in `shared/application`:
|
|||||||
see `upload.machine.ts`) — prefixing there avoids alias noise at the
|
see `upload.machine.ts`) — prefixing there avoids alias noise at the
|
||||||
composition site.
|
composition site.
|
||||||
- **`Result<E,T>` + value objects** ("parse, don't validate") — raw input becomes a
|
- **`Result<E,T>` + value objects** ("parse, don't validate") — raw input becomes a
|
||||||
branded type only via a parser returning `Result` (`registratie/domain/value-objects/`:
|
branded type only via a parser returning `Result` (ssp's
|
||||||
`Postcode`, `Uren`, `BigNummer`). Once you hold the type, never re-check it.
|
`registratie/domain/value-objects/`: `Postcode`, `Uren`, `BigNummer`). Once you hold
|
||||||
|
the type, never re-check it.
|
||||||
|
|
||||||
**Derive, don't store** what you can compute — e.g. the wizard's visible steps are
|
**Derive, don't store** what you can compute — e.g. the wizard's visible steps are
|
||||||
`visibleSteps(answers)`, not a stored field (`intake.machine.ts`).
|
`visibleSteps(answers)`, not a stored field (`intake.machine.ts`).
|
||||||
@@ -158,12 +192,15 @@ fails CI, never prod) — never runtime-editable. Org-templates are the delibera
|
|||||||
Vitest. Co-locate `*.spec.ts` next to the unit. **Domain and pure logic must have a
|
Vitest. Co-locate `*.spec.ts` next to the unit. **Domain and pure logic must have a
|
||||||
spec** (reducers, combinators, `visibleSteps`, parsers, boundary `parse*` adapters).
|
spec** (reducers, combinators, `visibleSteps`, parsers, boundary `parse*` adapters).
|
||||||
Test the pure function directly — no Angular TestBed for domain. UI is exercised via
|
Test the pure function directly — no Angular TestBed for domain. UI is exercised via
|
||||||
Storybook stories (`*.stories.ts` co-located, a11y addon on), not heavy component tests.
|
Storybook stories (`*.stories.ts` co-located, a11y addon on), not heavy component tests —
|
||||||
**Story titles mirror the sidebar's Design System/Domein split** (see
|
each app has its **own Storybook instance** (`.storybook-ssp/`, `.storybook-behandelportal/`,
|
||||||
`src/docs/layers.mdx`): a `shared/ui`/`shared/layout` component is titled
|
WP-67 — a single merged tsconfig can't resolve both apps' `@auth/*` at once), each globbing
|
||||||
`Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`; a component in a
|
its own app's stories plus both shared libraries'. **Story titles mirror the sidebar's
|
||||||
context's `ui/` is titled `Domein/<Context>/<Name>` — full stop, regardless of which
|
Design System/Domein split** (see `libs/shared/docs/layers.mdx`): a `libs/shared/ui|layout`
|
||||||
atomic layer it is (a context organism doesn't get its own `Organisms/` bucket).
|
or `libs/beheer/ui` component is titled `Design System/<Atoms|Molecules|Organisms|Templates|Devtools>/<Name>`;
|
||||||
|
a component in an app context's `ui/` is titled `Domein/<Context>/<Name>` — full stop,
|
||||||
|
regardless of which atomic layer it is (a context organism doesn't get its own
|
||||||
|
`Organisms/` bucket).
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
@@ -181,7 +218,7 @@ atomic layer it is (a context organism doesn't get its own `Organisms/` bucket).
|
|||||||
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
|
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
|
||||||
translation file, not a code change (the seam). Shared/English components must **not**
|
translation file, not a code change (the seam). Shared/English components must **not**
|
||||||
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
|
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
|
||||||
supplies the text (see `shared/ui/async`). Format-validation messages in
|
supplies the text (see `libs/shared/src/ui/async`). Format-validation messages in
|
||||||
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
|
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
|
||||||
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
|
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
|
||||||
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
|
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
|
||||||
@@ -190,12 +227,13 @@ atomic layer it is (a context organism doesn't get its own `Organisms/` bucket).
|
|||||||
- **Dates: `DatePipe` in templates, `formatDatumNl` in pure TS.** A template formats a
|
- **Dates: `DatePipe` in templates, `formatDatumNl` in pure TS.** A template formats a
|
||||||
date with Angular's `DatePipe` (`| date: 'longDate'`); pure TS that can't reach a pipe
|
date with Angular's `DatePipe` (`| date: 'longDate'`); pure TS that can't reach a pipe
|
||||||
(a domain function, a `$localize` string) uses the one hand-written
|
(a domain function, a `$localize` string) uses the one hand-written
|
||||||
`formatDatumNl` (`shared/kernel/datum.ts`). Never a third hand-rolled
|
`formatDatumNl` (`libs/shared/src/kernel/datum.ts`). Never a third hand-rolled
|
||||||
`toLocaleDateString` call.
|
`toLocaleDateString` call.
|
||||||
- Routes: lazy `loadComponent`, persistent `ShellComponent` parent, `canActivate:
|
- Routes: lazy `loadComponent`, persistent `ShellComponent` parent (`libs/shared`),
|
||||||
[authGuard]` on protected routes (`app.routes.ts`).
|
`canActivate: [authGuard]` on protected routes (each app's own `app.routes.ts`).
|
||||||
- Theming: CIBG Huisstijl (a customized Bootstrap 5.2 build) is vendored under
|
- Theming: CIBG Huisstijl (a customized Bootstrap 5.2 build) is vendored under
|
||||||
`public/cibg-huisstijl/` and loaded via a `<link>` in `index.html`; `src/styles.scss` holds a
|
`public/cibg-huisstijl/` and loaded via a `<link>` in each app's `index.html`;
|
||||||
|
`libs/shared/styles.scss` (one copy, both apps' `angular.json` point at it — WP-67) holds a
|
||||||
**token bridge** mapping the app's `--rhc-*` token vocabulary onto CIBG/`--bs-*` values (so
|
**token bridge** mapping the app's `--rhc-*` token vocabulary onto CIBG/`--bs-*` values (so
|
||||||
components keep referencing tokens). System-font stack (licensed RO/Rijks fonts not shipped). See ADR-0003.
|
components keep referencing tokens). System-font stack (licensed RO/Rijks fonts not shipped). See ADR-0003.
|
||||||
- Scenario toggle (**dev-only**, not wired in prod builds): `?scenario=slow|loading|empty|error`
|
- Scenario toggle (**dev-only**, not wired in prod builds): `?scenario=slow|loading|empty|error`
|
||||||
@@ -209,18 +247,22 @@ atomic layer it is (a context organism doesn't get its own `Organisms/` bucket).
|
|||||||
`/beheer/stamdata`, `/beheer/zaken`, `/beheer/audit`, `/beheer/functies`.
|
`/beheer/stamdata`, `/beheer/zaken`, `/beheer/audit`, `/beheer/functies`.
|
||||||
- Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`,
|
- Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`,
|
||||||
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
|
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
|
||||||
- **Enforced, not just hoped-for:** `npm run lint` (`eslint.config.mjs`) fails the build
|
- **Enforced, not just hoped-for:** `npm run lint` (`eslint.config.mjs`, scoped to
|
||||||
on `any` and on illegal imports — `domain/` importing Angular, or a context importing
|
`{apps,libs}/**`) fails the build on `any`; `npm run dep:check`
|
||||||
"upward" (the `herregistratie → registratie → shared`, `auth → shared` direction).
|
(`.dependency-cruiser.base.js` + one `.dependency-cruiser.<app>.js` per app, WP-67) fails
|
||||||
CI (`.github/workflows/ci.yml`) runs lint + `check:tokens` + test + build, backend
|
on illegal imports — `domain/` importing Angular, a context importing "upward" (the
|
||||||
`dotnet test`, and an API-client drift check.
|
`herregistratie → registratie → shared`, `auth → shared` direction), an app importing the
|
||||||
|
other app's source, or `libs/shared` depending on `libs/beheer`. CI
|
||||||
|
(`.github/workflows/ci.yml`) runs lint + `dep:check` + `check:tokens` + test (both apps +
|
||||||
|
both libraries) + build (both apps), backend `dotnet test`, and an API-client drift check
|
||||||
|
(one generated client, `libs/shared/src/infrastructure/api-client.ts`).
|
||||||
|
|
||||||
## Adding a feature (recipe)
|
## Adding a feature (recipe)
|
||||||
|
|
||||||
Domain first (types + pure rules + spec, no Angular) → infrastructure (adapter:
|
Domain first (types + pure rules + spec, no Angular) → infrastructure (adapter:
|
||||||
`httpResource` or command returning `Result`) → application (store if shared state;
|
`httpResource` or command returning `Result`) → application (store if shared state;
|
||||||
union + pure reduce) → UI last (compose `shared/ui` atoms, wrap async in `<app-async>`,
|
union + pure reduce) → UI last (compose `libs/shared/ui` atoms, wrap async in
|
||||||
dispatch messages). Worked example: the intake wizard (`herregistratie/`).
|
`<app-async>`, dispatch messages). Worked example: the SSP's intake wizard (`herregistratie/`).
|
||||||
|
|
||||||
The recipes are also invocable skills in `.claude/skills/`: `new-feature`,
|
The recipes are also invocable skills in `.claude/skills/`: `new-feature`,
|
||||||
`new-context`, `value-object`, `form-machine`, `bff-endpoint`, `mutation-command`,
|
`new-context`, `value-object`, `form-machine`, `bff-endpoint`, `mutation-command`,
|
||||||
|
|||||||
+228
-20
@@ -7,21 +7,21 @@
|
|||||||
},
|
},
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"atomic-design-poc": {
|
"ssp": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
"style": "scss"
|
"style": "scss"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "",
|
"root": "apps/ssp",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "apps/ssp/src",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"i18n": {
|
"i18n": {
|
||||||
"sourceLocale": { "code": "nl", "subPath": "" },
|
"sourceLocale": { "code": "nl", "subPath": "" },
|
||||||
"locales": {
|
"locales": {
|
||||||
"en": {
|
"en": {
|
||||||
"translation": "src/locale/messages.en.xlf"
|
"translation": "apps/ssp/src/locale/messages.en.xlf"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular/build:application",
|
"builder": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"browser": "src/main.ts",
|
"browser": "apps/ssp/src/main.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "apps/ssp/tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"input": "public"
|
"input": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["src/styles.scss"],
|
"styles": ["libs/shared/styles.scss"],
|
||||||
"polyfills": ["@angular/localize/init"],
|
"polyfills": ["@angular/localize/init"],
|
||||||
"i18nMissingTranslation": "error"
|
"i18nMissingTranslation": "error"
|
||||||
},
|
},
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
{
|
{
|
||||||
"replace": "src/environments/environment.ts",
|
"replace": "libs/shared/src/environments/environment.ts",
|
||||||
"with": "src/environments/environment.prod.ts"
|
"with": "libs/shared/src/environments/environment.prod.ts"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -77,21 +77,27 @@
|
|||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
|
"options": {
|
||||||
|
"proxyConfig": "apps/ssp/proxy.conf.json"
|
||||||
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "atomic-design-poc:build:production"
|
"buildTarget": "ssp:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "atomic-design-poc:build:development"
|
"buildTarget": "ssp:build:development"
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"buildTarget": "atomic-design-poc:build:development,en"
|
"buildTarget": "ssp:build:development,en"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular/build:unit-test",
|
"builder": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "apps/ssp/tsconfig.spec.json"
|
||||||
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"coverage": true,
|
"coverage": true,
|
||||||
@@ -100,9 +106,8 @@
|
|||||||
"**/*.spec.ts",
|
"**/*.spec.ts",
|
||||||
"**/*.stories.ts",
|
"**/*.stories.ts",
|
||||||
"**/contracts/**",
|
"**/contracts/**",
|
||||||
"src/app/shared/infrastructure/api-client.ts",
|
"libs/shared/src/infrastructure/api-client.ts",
|
||||||
"src/main.ts",
|
"apps/ssp/src/main.ts",
|
||||||
"src/test-setup.ts",
|
|
||||||
"**/*.d.ts"
|
"**/*.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -111,8 +116,8 @@
|
|||||||
"storybook": {
|
"storybook": {
|
||||||
"builder": "@storybook/angular:start-storybook",
|
"builder": "@storybook/angular:start-storybook",
|
||||||
"options": {
|
"options": {
|
||||||
"configDir": ".storybook",
|
"configDir": ".storybook-ssp",
|
||||||
"browserTarget": "atomic-design-poc:build",
|
"browserTarget": "ssp:build",
|
||||||
"compodoc": false,
|
"compodoc": false,
|
||||||
"port": 6006
|
"port": 6006
|
||||||
}
|
}
|
||||||
@@ -120,14 +125,217 @@
|
|||||||
"build-storybook": {
|
"build-storybook": {
|
||||||
"builder": "@storybook/angular:build-storybook",
|
"builder": "@storybook/angular:build-storybook",
|
||||||
"options": {
|
"options": {
|
||||||
"configDir": ".storybook",
|
"configDir": ".storybook-ssp",
|
||||||
"browserTarget": "atomic-design-poc:build",
|
"browserTarget": "ssp:build",
|
||||||
"compodoc": true,
|
"compodoc": true,
|
||||||
"compodocArgs": ["-e", "json", "-d", "."],
|
"compodocArgs": ["-e", "json", "-d", ".storybook-ssp"],
|
||||||
"outputDir": "storybook-static"
|
"outputDir": "storybook-static"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"behandelportal": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "apps/behandelportal",
|
||||||
|
"sourceRoot": "apps/behandelportal/src",
|
||||||
|
"prefix": "app",
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": { "code": "nl", "subPath": "" },
|
||||||
|
"locales": {
|
||||||
|
"en": {
|
||||||
|
"translation": "apps/behandelportal/src/locale/messages.en.xlf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "apps/behandelportal/src/main.ts",
|
||||||
|
"tsConfig": "apps/behandelportal/tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": ["libs/shared/styles.scss"],
|
||||||
|
"polyfills": ["@angular/localize/init"],
|
||||||
|
"i18nMissingTranslation": "error"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "1.5MB",
|
||||||
|
"maximumError": "2MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all",
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "libs/shared/src/environments/environment.ts",
|
||||||
|
"with": "libs/shared/src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"localize": ["en"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"options": {
|
||||||
|
"port": 4201,
|
||||||
|
"proxyConfig": "apps/behandelportal/proxy.conf.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "behandelportal:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "behandelportal:build:development"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"buildTarget": "behandelportal:build:development,en"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "apps/behandelportal/tsconfig.spec.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"coverage": {
|
||||||
|
"coverage": true,
|
||||||
|
"coverageReporters": ["text-summary", "html", "lcov"],
|
||||||
|
"coverageExclude": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.stories.ts",
|
||||||
|
"**/contracts/**",
|
||||||
|
"libs/shared/src/infrastructure/api-client.ts",
|
||||||
|
"apps/behandelportal/src/main.ts",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"builder": "@storybook/angular:start-storybook",
|
||||||
|
"options": {
|
||||||
|
"configDir": ".storybook-behandelportal",
|
||||||
|
"browserTarget": "behandelportal:build",
|
||||||
|
"compodoc": false,
|
||||||
|
"port": 6007
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build-storybook": {
|
||||||
|
"builder": "@storybook/angular:build-storybook",
|
||||||
|
"options": {
|
||||||
|
"configDir": ".storybook-behandelportal",
|
||||||
|
"browserTarget": "behandelportal:build",
|
||||||
|
"compodoc": true,
|
||||||
|
"compodocArgs": ["-e", "json", "-d", ".storybook-behandelportal"],
|
||||||
|
"outputDir": "storybook-static-behandelportal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shared": {
|
||||||
|
"projectType": "library",
|
||||||
|
"root": "libs/shared",
|
||||||
|
"sourceRoot": "libs/shared/src",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "libs/shared/src/test-entry.ts",
|
||||||
|
"tsConfig": "libs/shared/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development": {}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "libs/shared/tsconfig.spec.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"coverage": {
|
||||||
|
"coverage": true,
|
||||||
|
"coverageReporters": ["text-summary", "html", "lcov"],
|
||||||
|
"coverageExclude": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.stories.ts",
|
||||||
|
"**/contracts/**",
|
||||||
|
"src/infrastructure/api-client.ts",
|
||||||
|
"src/test-entry.ts",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"beheer": {
|
||||||
|
"projectType": "library",
|
||||||
|
"root": "libs/beheer",
|
||||||
|
"sourceRoot": "libs/beheer/src",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "libs/beheer/src/test-entry.ts",
|
||||||
|
"tsConfig": "libs/beheer/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development": {}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "libs/beheer/tsconfig.spec.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"coverage": {
|
||||||
|
"coverage": true,
|
||||||
|
"coverageReporters": ["text-summary", "html", "lcov"],
|
||||||
|
"coverageExclude": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.stories.ts",
|
||||||
|
"**/contracts/**",
|
||||||
|
"src/test-entry.ts",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { SESSION_PORT } from '@shared/application/session.port';
|
|||||||
import { SessionStore } from '@auth/application/session.store';
|
import { SessionStore } from '@auth/application/session.store';
|
||||||
import { provideRouteFocus } from '@shared/layout/route-focus';
|
import { provideRouteFocus } from '@shared/layout/route-focus';
|
||||||
import { provideUnloadFlush } from '@shared/application/pending-saves';
|
import { provideUnloadFlush } from '@shared/application/pending-saves';
|
||||||
|
import { HEADER_ADMIN_LINKS, HEADER_NAV_ITEMS } from '@shared/layout/site-header/nav-config';
|
||||||
|
import { ADMIN_LINKS, NAV_ITEMS } from './shell/nav.config';
|
||||||
|
|
||||||
// Both locales' data so DatePipe/number pipes work for whichever bundle is active.
|
// Both locales' data so DatePipe/number pipes work for whichever bundle is active.
|
||||||
registerLocaleData(localeNl);
|
registerLocaleData(localeNl);
|
||||||
@@ -59,5 +61,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
{ provide: LOCALE_ID, useFactory: () => $localize.locale ?? 'nl' },
|
{ provide: LOCALE_ID, useFactory: () => $localize.locale ?? 'nl' },
|
||||||
provideRouteFocus(),
|
provideRouteFocus(),
|
||||||
provideUnloadFlush(),
|
provideUnloadFlush(),
|
||||||
|
{ provide: HEADER_NAV_ITEMS, useValue: NAV_ITEMS },
|
||||||
|
{ provide: HEADER_ADMIN_LINKS, useValue: ADMIN_LINKS },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { ShellComponent } from '@shared/layout/shell/shell.component';
|
||||||
|
import { authGuard, capabilityGuard } from '@auth/auth.guard';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ShellComponent, // persistent header/footer; only children swap
|
||||||
|
children: [
|
||||||
|
{ path: '', pathMatch: 'full', redirectTo: 'login' },
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
loadComponent: () => import('@auth/ui/login.page').then((m) => m.LoginPage),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
canActivate: [authGuard],
|
||||||
|
// TODO(create-ssp): stopgap landing page — point this at a real overview once you have one.
|
||||||
|
loadComponent: () =>
|
||||||
|
import('@behandeling/ui/behandeling.page').then((m) => m.BehandelingPage),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'beheer/stamdata',
|
||||||
|
// Admin-only stamdata maintenance editor (ADR-0004): capabilityGuard denies-by-default
|
||||||
|
// unless GET /me resolved `stamdata:edit` (Admin role). Backend re-enforces via the
|
||||||
|
// StamdataAdmin gate — the guard just avoids loading a page that would 403.
|
||||||
|
canActivate: [capabilityGuard('stamdata:edit')],
|
||||||
|
loadComponent: () => import('@beheer/ui/stamdata.page').then((m) => m.StamdataPage),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'beheer/audit',
|
||||||
|
// Admin-only authz/PII-reveal audit trail (WP-41/42). capabilityGuard denies-by-default
|
||||||
|
// unless GET /me resolved `cases:manage` (reused for audit read). Backend re-enforces.
|
||||||
|
canActivate: [capabilityGuard('cases:manage')],
|
||||||
|
loadComponent: () => import('@beheer/ui/audit.page').then((m) => m.AuditPage),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'beheer/functies',
|
||||||
|
// Admin-only feature-flag toggles (WP-47), gated by `flags:manage`.
|
||||||
|
canActivate: [capabilityGuard('flags:manage')],
|
||||||
|
loadComponent: () =>
|
||||||
|
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'behandeling',
|
||||||
|
canActivate: [authGuard],
|
||||||
|
loadComponent: () =>
|
||||||
|
import('@behandeling/ui/behandeling.page').then((m) => m.BehandelingPage),
|
||||||
|
},
|
||||||
|
{ path: '**', redirectTo: 'login' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scaffolded by `gen:context` (WP-44) — replace with the `behandeling` context's first
|
||||||
|
* feature slice (the `new-feature` skill: domain first, then infrastructure/application, UI last).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-behandeling-page',
|
||||||
|
imports: [PageShellComponent],
|
||||||
|
template: `
|
||||||
|
<app-page-shell [heading]="heading">
|
||||||
|
<p>{{ intro }}</p>
|
||||||
|
</app-page-shell>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class BehandelingPage {
|
||||||
|
protected heading = $localize`:@@behandeling.landing.heading:Behandeling`;
|
||||||
|
protected intro = $localize`:@@behandeling.landing.intro:Hier komt de eerste behandeling-functionaliteit.`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { AdminLink, HeaderNavItem } from '@shared/layout/site-header/nav-config';
|
||||||
|
|
||||||
|
/** This app's primary nav — provided to the shared site header via HEADER_NAV_ITEMS
|
||||||
|
(see app.config.ts). */
|
||||||
|
export const NAV_ITEMS: readonly HeaderNavItem[] = [
|
||||||
|
{ label: $localize`:@@header.nav.overzicht:Overzicht`, to: '/dashboard' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** This app's admin pages — provided to the shared site header via HEADER_ADMIN_LINKS.
|
||||||
|
No huisstijl (that's the SSP's brief context) or zaken entry — inherited as-is from
|
||||||
|
WP-61's bootstrap trim, not revisited by this migration. */
|
||||||
|
export const ADMIN_LINKS: readonly AdminLink[] = [
|
||||||
|
{
|
||||||
|
label: $localize`:@@header.nav.stamdata:Stamdata`,
|
||||||
|
description: $localize`:@@admin.link.stamdata.desc:Business-tabellen onderhouden`,
|
||||||
|
to: '/beheer/stamdata',
|
||||||
|
cap: 'stamdata:edit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $localize`:@@header.nav.audit:Auditlog`,
|
||||||
|
description: $localize`:@@admin.link.audit.desc:Toegangs- en inzagebeslissingen bekijken`,
|
||||||
|
to: '/beheer/audit',
|
||||||
|
cap: 'cases:manage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $localize`:@@header.nav.functies:Functievlaggen`,
|
||||||
|
description: $localize`:@@admin.link.functies.desc:Functionaliteit aan- of uitzetten`,
|
||||||
|
to: '/beheer/functies',
|
||||||
|
cap: 'flags:manage',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Behandelportal</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
<!-- CIBG Huisstijl (customized Bootstrap 5.2). Loaded as a <link> so its relative
|
||||||
|
url(../fonts|icons|images) refs resolve against the vendored folder at runtime.
|
||||||
|
Licensed Rijksoverheid fonts are not used — styles.scss overrides the stack to system-ui. -->
|
||||||
|
<link rel="stylesheet" href="cibg-huisstijl/css/huisstijl.min.css" />
|
||||||
|
</head>
|
||||||
|
<!-- brand--cibg activates CIBG's official palette: robijn layout chrome + lintblauw accents
|
||||||
|
(without it, --ro-layout falls back to the blue default). -->
|
||||||
|
<body class="brand--cibg">
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,693 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="nl" datatype="plaintext" original="ng2.template">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="form.verplichteVelden" datatype="html">
|
||||||
|
<source>* verplichte velden</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
|
||||||
|
<context context-type="linenumber">15,18</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="login.bsnLabel" datatype="html">
|
||||||
|
<source>BSN</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
|
||||||
|
<context context-type="linenumber">22,23</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="login.bsnDescription" datatype="html">
|
||||||
|
<source>9-cijferig BSN, elfproef-geldig (demo: 123456782)</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
|
||||||
|
<context context-type="linenumber">25,28</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="login.wachtwoordLabel" datatype="html">
|
||||||
|
<source>Wachtwoord</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
|
||||||
|
<context context-type="linenumber">36,37</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="login.submit" datatype="html">
|
||||||
|
<source>Inloggen met DigiD</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
|
||||||
|
<context context-type="linenumber">41,43</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="login.heading" datatype="html">
|
||||||
|
<source>Inloggen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/auth/ui/login.page.ts</context>
|
||||||
|
<context context-type="linenumber">14,16</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="login.intro" datatype="html">
|
||||||
|
<source>Log in op uw persoonlijke BIG-register omgeving.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/auth/ui/login.page.ts</context>
|
||||||
|
<context context-type="linenumber">17,19</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="behandeling.landing.heading" datatype="html">
|
||||||
|
<source>Behandeling</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/behandeling/ui/behandeling.page.ts</context>
|
||||||
|
<context context-type="linenumber">18</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="behandeling.landing.intro" datatype="html">
|
||||||
|
<source>Hier komt de eerste behandeling-functionaliteit.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/behandeling/ui/behandeling.page.ts</context>
|
||||||
|
<context context-type="linenumber">19</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.noTables" datatype="html">
|
||||||
|
<source>Er is geen stamdata om te beheren.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/application/stamdata.store.ts</context>
|
||||||
|
<context context-type="linenumber">150</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.validation.key" datatype="html">
|
||||||
|
<source>Vul de sleutelkolom in.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/domain/stamdata.ts</context>
|
||||||
|
<context context-type="linenumber">68</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.validation.van" datatype="html">
|
||||||
|
<source>Vul een 'geldig van'-datum in.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/domain/stamdata.ts</context>
|
||||||
|
<context context-type="linenumber">72</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.validation.range" datatype="html">
|
||||||
|
<source>'Geldig tot' moet ná 'geldig van' liggen.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/domain/stamdata.ts</context>
|
||||||
|
<context context-type="linenumber">74</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.load.failed" datatype="html">
|
||||||
|
<source>De stamdata kon niet worden geladen.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/infrastructure/stamdata.adapter.ts</context>
|
||||||
|
<context context-type="linenumber">13</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.heading" datatype="html">
|
||||||
|
<source>Auditlog</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">102</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.intro" datatype="html">
|
||||||
|
<source>Toegangs- en inzagebeslissingen (autorisatie en het tonen van afgeschermde gegevens). Vastgelegd zonder persoonsgegevens.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">103</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.denied" datatype="html">
|
||||||
|
<source>U hebt geen rechten om de auditlog te bekijken.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">104</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.failed" datatype="html">
|
||||||
|
<source>De auditlog kon niet worden geladen.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">105</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.empty" datatype="html">
|
||||||
|
<source>Nog geen auditregels.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">106</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.retry" datatype="html">
|
||||||
|
<source>Opnieuw proberen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">107</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.col.tijd" datatype="html">
|
||||||
|
<source>Tijd</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">108</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.col.actie" datatype="html">
|
||||||
|
<source>Actie</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">109</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.col.resource" datatype="html">
|
||||||
|
<source>Resource</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">110</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.col.besluit" datatype="html">
|
||||||
|
<source>Besluit</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">111</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.col.rol" datatype="html">
|
||||||
|
<source>Rol</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">112</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="audit.col.cid" datatype="html">
|
||||||
|
<source>Correlatie-id</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/audit.page.ts</context>
|
||||||
|
<context context-type="linenumber">113</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.heading" datatype="html">
|
||||||
|
<source>Functievlaggen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">82</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.intro" datatype="html">
|
||||||
|
<source>Zet functionaliteit aan of uit tijdens runtime. De catalogus staat vast in code; hier beheert u de status.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">83</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.denied" datatype="html">
|
||||||
|
<source>U hebt geen rechten om functievlaggen te beheren.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">84</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.failed" datatype="html">
|
||||||
|
<source>De functievlaggen konden niet worden geladen.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">85</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.retry" datatype="html">
|
||||||
|
<source>Opnieuw proberen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">86</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.on" datatype="html">
|
||||||
|
<source>Aan</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">87</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.off" datatype="html">
|
||||||
|
<source>Uit</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">88</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.enable" datatype="html">
|
||||||
|
<source>Aanzetten</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">89</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="flags.disable" datatype="html">
|
||||||
|
<source>Uitzetten</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/feature-flags.page.ts</context>
|
||||||
|
<context context-type="linenumber">90</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.added" datatype="html">
|
||||||
|
<source>toegevoegd</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">228</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.edited" datatype="html">
|
||||||
|
<source>gewijzigd</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">229</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.removed" datatype="html">
|
||||||
|
<source>verwijderd</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">230</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.table" datatype="html">
|
||||||
|
<source>Tabel</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">236</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.peildatum" datatype="html">
|
||||||
|
<source>Toon geldig op</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">237</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.showAll" datatype="html">
|
||||||
|
<source>Toon alles</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">238</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.previewNote" datatype="html">
|
||||||
|
<source>Voorbeeld: alleen de rijen die op deze datum geldig zijn. Bewerken staat uit.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">239</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.actions" datatype="html">
|
||||||
|
<source>Acties</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">240</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.remove" datatype="html">
|
||||||
|
<source>Verwijderen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">241</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.expire" datatype="html">
|
||||||
|
<source>Sluiten per vandaag</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">242</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.removeConfirm" datatype="html">
|
||||||
|
<source>Rij verwijderen? Als andere gegevens ernaar verwijzen, faalt de build-controle (CI). Bij een tabel met een geldigheidsperiode kunt u de rij beter sluiten (geldig tot) in plaats van verwijderen.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">243</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.undo" datatype="html">
|
||||||
|
<source>Ongedaan maken</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">257</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.redo" datatype="html">
|
||||||
|
<source>Opnieuw uitvoeren</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">258</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.addRow" datatype="html">
|
||||||
|
<source>Rij toevoegen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">259</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.download" datatype="html">
|
||||||
|
<source>Download JSON</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">260</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.applyHint" datatype="html">
|
||||||
|
<source>Wijzigingen worden als JSON-bestand gedownload en via een pull request toegepast — de build (CI) controleert ze.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata-table-editor/stamdata-table-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">261</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.page.heading" datatype="html">
|
||||||
|
<source>Stamdata onderhouden</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
|
||||||
|
<context context-type="linenumber">72</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.page.intro" datatype="html">
|
||||||
|
<source>Beheer de business-tabellen die de registratie stuurt. Wijzigingen worden als JSON gedownload en via een pull request toegepast; de build blijft de bewaker.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
|
||||||
|
<context context-type="linenumber">73</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.page.denied" datatype="html">
|
||||||
|
<source>U hebt geen rechten om stamdata te onderhouden.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
|
||||||
|
<context context-type="linenumber">74</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.page.failed" datatype="html">
|
||||||
|
<source>De stamdata kon niet worden geladen.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
|
||||||
|
<context context-type="linenumber">75</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="beheer.page.retry" datatype="html">
|
||||||
|
<source>Opnieuw proberen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/beheer/ui/stamdata.page.ts</context>
|
||||||
|
<context context-type="linenumber">76</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="submit.failed" datatype="html">
|
||||||
|
<source>Het indienen is niet gelukt. Probeer het later opnieuw.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/application/submit.ts</context>
|
||||||
|
<context context-type="linenumber">28</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="validation.bsn" datatype="html">
|
||||||
|
<source>Voer een geldig BSN van 9 cijfers in.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
|
||||||
|
<context context-type="linenumber">18</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="validation.bsnElfproef" datatype="html">
|
||||||
|
<source>Dit is geen geldig BSN (klopt niet met de elfproef).</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
|
||||||
|
<context context-type="linenumber">23</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.nav.stamdata" datatype="html">
|
||||||
|
<source>Stamdata</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
|
||||||
|
<context context-type="linenumber">17</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="admin.link.stamdata.desc" datatype="html">
|
||||||
|
<source>Business-tabellen onderhouden</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
|
||||||
|
<context context-type="linenumber">18</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.nav.audit" datatype="html">
|
||||||
|
<source>Auditlog</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
|
||||||
|
<context context-type="linenumber">23</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="admin.link.audit.desc" datatype="html">
|
||||||
|
<source>Toegangs- en inzagebeslissingen bekijken</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
|
||||||
|
<context context-type="linenumber">24</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.nav.functies" datatype="html">
|
||||||
|
<source>Functievlaggen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
|
||||||
|
<context context-type="linenumber">29</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="admin.link.functies.desc" datatype="html">
|
||||||
|
<source>Functionaliteit aan- of uitzetten</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/admin-links.ts</context>
|
||||||
|
<context context-type="linenumber">30</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="crumb.dashboard" datatype="html">
|
||||||
|
<source>Mijn overzicht</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
|
||||||
|
<context context-type="linenumber">12</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="crumb.registratie" datatype="html">
|
||||||
|
<source>Mijn gegevens</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
|
||||||
|
<context context-type="linenumber">13</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="crumb.registreren" datatype="html">
|
||||||
|
<source>Inschrijven</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
|
||||||
|
<context context-type="linenumber">14</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="crumb.herregistratie" datatype="html">
|
||||||
|
<source>Herregistratie</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
|
||||||
|
<context context-type="linenumber">16</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="crumb.intake" datatype="html">
|
||||||
|
<source>Herregistratie-intake</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
|
||||||
|
<context context-type="linenumber">19</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="crumb.concepts" datatype="html">
|
||||||
|
<source>Functionele patronen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb-trail.ts</context>
|
||||||
|
<context context-type="linenumber">20</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="breadcrumb.aria" datatype="html">
|
||||||
|
<source>Kruimelpad</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb.component.ts</context>
|
||||||
|
<context context-type="linenumber">27,28</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="breadcrumb.hier" datatype="html">
|
||||||
|
<source>U bevindt zich hier:</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/breadcrumb/breadcrumb.component.ts</context>
|
||||||
|
<context context-type="linenumber">28,29</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="lang.navLabel" datatype="html">
|
||||||
|
<source>Taal / Language</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/language-switcher/language-switcher.component.ts</context>
|
||||||
|
<context context-type="linenumber">95</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="lang.heading" datatype="html">
|
||||||
|
<source>Kies een taal</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/language-switcher/language-switcher.component.ts</context>
|
||||||
|
<context context-type="linenumber">96</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="pageShell.backLabel" datatype="html">
|
||||||
|
<source>Terug naar overzicht</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/page-shell/page-shell.component.ts</context>
|
||||||
|
<context context-type="linenumber">49</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="shell.skipLink" datatype="html">
|
||||||
|
<source>Naar de inhoud</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/shell/shell.component.ts</context>
|
||||||
|
<context context-type="linenumber">53,54</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.tagline" datatype="html">
|
||||||
|
<source>De Rijksoverheid. Voor Nederland.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">85,86</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.ministry" datatype="html">
|
||||||
|
<source> CIBG — Ministerie van Volksgezondheid, Welzijn en Sport </source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">87,89</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.overSiteAria" datatype="html">
|
||||||
|
<source>Over deze site</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">90,91</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.overSite" datatype="html">
|
||||||
|
<source>Over deze site</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">91,92</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.privacy" datatype="html">
|
||||||
|
<source>Privacy</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">99,101</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.cookies" datatype="html">
|
||||||
|
<source>Cookies</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">108,110</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.toegankelijkheid" datatype="html">
|
||||||
|
<source>Toegankelijkheid</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">117,120</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="footer.demo" datatype="html">
|
||||||
|
<source>Demo / POC — geen echte gegevens.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-footer/site-footer.component.ts</context>
|
||||||
|
<context context-type="linenumber">122,124</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.nav.overzicht" datatype="html">
|
||||||
|
<source>Overzicht</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
|
||||||
|
<context context-type="linenumber">19</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.sender" datatype="html">
|
||||||
|
<source>BIG-register</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
|
||||||
|
<context context-type="linenumber">54,55</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.ministry" datatype="html">
|
||||||
|
<source>Ministerie van Volksgezondheid, Welzijn en Sport</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
|
||||||
|
<context context-type="linenumber">56,58</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.uitloggen" datatype="html">
|
||||||
|
<source> Uitloggen </source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
|
||||||
|
<context context-type="linenumber">78,79</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="header.navAria" datatype="html">
|
||||||
|
<source>Hoofdnavigatie</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/layout/site-header/site-header.component.ts</context>
|
||||||
|
<context context-type="linenumber">86,87</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="alert.icon.info" datatype="html">
|
||||||
|
<source>Informatie</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
|
||||||
|
<context context-type="linenumber">7</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="alert.icon.ok" datatype="html">
|
||||||
|
<source>Gelukt</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
|
||||||
|
<context context-type="linenumber">8</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="alert.icon.warning" datatype="html">
|
||||||
|
<source>Waarschuwing</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
|
||||||
|
<context context-type="linenumber">9</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="alert.icon.error" datatype="html">
|
||||||
|
<source>Foutmelding</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/alert/alert.component.ts</context>
|
||||||
|
<context context-type="linenumber">10</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="async.error" datatype="html">
|
||||||
|
<source>Er ging iets mis bij het laden van de gegevens.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/async/async.component.ts</context>
|
||||||
|
<context context-type="linenumber">105</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="async.retry" datatype="html">
|
||||||
|
<source>Opnieuw proberen</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/async/async.component.ts</context>
|
||||||
|
<context context-type="linenumber">106</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="async.empty" datatype="html">
|
||||||
|
<source>Geen gegevens gevonden.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/async/async.component.ts</context>
|
||||||
|
<context context-type="linenumber">107</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="spinner.aria" datatype="html">
|
||||||
|
<source>Bezig met laden</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/shared/ui/spinner/spinner.component.ts</context>
|
||||||
|
<context context-type="linenumber">36,40</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* Per-app base: adds this app's own alias map on top of the workspace-wide compiler
|
||||||
|
options in the root tsconfig.json. See that file's comment on why paths live here,
|
||||||
|
per-app, instead of at the root. */
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@shared/*": ["../../libs/shared/src/*"],
|
||||||
|
"@beheer/*": ["../../libs/beheer/src/*"],
|
||||||
|
"@auth/*": ["src/app/auth/*"],
|
||||||
|
"@behandeling/*": ["src/app/behandeling/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:5000",
|
||||||
|
"secure": false,
|
||||||
|
"changeOrigin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
ApplicationConfig,
|
||||||
|
LOCALE_ID,
|
||||||
|
isDevMode,
|
||||||
|
provideBrowserGlobalErrorListeners,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { provideRouter, withInMemoryScrolling, withViewTransitions } from '@angular/router';
|
||||||
|
import type { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
|
import { registerLocaleData } from '@angular/common';
|
||||||
|
import localeNl from '@angular/common/locales/nl';
|
||||||
|
import localeEn from '@angular/common/locales/en';
|
||||||
|
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { scenarioInterceptor } from '@shared/infrastructure/scenario.interceptor';
|
||||||
|
import { roleInterceptor } from '@shared/infrastructure/role.interceptor';
|
||||||
|
import { provideApiClient } from '@shared/infrastructure/api-client.provider';
|
||||||
|
import { SESSION_PORT } from '@shared/application/session.port';
|
||||||
|
import { SessionStore } from '@auth/application/session.store';
|
||||||
|
import { provideRouteFocus } from '@shared/layout/route-focus';
|
||||||
|
import { provideUnloadFlush } from '@shared/application/pending-saves';
|
||||||
|
import { HEADER_ADMIN_LINKS, HEADER_NAV_ITEMS } from '@shared/layout/site-header/nav-config';
|
||||||
|
import { DEBUG_PANEL } from '@shared/layout/shell/shell.component';
|
||||||
|
import { ADMIN_LINKS, NAV_ITEMS } from './shell/nav.config';
|
||||||
|
import { DebugStateComponent } from './shell/debug-state/debug-state.component';
|
||||||
|
|
||||||
|
// Both locales' data so DatePipe/number pipes work for whichever bundle is active.
|
||||||
|
registerLocaleData(localeNl);
|
||||||
|
registerLocaleData(localeEn);
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideRouter(
|
||||||
|
routes,
|
||||||
|
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
|
||||||
|
// Cross-fade page-to-page navigations only. A silent same-route nav — e.g.
|
||||||
|
// draft-sync stamping `?aanvraag=<id>` into the URL mid-wizard — must NOT
|
||||||
|
// animate: for the transition's duration Firefox's `::view-transition`
|
||||||
|
// overlay swallows pointer events (Chrome sets pointer-events:none, so it
|
||||||
|
// doesn't), which loses a click landing on it and makes the wizard's "next"
|
||||||
|
// button need a second click. Skip the transition when the route is unchanged.
|
||||||
|
withViewTransitions({
|
||||||
|
onViewTransitionCreated: ({ transition, from, to }) => {
|
||||||
|
// `from`/`to` are the ROOT snapshots (the shared shell), so descend to the
|
||||||
|
// leaf before comparing — otherwise every navigation looks "same route".
|
||||||
|
const leaf = (r: ActivatedRouteSnapshot) => {
|
||||||
|
while (r.firstChild) r = r.firstChild;
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
if (leaf(from).routeConfig === leaf(to).routeConfig) transition.skipTransition();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
// Dev-only: the ?scenario= toggle must never reach a production build, where
|
||||||
|
// a query param could otherwise force errors on the live app.
|
||||||
|
provideHttpClient(withInterceptors(isDevMode() ? [scenarioInterceptor, roleInterceptor] : [])),
|
||||||
|
provideApiClient(),
|
||||||
|
{ provide: SESSION_PORT, useExisting: SessionStore },
|
||||||
|
// Per-bundle locale: the localize build sets `$localize.locale` ('nl'/'en'); the
|
||||||
|
// non-localized dev/source build leaves it undefined → fall back to 'nl'. (Was hardcoded
|
||||||
|
// 'nl', which mis-formatted dates/numbers in the en bundle.)
|
||||||
|
{ provide: LOCALE_ID, useFactory: () => $localize.locale ?? 'nl' },
|
||||||
|
provideRouteFocus(),
|
||||||
|
provideUnloadFlush(),
|
||||||
|
{ provide: HEADER_NAV_ITEMS, useValue: NAV_ITEMS },
|
||||||
|
{ provide: HEADER_ADMIN_LINKS, useValue: ADMIN_LINKS },
|
||||||
|
{ provide: DEBUG_PANEL, useValue: DebugStateComponent },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
template: '<router-outlet />',
|
||||||
|
})
|
||||||
|
export class App {}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable, computed, effect, inject, signal } from '@angular/core';
|
||||||
|
import { Result } from '@shared/kernel/fp';
|
||||||
|
import { Session } from '../domain/session';
|
||||||
|
import { DigidAdapter } from '../infrastructure/digid.adapter';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'session-v1';
|
||||||
|
|
||||||
|
/** Restore a persisted session (best-effort; corrupt entry → logged out).
|
||||||
|
G2: validate the shape before trusting it. G1: the BSN is never persisted
|
||||||
|
(see the effect below), so a restored session carries an empty one — it is
|
||||||
|
unused after login; only `naam` is shown in the chrome. */
|
||||||
|
function restore(): Session | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw) as Partial<Session>;
|
||||||
|
return typeof parsed?.naam === 'string' ? { bsn: '', naam: parsed.naam } : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the current session for the whole app. Because it is providedIn:'root'
|
||||||
|
* there is exactly one instance — every component that injects it sees the same
|
||||||
|
* session signal, so logging in is instantly visible everywhere (the guard, the
|
||||||
|
* header, etc.). The session is mirrored to localStorage so a refresh, a deep-link,
|
||||||
|
* or the full-page navigation the language switch performs (nl at `/` ⇄ en at `/en/`,
|
||||||
|
* separate bundles) keeps you logged in. ponytail: localStorage, not sessionStorage —
|
||||||
|
* sessionStorage's per-tab clearing dropped the login on the cross-bundle language
|
||||||
|
* switch. Trade-off: the demo session now survives tab close; a real portal keeps auth
|
||||||
|
* in an httpOnly cookie/token, not web storage.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class SessionStore {
|
||||||
|
private digid = inject(DigidAdapter);
|
||||||
|
private _session = signal<Session | null>(restore());
|
||||||
|
|
||||||
|
readonly session = this._session.asReadonly();
|
||||||
|
readonly isAuthenticated = computed(() => this._session() !== null);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
const s = this._session();
|
||||||
|
// G1: persist only `naam` — never write the BSN (national ID) to storage.
|
||||||
|
if (s) localStorage.setItem(STORAGE_KEY, JSON.stringify({ naam: s.naam }));
|
||||||
|
else localStorage.removeItem(STORAGE_KEY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Effectful command: authenticate, then store the session on success. */
|
||||||
|
async login(bsn: string): Promise<Result<string, Session>> {
|
||||||
|
const r = await this.digid.authenticate(bsn);
|
||||||
|
if (r.ok) this._session.set(r.value);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this._session.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { AccessStore } from '@shared/application/access.store';
|
||||||
|
import { SessionStore } from './application/session.store';
|
||||||
|
import { authGuard, capabilityGuard } from './auth.guard';
|
||||||
|
|
||||||
|
type Opts = {
|
||||||
|
authed: boolean;
|
||||||
|
can?: (c: string) => boolean;
|
||||||
|
whenReady?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function setup({ authed, can = () => false, whenReady = () => Promise.resolve() }: Opts) {
|
||||||
|
const createUrlTree = vi.fn((cmds: string[]) => ({ tree: cmds }));
|
||||||
|
const readySpy = vi.fn(whenReady);
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: SessionStore, useValue: { isAuthenticated: () => authed } },
|
||||||
|
{ provide: AccessStore, useValue: { whenReady: readySpy, can } },
|
||||||
|
{ provide: Router, useValue: { createUrlTree } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return { createUrlTree, readySpy };
|
||||||
|
}
|
||||||
|
|
||||||
|
// The guards ignore their (route, state) args; cast to call with none.
|
||||||
|
const call = <T>(fn: unknown) => TestBed.runInInjectionContext(() => (fn as () => T)());
|
||||||
|
|
||||||
|
describe('authGuard', () => {
|
||||||
|
it('allows an authenticated user', () => {
|
||||||
|
setup({ authed: true });
|
||||||
|
expect(call(authGuard)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects an anonymous user to /login', () => {
|
||||||
|
const { createUrlTree } = setup({ authed: false });
|
||||||
|
expect(call(authGuard)).toEqual({ tree: ['/login'] });
|
||||||
|
expect(createUrlTree).toHaveBeenCalledWith(['/login']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('capabilityGuard', () => {
|
||||||
|
const guard = () => capabilityGuard('stamdata:edit');
|
||||||
|
|
||||||
|
it('waits for /me, then allows an entitled admin', async () => {
|
||||||
|
const { readySpy } = setup({ authed: true, can: (c) => c === 'stamdata:edit' });
|
||||||
|
await expect(call<Promise<unknown>>(guard())).resolves.toBe(true);
|
||||||
|
expect(readySpy).toHaveBeenCalledOnce(); // it awaited caps before deciding
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends an authenticated-but-unentitled user to /dashboard (not a login loop)', async () => {
|
||||||
|
setup({ authed: true, can: () => false });
|
||||||
|
await expect(call<Promise<unknown>>(guard())).resolves.toEqual({ tree: ['/dashboard'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects an anonymous user to /login without waiting for caps', async () => {
|
||||||
|
const { readySpy } = setup({ authed: false, can: () => true });
|
||||||
|
await expect(call<Promise<unknown>>(guard())).resolves.toEqual({ tree: ['/login'] });
|
||||||
|
expect(readySpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
|
import { AccessStore } from '@shared/application/access.store';
|
||||||
|
import { Capability } from '@shared/domain/capability';
|
||||||
|
import { SessionStore } from './application/session.store';
|
||||||
|
|
||||||
|
/** Route guard: only let authenticated users in; otherwise redirect to /login. */
|
||||||
|
export const authGuard: CanActivateFn = () => {
|
||||||
|
const store = inject(SessionStore);
|
||||||
|
const router = inject(Router);
|
||||||
|
return store.isAuthenticated() ? true : router.createUrlTree(['/login']);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route guard factory (PRD-0002 §6): authenticated AND holding `capability`, else
|
||||||
|
* redirect. Used by the admin pages (`/brief/huisstijl`, `/beheer/stamdata`).
|
||||||
|
*
|
||||||
|
* **Async on purpose:** `can()` is deny-by-default, so it must not be read while `/me`
|
||||||
|
* is still loading — it would deny an entitled admin and bounce them. We await
|
||||||
|
* `AccessStore.whenReady()` (caps resolved) before deciding. An unauthenticated user
|
||||||
|
* goes to `/login`; an authenticated-but-unentitled user goes to `/dashboard` (they're
|
||||||
|
* logged in, just not allowed here — no re-login loop). The backend re-enforces
|
||||||
|
* regardless (403); this guard is the UX pre-gate.
|
||||||
|
*/
|
||||||
|
export function capabilityGuard(capability: Capability): CanActivateFn {
|
||||||
|
return async () => {
|
||||||
|
const session = inject(SessionStore);
|
||||||
|
const access = inject(AccessStore);
|
||||||
|
const router = inject(Router);
|
||||||
|
if (!session.isAuthenticated()) return router.createUrlTree(['/login']);
|
||||||
|
await access.whenReady();
|
||||||
|
return access.can(capability) ? true : router.createUrlTree(['/dashboard']);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** Who is logged in. Framework-free domain type. */
|
||||||
|
export interface Session {
|
||||||
|
readonly bsn: string;
|
||||||
|
readonly naam: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAuthenticated(s: Session | null): s is Session {
|
||||||
|
return s !== null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Result, ok } from '@shared/kernel/fp';
|
||||||
|
import { parseBsn } from '@shared/kernel/bsn';
|
||||||
|
import { Session } from '../domain/session';
|
||||||
|
|
||||||
|
/** Infrastructure: talks to the (mock) DigiD identity provider. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class DigidAdapter {
|
||||||
|
// ponytail: fake DigiD — any elfproef-valid BSN authenticates to a fixed identity.
|
||||||
|
// Real BSN validation (parseBsn, WP-40) is the trust boundary; swap the fixed identity
|
||||||
|
// for a real OIDC redirect flow when there's an IdP.
|
||||||
|
async authenticate(bsn: string): Promise<Result<string, Session>> {
|
||||||
|
const r = parseBsn(bsn);
|
||||||
|
return r.ok ? ok({ bsn: r.value, naam: 'Dr. A. (Anna) de Vries' }) : r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Component, output } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
|
||||||
|
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
|
||||||
|
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||||
|
|
||||||
|
/** Organism: DigiD-style mock login. No real auth — just composes atoms/molecules. */
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login-form',
|
||||||
|
imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent],
|
||||||
|
template: `
|
||||||
|
<form (ngSubmit)="submitted.emit(bsn)" class="form-horizontal">
|
||||||
|
<div class="form-header">
|
||||||
|
<div class="form-action">
|
||||||
|
<span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-form-field
|
||||||
|
i18n-label="@@login.bsnLabel"
|
||||||
|
label="BSN"
|
||||||
|
fieldId="bsn"
|
||||||
|
required
|
||||||
|
i18n-description="@@login.bsnDescription"
|
||||||
|
description="9-cijferig BSN, elfproef-geldig (demo: 123456782)"
|
||||||
|
>
|
||||||
|
<app-text-input
|
||||||
|
inputId="bsn"
|
||||||
|
hasDescription
|
||||||
|
[(ngModel)]="bsn"
|
||||||
|
name="bsn"
|
||||||
|
placeholder="123456782"
|
||||||
|
/>
|
||||||
|
</app-form-field>
|
||||||
|
|
||||||
|
<app-form-field i18n-label="@@login.wachtwoordLabel" label="Wachtwoord" fieldId="pw" required>
|
||||||
|
<app-text-input inputId="pw" type="password" [(ngModel)]="password" name="pw" />
|
||||||
|
</app-form-field>
|
||||||
|
|
||||||
|
<app-button type="submit" variant="primary" i18n="@@login.submit"
|
||||||
|
>Inloggen met DigiD</app-button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class LoginFormComponent {
|
||||||
|
bsn = '';
|
||||||
|
password = '';
|
||||||
|
submitted = output<string>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/angular';
|
||||||
|
import { LoginFormComponent } from './login-form.component';
|
||||||
|
|
||||||
|
const meta: Meta<LoginFormComponent> = {
|
||||||
|
title: 'Domein/Auth/Login Form',
|
||||||
|
component: LoginFormComponent,
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<LoginFormComponent>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||||
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||||
|
import { LoginFormComponent } from '@auth/ui/login-form/login-form.component';
|
||||||
|
import { SessionStore } from '@auth/application/session.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login-page',
|
||||||
|
imports: [PageShellComponent, AlertComponent, LoginFormComponent],
|
||||||
|
template: `
|
||||||
|
<app-page-shell
|
||||||
|
i18n-heading="@@login.heading"
|
||||||
|
heading="Inloggen"
|
||||||
|
width="narrow"
|
||||||
|
i18n-intro="@@login.intro"
|
||||||
|
intro="Log in op uw persoonlijke BIG-register omgeving."
|
||||||
|
>
|
||||||
|
@if (error()) {
|
||||||
|
<app-alert type="error">{{ error() }}</app-alert>
|
||||||
|
}
|
||||||
|
<app-login-form (submitted)="login($event)" />
|
||||||
|
</app-page-shell>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class LoginPage {
|
||||||
|
private store = inject(SessionStore);
|
||||||
|
private router = inject(Router);
|
||||||
|
error = signal('');
|
||||||
|
|
||||||
|
async login(bsn: string) {
|
||||||
|
const r = await this.store.login(bsn);
|
||||||
|
if (r.ok) this.router.navigate(['/dashboard']);
|
||||||
|
else this.error.set(r.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Result, ok, err } from '@shared/kernel/fp';
|
import { Result, ok, err } from '@shared/kernel/fp';
|
||||||
import { currentRole } from '@shared/infrastructure/role';
|
import { currentRole } from '@shared/infrastructure/role';
|
||||||
import { problemDetail } from '@shared/infrastructure/api-error';
|
import { problemDetail } from '@shared/infrastructure/api-error';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '@shared/environments/environment';
|
||||||
|
|
||||||
const PREVIEW_FAILED = $localize`:@@brief.preview.failed:De voorvertoning kon niet worden geopend.`;
|
const PREVIEW_FAILED = $localize`:@@brief.preview.failed:De voorvertoning kon niet worden geopend.`;
|
||||||
|
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { Result, ok, err } from '@shared/kernel/fp';
|
|||||||
import { runSubmit } from '@shared/application/submit';
|
import { runSubmit } from '@shared/application/submit';
|
||||||
import { currentRole } from '@shared/infrastructure/role';
|
import { currentRole } from '@shared/infrastructure/role';
|
||||||
import { problemDetail } from '@shared/infrastructure/api-error';
|
import { problemDetail } from '@shared/infrastructure/api-error';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '@shared/environments/environment';
|
||||||
import {
|
import {
|
||||||
ApiClient,
|
ApiClient,
|
||||||
OrgTemplateAdminViewDto,
|
OrgTemplateAdminViewDto,
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Result, ok, err } from '@shared/kernel/fp';
|
import { Result, ok, err } from '@shared/kernel/fp';
|
||||||
import { currentRole } from '@shared/infrastructure/role';
|
import { currentRole } from '@shared/infrastructure/role';
|
||||||
import { problemDetail } from '@shared/infrastructure/api-error';
|
import { problemDetail } from '@shared/infrastructure/api-error';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '@shared/environments/environment';
|
||||||
|
|
||||||
const REVEAL_FAILED = $localize`:@@brief.reveal.failed:Het BIG-nummer kon niet worden getoond.`;
|
const REVEAL_FAILED = $localize`:@@brief.reveal.failed:Het BIG-nummer kon niet worden getoond.`;
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user