Model Registration status as a discriminated union
Each status variant now owns its own data: only Geregistreerd carries a herregistratieDatum; Geschorst/Doorgehaald carry their own dates + reason. A struck-off registration can no longer hold a future herregistratie date — that impossible combination is gone from the type. - status-badge keys color off the tag via a switch + assertNever - registration-summary renders only the rows a variant's data supports - registration.json nests the status; stories cover all three variants Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,7 @@
|
||||
"bigNummer": "19012345601",
|
||||
"naam": "Dr. A. (Anna) de Vries",
|
||||
"beroep": "Arts",
|
||||
"status": "Geregistreerd",
|
||||
"registratiedatum": "2012-09-01",
|
||||
"herregistratieDatum": "2027-09-01",
|
||||
"geboortedatum": "1985-03-14"
|
||||
"geboortedatum": "1985-03-14",
|
||||
"status": { "tag": "Geregistreerd", "herregistratieDatum": "2027-09-01" }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { RegistrationStatus } from '../../core/models';
|
||||
import { StatusTag } from '../../core/models';
|
||||
import { assertNever } from '../../core/fp';
|
||||
|
||||
/** Atom: status badge = colored RHC dot-badge + label. Hand-built to show how
|
||||
you compose a new atom from design tokens (dot color driven per status). */
|
||||
/** Atom: status badge = colored RHC dot-badge + label. The color is a total
|
||||
function of the status tag — assertNever makes a new status fail to compile
|
||||
until a color is chosen for it. */
|
||||
@Component({
|
||||
selector: 'app-status-badge',
|
||||
template: `
|
||||
@@ -13,10 +15,18 @@ import { RegistrationStatus } from '../../core/models';
|
||||
`,
|
||||
})
|
||||
export class StatusBadgeComponent {
|
||||
status = input.required<RegistrationStatus>();
|
||||
color = computed(() => ({
|
||||
Geregistreerd: 'var(--rhc-color-groen-500)',
|
||||
Doorgehaald: 'var(--rhc-color-rood-500)',
|
||||
Geschorst: 'var(--rhc-color-oranje-500)',
|
||||
}[this.status()]));
|
||||
status = input.required<StatusTag>();
|
||||
color = computed(() => {
|
||||
const tag = this.status();
|
||||
switch (tag) {
|
||||
case 'Geregistreerd':
|
||||
return 'var(--rhc-color-groen-500)';
|
||||
case 'Doorgehaald':
|
||||
return 'var(--rhc-color-rood-500)';
|
||||
case 'Geschorst':
|
||||
return 'var(--rhc-color-oranje-500)';
|
||||
default:
|
||||
return assertNever(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
export type RegistrationStatus = 'Geregistreerd' | 'Doorgehaald' | 'Geschorst';
|
||||
/**
|
||||
* Registration status as a discriminated union: each variant owns exactly the
|
||||
* data that makes sense for it. Only an active (Geregistreerd) registration has
|
||||
* a herregistratie date; a struck-off (Doorgehaald) one cannot carry one. The
|
||||
* old flat interface allowed that impossible combination — this makes it
|
||||
* unrepresentable.
|
||||
*/
|
||||
export type RegistrationStatus =
|
||||
| { tag: 'Geregistreerd'; herregistratieDatum: string } // ISO date
|
||||
| { tag: 'Geschorst'; geschorstTot: string; reden: string }
|
||||
| { tag: 'Doorgehaald'; doorgehaaldOp: string; reden: string };
|
||||
|
||||
/** Just the discriminant — for atoms that only need the label/color. */
|
||||
export type StatusTag = RegistrationStatus['tag'];
|
||||
|
||||
export interface Registration {
|
||||
bigNummer: string;
|
||||
naam: string;
|
||||
beroep: string; // arts, verpleegkundige, apotheker, ...
|
||||
status: RegistrationStatus;
|
||||
registratiedatum: string; // ISO date
|
||||
herregistratieDatum: string;
|
||||
beroep: string; // arts, verpleegkundige, apotheker, ...
|
||||
registratiedatum: string; // ISO date
|
||||
geboortedatum: string;
|
||||
status: RegistrationStatus;
|
||||
}
|
||||
|
||||
export interface Aantekening {
|
||||
type: string; // specialisme of aantekening
|
||||
type: string; // specialisme of aantekening
|
||||
omschrijving: string;
|
||||
datum: string;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,23 @@ import { StatusBadgeComponent } from '../../atoms/status-badge/status-badge.comp
|
||||
<app-data-row key="Naam" [value]="reg().naam" />
|
||||
<app-data-row key="Beroep" [value]="reg().beroep" />
|
||||
<app-data-row key="Status">
|
||||
<app-status-badge [status]="reg().status" />
|
||||
<app-status-badge [status]="reg().status.tag" />
|
||||
</app-data-row>
|
||||
<app-data-row key="Registratiedatum" [value]="reg().registratiedatum | date:'longDate'" />
|
||||
<app-data-row key="Uiterste herregistratie" [value]="reg().herregistratieDatum | date:'longDate'" />
|
||||
<!-- Each status variant renders only the row its own data supports. -->
|
||||
@switch (reg().status.tag) {
|
||||
@case ('Geregistreerd') {
|
||||
<app-data-row key="Uiterste herregistratie" [value]="$any(reg().status).herregistratieDatum | date:'longDate'" />
|
||||
}
|
||||
@case ('Geschorst') {
|
||||
<app-data-row key="Geschorst tot" [value]="$any(reg().status).geschorstTot | date:'longDate'" />
|
||||
<app-data-row key="Reden" [value]="$any(reg().status).reden" />
|
||||
}
|
||||
@case ('Doorgehaald') {
|
||||
<app-data-row key="Doorgehaald op" [value]="$any(reg().status).doorgehaaldOp | date:'longDate'" />
|
||||
<app-data-row key="Reden" [value]="$any(reg().status).reden" />
|
||||
}
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -2,15 +2,13 @@ import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { RegistrationSummaryComponent } from './registration-summary.component';
|
||||
import { Registration } from '../../core/models';
|
||||
|
||||
const sample: Registration = {
|
||||
const base = {
|
||||
bigNummer: '19012345601',
|
||||
naam: 'Dr. A. (Anna) de Vries',
|
||||
beroep: 'Arts',
|
||||
status: 'Geregistreerd',
|
||||
registratiedatum: '2012-09-01',
|
||||
herregistratieDatum: '2027-09-01',
|
||||
geboortedatum: '1985-03-14',
|
||||
};
|
||||
} satisfies Omit<Registration, 'status'>;
|
||||
|
||||
const meta: Meta<RegistrationSummaryComponent> = {
|
||||
title: 'Organisms/Registration Summary',
|
||||
@@ -19,5 +17,14 @@ const meta: Meta<RegistrationSummaryComponent> = {
|
||||
export default meta;
|
||||
type Story = StoryObj<RegistrationSummaryComponent>;
|
||||
|
||||
export const Default: Story = { args: { reg: sample } };
|
||||
export const Geschorst: Story = { args: { reg: { ...sample, status: 'Geschorst' } } };
|
||||
// Each story feeds a different union variant; the card renders only the rows
|
||||
// that variant's data supports (note Doorgehaald has no herregistratie date).
|
||||
export const Geregistreerd: Story = {
|
||||
args: { reg: { ...base, status: { tag: 'Geregistreerd', herregistratieDatum: '2027-09-01' } } },
|
||||
};
|
||||
export const Geschorst: Story = {
|
||||
args: { reg: { ...base, status: { tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'Lopend tuchtonderzoek' } } },
|
||||
};
|
||||
export const Doorgehaald: Story = {
|
||||
args: { reg: { ...base, status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' } } },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user