feat(registratie): WP-34 — phone field + BRP address read-only

Reshape the adreswijziging form into a contact-change form: the BRP address is
authoritative and shown read-only (you change it at the gemeente), and the phone
number becomes the editable/submittable field. New Telefoonnummer value object
(parse-don't-validate); backend RejectPhoneChange re-validates as authority.
POST /change-requests now carries { telefoon } (typed client regenerated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-22 20:11:26 +02:00
co-authored by Claude Opus 4.8
parent 1ed4850858
commit 0ea43af7b6
21 changed files with 329 additions and 171 deletions
@@ -77,7 +77,7 @@ public sealed record DocumentRefDto(string CategoryId, string Channel, string? D
public sealed record RegistratieRequest(string DiplomaHerkomst, IReadOnlyList<DocumentRefDto>? Documents = null); public sealed record RegistratieRequest(string DiplomaHerkomst, IReadOnlyList<DocumentRefDto>? Documents = null);
public sealed record IntakeRequest(int Uren); public sealed record IntakeRequest(int Uren);
public sealed record HerregistratieRequest(int Uren, IReadOnlyList<DocumentRefDto>? Documents = null); public sealed record HerregistratieRequest(int Uren, IReadOnlyList<DocumentRefDto>? Documents = null);
public sealed record ChangeRequestRequest(string Straat, string Postcode, string Woonplaats); public sealed record ChangeRequestRequest(string Telefoon);
public sealed record ReferentieResponse(string Referentie); public sealed record ReferentieResponse(string Referentie);
@@ -19,15 +19,17 @@ public static class SubmissionRules
public static string? RejectZeroUren(int uren) => public static string? RejectZeroUren(int uren) =>
uren == 0 ? "Aanvraag afgewezen: geen gewerkte uren geregistreerd." : null; uren == 0 ? "Aanvraag afgewezen: geen gewerkte uren geregistreerd." : null;
private static readonly Regex PostcodePattern = private static readonly Regex PhonePattern =
new(@"^[1-9]\d{3}\s?[A-Z]{2}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); new(@"^0\d{9}$", RegexOptions.Compiled);
// RULE: a change request needs a street and a well-formed Dutch postcode. The // RULE: a contact change needs a well-formed Dutch phone number (10 digits, leading
// server re-validates format authoritatively (the FE check is UX-only). // 0, formatting stripped). The BRP address is authoritative and cannot be changed
public static string? RejectChangeRequest(string straat, string postcode) // here (WP-34), so only the phone is submitted. The server re-validates format
// authoritatively (the FE check is UX-only).
public static string? RejectPhoneChange(string telefoon)
{ {
if (string.IsNullOrWhiteSpace(straat)) return "Vul straat en huisnummer in."; var digits = (telefoon ?? "").Trim().Replace(" ", "").Replace("-", "");
if (!PostcodePattern.IsMatch(postcode?.Trim() ?? "")) return "Voer een geldige postcode in, bijv. 1234 AB."; if (!PhonePattern.IsMatch(digits)) return "Voer een geldig telefoonnummer in, bijv. 0612345678.";
return null; return null;
} }
+1 -1
View File
@@ -146,7 +146,7 @@ api.MapPost("/intakes", (IntakeRequest req, HttpContext ctx) =>
.ProducesProblem(StatusCodes.Status422UnprocessableEntity); .ProducesProblem(StatusCodes.Status422UnprocessableEntity);
api.MapPost("/change-requests", (ChangeRequestRequest req, HttpContext ctx) => api.MapPost("/change-requests", (ChangeRequestRequest req, HttpContext ctx) =>
Submit(ctx, "adreswijziging", SubmissionRules.RejectChangeRequest(req.Straat, req.Postcode))) Submit(ctx, "telefoonwijziging", SubmissionRules.RejectPhoneChange(req.Telefoon)))
.Produces<ReferentieResponse>() .Produces<ReferentieResponse>()
.ProducesProblem(StatusCodes.Status422UnprocessableEntity); .ProducesProblem(StatusCodes.Status422UnprocessableEntity);
+1 -9
View File
@@ -1528,15 +1528,7 @@
"ChangeRequestRequest": { "ChangeRequestRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"straat": { "telefoon": {
"type": "string",
"nullable": true
},
"postcode": {
"type": "string",
"nullable": true
},
"woonplaats": {
"type": "string", "type": "string",
"nullable": true "nullable": true
} }
@@ -101,20 +101,20 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
} }
[Fact] [Fact]
public async Task Change_request_with_valid_address_succeeds() public async Task Change_request_with_valid_phone_succeeds()
{ {
var res = await _client.PostAsJsonAsync("/api/v1/change-requests", var res = await _client.PostAsJsonAsync("/api/v1/change-requests",
new { straat = "Lange Voorhout 9", postcode = "2514 EA", woonplaats = "Den Haag" }); new { telefoon = "0612345678" });
res.EnsureSuccessStatusCode(); res.EnsureSuccessStatusCode();
var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>(); var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>();
Assert.StartsWith("BIG-2026-", body!.Referentie); Assert.StartsWith("BIG-2026-", body!.Referentie);
} }
[Fact] [Fact]
public async Task Change_request_with_bad_postcode_is_rejected() public async Task Change_request_with_bad_phone_is_rejected()
{ {
var res = await _client.PostAsJsonAsync("/api/v1/change-requests", var res = await _client.PostAsJsonAsync("/api/v1/change-requests",
new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }); new { telefoon = "nope" });
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode); Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
} }
@@ -12,7 +12,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture
{ {
var req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests") var req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests")
{ {
Content = JsonContent.Create(new { straat = "Lange Voorhout 9", postcode = "2514 EA", woonplaats = "Den Haag" }), Content = JsonContent.Create(new { telefoon = "0612345678" }),
}; };
req.Headers.Add("Idempotency-Key", key); req.Headers.Add("Idempotency-Key", key);
return req; return req;
@@ -51,7 +51,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture
var key = Guid.NewGuid().ToString(); var key = Guid.NewGuid().ToString();
var badRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests") var badRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests")
{ {
Content = JsonContent.Create(new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }), Content = JsonContent.Create(new { telefoon = "nope" }),
}; };
badRequest.Headers.Add("Idempotency-Key", key); badRequest.Headers.Add("Idempotency-Key", key);
@@ -60,7 +60,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture
var replayRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests") var replayRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests")
{ {
Content = JsonContent.Create(new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }), Content = JsonContent.Create(new { telefoon = "nope" }),
}; };
replayRequest.Headers.Add("Idempotency-Key", key); replayRequest.Headers.Add("Idempotency-Key", key);
var replay = await _client.SendAsync(replayRequest); var replay = await _client.SendAsync(replayRequest);
+6 -5
View File
@@ -172,9 +172,10 @@ public class SubmissionRuleTests
Assert.Null(SubmissionRules.RejectZeroUren(40)); Assert.Null(SubmissionRules.RejectZeroUren(40));
[Theory] [Theory]
[InlineData("Lange Voorhout 9", "2514 EA", null)] // valid [InlineData("0612345678", null)] // valid mobile
[InlineData("", "2514 EA", "Vul straat en huisnummer in.")] [InlineData("070 123 45 67", null)] // valid landline, formatting stripped
[InlineData("Straat 1", "nope", "Voer een geldige postcode in, bijv. 1234 AB.")] [InlineData("nope", "Voer een geldig telefoonnummer in, bijv. 0612345678.")]
public void Change_request_is_validated(string straat, string postcode, string? expected) => [InlineData("12345", "Voer een geldig telefoonnummer in, bijv. 0612345678.")]
Assert.Equal(expected, SubmissionRules.RejectChangeRequest(straat, postcode)); public void Phone_change_is_validated(string telefoon, string? expected) =>
Assert.Equal(expected, SubmissionRules.RejectPhoneChange(telefoon));
} }
+1 -1
View File
@@ -78,7 +78,7 @@ for its existing violations, so every WP ends green.
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done | | [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
| [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done | | [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done |
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done | | [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | todo | | [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | todo | | [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | todo |
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | todo | | [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | todo |
@@ -0,0 +1,45 @@
# WP-34 — Adres: phone field + BRP address read-only
Status: done
Phase: 7 — refinements
## Why
The "Mijn gegevens" screen let the user *edit* their address (straat/postcode/woonplaats) and
submit it as an adreswijziging. But the BRP (Basisregistratie Personen) is the authority for a
person's address — you change it at the municipality, not in a register self-service portal.
This WP corrects that: the address is shown **read-only** (rendered from the BRP data the
screen already loads), and the editable/submittable contact detail becomes the **phone number**
— the thing CIBG actually holds and the professional can update here.
## Decisions (made while building — no spec existed; flagged for review)
- **The adreswijziging form becomes a telefoonwijziging form.** Address is read-only display;
phone is the one editable field. Same single-step machine idiom (Model/Msg/pure reduce +
value object + submit command) — only the fields changed.
- **New `Telefoonnummer` value object** (parse-don't-validate, lax Dutch format: 10 digits,
leading 0, `+31``0`, formatting stripped). Backend `RejectPhoneChange` re-validates as the
authority (ADR-0001); the FE check is UX-only. Replaces the old address/`RejectChangeRequest`.
- **Phone starts empty.** There is no current-phone anywhere in BRP/seed/Person, so no
current-value round-trip was added (YAGNI) — the form submits a new/updated number. The
read-only BRP address gives the screen its context.
- **Endpoint reused, contract narrowed.** `POST /change-requests` now takes `{ telefoon }`
(category `telefoonwijziging`); the typed client was regenerated (drift check clean).
## Files
- `registratie/domain/value-objects/telefoonnummer.ts` (+spec) — new value object.
- `registratie/domain/change-request.machine.ts` (+spec) — Draft/Valid now `{ telefoon }`.
- `registratie/infrastructure/change-request.adapter.ts` — sends `{ telefoon }`.
- `registratie/ui/change-request-form/change-request-form.component.ts` (+story) — read-only
BRP address block + editable phone field; takes `brpAdres` input.
- `registratie/ui/registration-detail.page.ts` — passes `profile()?.person?.adres`.
- Backend: `Dtos.cs`, `Program.cs`, `SubmissionRules.cs` (+ RuleTests/EndpointTests/IdempotencyTests).
- `src/locale/*` — new/changed `$localize` ids + English targets.
## Acceptance criteria
- [x] BRP address rendered read-only with a "change it at your municipality" note.
- [x] Phone field with format validation (client instant + server authoritative).
- [x] `npm run ci` green (lint, format, tokens, 332 FE tests, localized build, backend 122
tests, api-client drift clean after commit).
@@ -3,16 +3,12 @@ import { describe, it, expect } from 'vitest';
import { Valid } from '@registratie/domain/change-request.machine'; import { Valid } from '@registratie/domain/change-request.machine';
import { ChangeRequestAdapter } from '@registratie/infrastructure/change-request.adapter'; import { ChangeRequestAdapter } from '@registratie/infrastructure/change-request.adapter';
import { createSubmitChangeRequest } from './submit-change-request'; import { createSubmitChangeRequest } from './submit-change-request';
import { parsePostcode } from '@registratie/domain/value-objects/postcode'; import { parseTelefoonnummer } from '@registratie/domain/value-objects/telefoonnummer';
const postcode = parsePostcode('2514 EA'); const telefoon = parseTelefoonnummer('0612345678');
if (!postcode.ok) throw new Error('fixture postcode should parse'); if (!telefoon.ok) throw new Error('fixture phone should parse');
const data: Valid = { const data: Valid = { telefoon: telefoon.value };
straat: 'Lange Voorhout 9',
postcode: postcode.value,
woonplaats: 'Den Haag',
};
function setup(adapter: Partial<ChangeRequestAdapter>) { function setup(adapter: Partial<ChangeRequestAdapter>) {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -38,9 +34,9 @@ describe('createSubmitChangeRequest', () => {
it('surfaces a ProblemDetails detail message when the server rejects with one', async () => { it('surfaces a ProblemDetails detail message when the server rejects with one', async () => {
const submit = setup({ const submit = setup({
changeRequest: () => Promise.reject({ detail: 'Postcode komt niet overeen met de straat.' }), changeRequest: () => Promise.reject({ detail: 'Telefoonnummer is ongeldig.' }),
}); });
const r = await submit(data); const r = await submit(data);
expect(r).toEqual({ ok: false, error: 'Postcode komt niet overeen met de straat.' }); expect(r).toEqual({ ok: false, error: 'Telefoonnummer is ongeldig.' });
}); });
}); });
@@ -1,67 +1,56 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { ChangeRequestState, reduce, initial } from './change-request.machine'; import { ChangeRequestState, reduce, initial } from './change-request.machine';
const editingWith = ( const editingWith = (telefoon: string): ChangeRequestState => ({
draft: Partial<{ straat: string; postcode: string; woonplaats: string }>,
): ChangeRequestState => ({
tag: 'Editing', tag: 'Editing',
draft: { straat: '', postcode: '', woonplaats: '', ...draft }, draft: { telefoon },
errors: {}, errors: {},
}); });
describe('change-request reduce', () => { describe('change-request reduce', () => {
it('SetField updates the draft while editing', () => { it('SetField updates the draft while editing', () => {
const s = reduce(initial, { tag: 'SetField', key: 'straat', value: 'Lange Voorhout 9' }); const s = reduce(initial, { tag: 'SetField', key: 'telefoon', value: '0612345678' });
expect(s.tag).toBe('Editing'); expect(s.tag).toBe('Editing');
expect((s as Extract<ChangeRequestState, { tag: 'Editing' }>).draft.straat).toBe( expect((s as Extract<ChangeRequestState, { tag: 'Editing' }>).draft.telefoon).toBe(
'Lange Voorhout 9', '0612345678',
); );
}); });
it('Submit with an invalid draft stays Editing and reports field errors', () => { it('Submit with an invalid draft stays Editing and reports field errors', () => {
const s = reduce(editingWith({ straat: '', postcode: 'nope' }), { tag: 'Submit' }); const s = reduce(editingWith('nope'), { tag: 'Submit' });
expect(s.tag).toBe('Editing'); expect(s.tag).toBe('Editing');
const errors = (s as Extract<ChangeRequestState, { tag: 'Editing' }>).errors; const errors = (s as Extract<ChangeRequestState, { tag: 'Editing' }>).errors;
expect(errors.straat).toBeTruthy(); expect(errors.telefoon).toBeTruthy();
expect(errors.postcode).toBeTruthy();
}); });
it('Submit with a valid draft moves to Submitting with parsed (normalised) data', () => { it('Submit with a valid draft moves to Submitting with parsed (normalised) data', () => {
const s = reduce(editingWith({ straat: 'Lange Voorhout 9', postcode: '2514ea' }), { const s = reduce(editingWith('06 12 34 56 78'), { tag: 'Submit' });
tag: 'Submit',
});
expect(s.tag).toBe('Submitting'); expect(s.tag).toBe('Submitting');
expect((s as Extract<ChangeRequestState, { tag: 'Submitting' }>).data.postcode).toBe('2514 EA'); expect((s as Extract<ChangeRequestState, { tag: 'Submitting' }>).data.telefoon).toBe(
'0612345678',
);
}); });
it('SubmitConfirmed maps Submitting to Submitted with the referentie', () => { it('SubmitConfirmed maps Submitting to Submitted with the referentie', () => {
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' });
tag: 'Submit',
});
const ok = reduce(submitting, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-1' }); const ok = reduce(submitting, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-1' });
expect(ok).toMatchObject({ tag: 'Submitted', referentie: 'BIG-2026-1' }); expect(ok).toMatchObject({ tag: 'Submitted', referentie: 'BIG-2026-1' });
}); });
it('SubmitFailed maps Submitting to Failed with the error', () => { it('SubmitFailed maps Submitting to Failed with the error', () => {
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' });
tag: 'Submit',
});
const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' }); const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' });
expect(failed).toMatchObject({ tag: 'Failed', error: 'boom' }); expect(failed).toMatchObject({ tag: 'Failed', error: 'boom' });
}); });
it('Retry re-submits a failure', () => { it('Retry re-submits a failure', () => {
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' });
tag: 'Submit',
});
const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' }); const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'boom' });
expect(reduce(failed, { tag: 'Retry' }).tag).toBe('Submitting'); expect(reduce(failed, { tag: 'Retry' }).tag).toBe('Submitting');
}); });
it('Reset returns to the initial editing state', () => { it('Reset returns to the initial editing state', () => {
const submitting = reduce(editingWith({ straat: 'A 1', postcode: '2514 EA' }), { const submitting = reduce(editingWith('0612345678'), { tag: 'Submit' });
tag: 'Submit',
});
expect(reduce(submitting, { tag: 'Reset' })).toEqual(initial); expect(reduce(submitting, { tag: 'Reset' })).toEqual(initial);
}); });
}); });
@@ -1,24 +1,25 @@
import { Result, assertNever } from '@shared/kernel/fp'; import { Result, assertNever } from '@shared/kernel/fp';
import { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode'; import {
Telefoonnummer,
parseTelefoonnummer,
} from '@registratie/domain/value-objects/telefoonnummer';
/** What the user is typing (raw, possibly invalid). */ /** What the user is typing (raw, possibly invalid). The BRP address is NOT part of
the form — it is authoritative and shown read-only (WP-34); only the phone number
is editable here. */
export interface Draft { export interface Draft {
straat: string; telefoon: string;
postcode: string;
woonplaats: string;
} }
/** After parsing — postcode is the branded type, so downstream can't get a raw one. */ /** After parsing — telefoon is the branded type, so downstream can't get a raw one. */
export interface Valid { export interface Valid {
straat: string; telefoon: Telefoonnummer;
postcode: Postcode;
woonplaats: string;
} }
export type Errors = Partial<Record<keyof Draft, string>>; export type Errors = Partial<Record<keyof Draft, string>>;
/** /**
* The change-request (adreswijziging) form as one tagged union — the SAME idiom * The contact-change (telefoonwijziging) form as one tagged union — the SAME idiom
* as the wizards, just single-step. `draft`/`errors` exist only while Editing; * as the wizards, just single-step. `draft`/`errors` exist only while Editing;
* Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting * Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting
* an invalid draft, a success screen with errors) are unrepresentable. * an invalid draft, a success screen with errors) are unrepresentable.
@@ -31,24 +32,15 @@ export type ChangeRequestState =
export const initial: ChangeRequestState = { export const initial: ChangeRequestState = {
tag: 'Editing', tag: 'Editing',
draft: { straat: '', postcode: '', woonplaats: '' }, draft: { telefoon: '' },
errors: {}, errors: {},
}; };
/** Parse via the value objects; on success hand back a Valid, else per-field errors. */ /** Parse via the value object; on success hand back a Valid, else per-field errors. */
function validate(draft: Draft): Result<Errors, Valid> { function validate(draft: Draft): Result<Errors, Valid> {
const straat = draft.straat.trim(); const telefoon = parseTelefoonnummer(draft.telefoon);
const postcode = parsePostcode(draft.postcode); if (telefoon.ok) return { ok: true, value: { telefoon: telefoon.value } };
const errors: Errors = {}; return { ok: false, error: { telefoon: telefoon.error } };
if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;
if (!postcode.ok) errors.postcode = postcode.error;
if (straat && postcode.ok) {
return {
ok: true,
value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() },
};
}
return { ok: false, error: errors };
} }
export type ChangeRequestMsg = export type ChangeRequestMsg =
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest';
import { parseTelefoonnummer } from './telefoonnummer';
describe('parseTelefoonnummer', () => {
it('accepts a 10-digit number starting 0 and strips formatting', () => {
const r = parseTelefoonnummer('06 12 34 56 78');
expect(r.ok && r.value).toBe('0612345678');
});
it('normalises a +31 prefix to a leading 0', () => {
const r = parseTelefoonnummer('+31 6 12345678');
expect(r.ok && r.value).toBe('0612345678');
});
it('rejects a too-short number, a non-0 start, and junk', () => {
expect(parseTelefoonnummer('12345').ok).toBe(false);
expect(parseTelefoonnummer('1612345678').ok).toBe(false);
expect(parseTelefoonnummer('nope').ok).toBe(false);
});
});
@@ -0,0 +1,25 @@
import { Brand, Result, ok, err } from '@shared/kernel/fp';
/**
* Value object: a Dutch phone number. "Parse, don't validate" — a Telefoonnummer is
* a distinct type from a raw string, mintable only via parseTelefoonnummer, so holding
* one is proof it is well-formed. Format-only check (the FE keeps format validation for
* instant feedback; the backend stays the authority — see ADR-0001). The parsed value
* is normalised to digits (spaces/dashes/parens dropped, a leading +31 → 0).
*/
export type Telefoonnummer = Brand<string, 'Telefoonnummer'>;
export function parseTelefoonnummer(raw: string): Result<string, Telefoonnummer> {
const digits = raw
.trim()
.replace(/[\s\-()]/g, '')
.replace(/^\+31/, '0');
// Deliberately lax: a Dutch number is 10 digits starting 0 (mobile 06 or landline).
// Good enough for instant feedback; the server re-validates.
if (!/^0\d{9}$/.test(digits)) {
return err(
$localize`:@@validation.telefoon:Voer een geldig telefoonnummer in, bijv. 0612345678.`,
);
}
return ok(digits as Telefoonnummer);
}
@@ -3,21 +3,18 @@ import { ApiClient } from '@shared/infrastructure/api-client';
import { Valid } from '@registratie/domain/change-request.machine'; import { Valid } from '@registratie/domain/change-request.machine';
/** /**
* Infrastructure adapter for the adreswijziging POST (`/api/v1/change-requests`) — * Infrastructure adapter for the telefoonwijziging POST (`/api/v1/change-requests`) —
* the single place the network client lives for change requests, so the command * the single place the network client lives for contact changes, so the command
* and the UI never touch `ApiClient`. Returns the server reference; the server * and the UI never touch `ApiClient`. The BRP address is authoritative and not
* re-validates and is the authority. * submitted (WP-34); only the phone number is. Returns the server reference; the
* server re-validates and is the authority.
*/ */
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ChangeRequestAdapter { export class ChangeRequestAdapter {
private client = inject(ApiClient); private client = inject(ApiClient);
async changeRequest(data: Valid): Promise<string> { async changeRequest(data: Valid): Promise<string> {
const res = await this.client.changeRequests({ const res = await this.client.changeRequests({ telefoon: data.telefoon });
straat: data.straat,
postcode: data.postcode,
woonplaats: data.woonplaats,
});
return res.referentie ?? ''; return res.referentie ?? '';
} }
} }
@@ -3,11 +3,9 @@ import { FormsModule } from '@angular/forms';
import { ButtonComponent } from '@shared/ui/button/button.component'; import { ButtonComponent } from '@shared/ui/button/button.component';
import { HeadingComponent } from '@shared/ui/heading/heading.component'; import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { AlertComponent } from '@shared/ui/alert/alert.component'; import { AlertComponent } from '@shared/ui/alert/alert.component';
import { import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
AddressFieldsComponent, import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
AdresValue, import { Adres } from '@registratie/domain/person';
AdresErrors,
} from '@registratie/ui/address-fields/address-fields.component';
import { createStore } from '@shared/application/store'; import { createStore } from '@shared/application/store';
import { whenTag } from '@shared/kernel/fp'; import { whenTag } from '@shared/kernel/fp';
import { import {
@@ -19,19 +17,44 @@ import {
import { createSubmitChangeRequest } from '@registratie/application/submit-change-request'; import { createSubmitChangeRequest } from '@registratie/application/submit-change-request';
/** /**
* Organism: change-request (adreswijziging) form. Uses the SAME idiom as the * Organism: contact-change (telefoonwijziging) form. The BRP address is authoritative
* wizards — all state in one signal driven by the pure `reduce` * and shown READ-ONLY (WP-34) — you change your address at the gemeente, not here — so
* (change-request.machine.ts), submitted via a `submit-*` command returning * only the phone number is editable. Uses the SAME idiom as the wizards: all state in
* `Result`. Renders the shared `<app-address-fields>`; the server re-validates. * one signal driven by the pure `reduce` (change-request.machine.ts), submitted via a
* `submit-*` command returning `Result`. The server re-validates.
*/ */
@Component({ @Component({
selector: 'app-change-request-form', selector: 'app-change-request-form',
imports: [FormsModule, ButtonComponent, HeadingComponent, AlertComponent, AddressFieldsComponent], imports: [
FormsModule,
ButtonComponent,
HeadingComponent,
AlertComponent,
FormFieldComponent,
TextInputComponent,
],
styles: [
`
.brp {
margin-block-end: var(--rhc-space-max-lg);
}
.brp dt {
font-weight: var(--rhc-text-font-weight-semi-bold);
}
.brp dd {
margin: 0 0 var(--rhc-space-max-sm) 0;
}
.brp .source {
color: var(--rhc-color-grijs-700);
font-size: var(--rhc-text-font-size-sm);
}
`,
],
template: ` template: `
@if (state().tag === 'Submitted') { @if (state().tag === 'Submitted') {
<app-alert type="ok" i18n="@@changeRequest.success"> <app-alert type="ok" i18n="@@changeRequest.success">
Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 Uw wijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 werkdagen
werkdagen bericht. bericht.
</app-alert> </app-alert>
<div class="app-section"> <div class="app-section">
<app-button <app-button
@@ -42,19 +65,43 @@ import { createSubmitChangeRequest } from '@registratie/application/submit-chang
> >
</div> </div>
} @else { } @else {
<app-heading [level]="2" i18n="@@changeRequest.heading">Adreswijziging doorgeven</app-heading> <app-heading [level]="2" i18n="@@changeRequest.heading">Contactgegevens wijzigen</app-heading>
@if (brpAdres(); as a) {
<dl class="brp app-section">
<dt i18n="@@changeRequest.brpAdresLabel">Adres (BRP)</dt>
<dd>{{ a.straat }}<br />{{ a.postcode }} {{ a.woonplaats }}</dd>
<dd class="source" i18n="@@changeRequest.brpAdresBron">
Uw adres komt uit de Basisregistratie Personen en kan hier niet worden gewijzigd. Wijzig
het bij uw gemeente.
</dd>
</dl>
}
<form (ngSubmit)="onSubmit()" class="form-horizontal app-section"> <form (ngSubmit)="onSubmit()" class="form-horizontal app-section">
<div class="form-header"> <div class="form-header">
<div class="form-action"> <div class="form-action">
<span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span> <span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span>
</div> </div>
</div> </div>
<app-address-fields <app-form-field
idPrefix="cr" i18n-label="@@changeRequest.telefoonLabel"
[value]="adres()" label="Telefoonnummer"
[errors]="errors()" fieldId="cr-telefoon"
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })" required
[error]="errors().telefoon"
>
<app-text-input
inputId="cr-telefoon"
[invalid]="!!errors().telefoon"
[ngModel]="telefoon()"
(ngModelChange)="dispatch({ tag: 'SetField', key: 'telefoon', value: $event })"
name="telefoon"
i18n-placeholder="@@changeRequest.telefoonPlaceholder"
placeholder="0612345678"
[ngModelOptions]="{ standalone: true }"
/> />
</app-form-field>
@if (failedError()) { @if (failedError()) {
<app-alert type="error" <app-alert type="error"
@@ -77,6 +124,9 @@ export class ChangeRequestFormComponent {
private submit = createSubmitChangeRequest(); private submit = createSubmitChangeRequest();
private store = createStore<ChangeRequestState, ChangeRequestMsg>(initial, reduce); private store = createStore<ChangeRequestState, ChangeRequestMsg>(initial, reduce);
/** BRP address, shown read-only. Undefined until the profile loads. */
brpAdres = input<Adres | undefined>(undefined);
/** Optional seed so Storybook / tests can mount any state directly. */ /** Optional seed so Storybook / tests can mount any state directly. */
seed = input<ChangeRequestState>(initial); seed = input<ChangeRequestState>(initial);
@@ -87,19 +137,17 @@ export class ChangeRequestFormComponent {
protected readonly submitBezigLabel = $localize`:@@changeRequest.submitBezig:Bezig met indienen…`; protected readonly submitBezigLabel = $localize`:@@changeRequest.submitBezig:Bezig met indienen…`;
private editing = computed(() => whenTag(this.state(), 'Editing')); private editing = computed(() => whenTag(this.state(), 'Editing'));
protected errors = computed<AdresErrors>(() => this.editing()?.errors ?? {}); protected errors = computed(() => this.editing()?.errors ?? {});
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? ''); protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
protected referentie = computed(() => whenTag(this.state(), 'Submitted')?.referentie ?? ''); protected referentie = computed(() => whenTag(this.state(), 'Submitted')?.referentie ?? '');
/** The address shown in the fields — the live draft while editing, the parsed /** The phone shown in the field — the live draft while editing, the parsed value
data while submitting/failed (so the user sees what they sent). */ while submitting/failed (so the user sees what they sent). */
protected adres = computed<AdresValue>(() => { protected telefoon = computed(() => {
const s = this.state(); const s = this.state();
if (s.tag === 'Editing') return s.draft; if (s.tag === 'Editing') return s.draft.telefoon;
if (s.tag === 'Submitting' || s.tag === 'Failed') { if (s.tag === 'Submitting' || s.tag === 'Failed') return s.data.telefoon;
return { straat: s.data.straat, postcode: s.data.postcode, woonplaats: s.data.woonplaats }; return '';
}
return { straat: '', postcode: '', woonplaats: '' }; // Submitted shows the success alert, not the fields
}); });
constructor() { constructor() {
@@ -3,38 +3,31 @@ import { applicationConfig } from '@storybook/angular';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient } from '@angular/common/http';
import { ChangeRequestFormComponent } from './change-request-form.component'; import { ChangeRequestFormComponent } from './change-request-form.component';
import { provideApiClient } from '@shared/infrastructure/api-client.provider'; import { provideApiClient } from '@shared/infrastructure/api-client.provider';
import { Postcode } from '@registratie/domain/value-objects/postcode'; import { Telefoonnummer } from '@registratie/domain/value-objects/telefoonnummer';
const validData = { const brpAdres = { straat: 'Lange Voorhout 9', postcode: '2514 EA', woonplaats: 'Den Haag' };
straat: 'Lange Voorhout 9', const validData = { telefoon: '0612345678' as Telefoonnummer };
postcode: '2514 EA' as Postcode,
woonplaats: 'Den Haag',
};
const meta: Meta<ChangeRequestFormComponent> = { const meta: Meta<ChangeRequestFormComponent> = {
title: 'Domein/Registratie/Change Request Form', title: 'Domein/Registratie/Change Request Form',
component: ChangeRequestFormComponent, component: ChangeRequestFormComponent,
// The form injects ApiClient (over HttpClient) for the submit command. // The form injects ApiClient (over HttpClient) for the submit command.
decorators: [applicationConfig({ providers: [provideHttpClient(), provideApiClient()] })], decorators: [applicationConfig({ providers: [provideHttpClient(), provideApiClient()] })],
args: { brpAdres },
}; };
export default meta; export default meta;
type Story = StoryObj<ChangeRequestFormComponent>; type Story = StoryObj<ChangeRequestFormComponent>;
// One render per state of the machine. // One render per state of the machine.
export const Empty: Story = { export const Empty: Story = {
args: { args: { seed: { tag: 'Editing', draft: { telefoon: '' }, errors: {} } },
seed: { tag: 'Editing', draft: { straat: '', postcode: '', woonplaats: '' }, errors: {} },
},
}; };
export const WithErrors: Story = { export const WithErrors: Story = {
args: { args: {
seed: { seed: {
tag: 'Editing', tag: 'Editing',
draft: { straat: '', postcode: 'nope', woonplaats: '' }, draft: { telefoon: 'nope' },
errors: { errors: { telefoon: 'Voer een geldig telefoonnummer in, bijv. 0612345678.' },
straat: 'Vul straat en huisnummer in.',
postcode: 'Voer een geldige postcode in, bijv. 1234 AB.',
},
}, },
}, },
}; };
@@ -33,7 +33,7 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
</app-async> </app-async>
<div class="app-section"> <div class="app-section">
<app-change-request-form /> <app-change-request-form [brpAdres]="profile()?.person?.adres" />
</div> </div>
</app-page-shell> </app-page-shell>
`, `,
+1 -3
View File
@@ -1721,9 +1721,7 @@ export interface CaseContextDto {
} }
export interface ChangeRequestRequest { export interface ChangeRequestRequest {
straat?: string | undefined; telefoon?: string | undefined;
postcode?: string | undefined;
woonplaats?: string | undefined;
} }
export interface CreateApplicationRequest { export interface CreateApplicationRequest {
+41 -9
View File
@@ -1074,12 +1074,12 @@
<context context-type="linenumber">93</context> <context context-type="linenumber">93</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="validation.straat" datatype="html"> <trans-unit id="validation.telefoon" datatype="html">
<source>Vul straat en huisnummer in.</source> <source>Voer een geldig telefoonnummer in, bijv. 0612345678.</source>
<target datatype="html">Enter a street and house number.</target> <target datatype="html">Enter a valid phone number, e.g. 0612345678.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/domain/change-request.machine.ts</context> <context context-type="sourcefile">src/app/registratie/domain/value-objects/telefoonnummer.ts</context>
<context context-type="linenumber">43</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="validation.straat2" datatype="html"> <trans-unit id="validation.straat2" datatype="html">
@@ -1295,8 +1295,8 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.success" datatype="html"> <trans-unit id="changeRequest.success" datatype="html">
<source> Uw adreswijziging is ontvangen (referentie <x id="INTERPOLATION" equiv-text="{{ referentie() }}"/>). U ontvangt binnen 5 werkdagen bericht. </source> <source> Uw wijziging is ontvangen (referentie <x id="INTERPOLATION" equiv-text="{{ referentie() }}"/>). U ontvangt binnen 5 werkdagen bericht. </source>
<target datatype="html"> Your address change has been received (reference <x id="INTERPOLATION" equiv-text="{{ referentie() }}"/>). You will hear from us within 5 business days. </target> <target datatype="html"> Your change has been received (reference <x id="INTERPOLATION" equiv-text="{{ referentie() }}"/>). You will hear from us within 5 business days. </target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">28,30</context> <context context-type="linenumber">28,30</context>
@@ -1311,13 +1311,45 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.heading" datatype="html"> <trans-unit id="changeRequest.heading" datatype="html">
<source>Adreswijziging doorgeven</source> <source>Contactgegevens wijzigen</source>
<target datatype="html">Report address change</target> <target datatype="html">Change contact details</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">40,41</context> <context context-type="linenumber">40,41</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.brpAdresLabel" datatype="html">
<source>Adres (BRP)</source>
<target datatype="html">Address (BRP)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.brpAdresBron" datatype="html">
<source> Uw adres komt uit de Basisregistratie Personen en kan hier niet worden gewijzigd. Wijzig het bij uw gemeente. </source>
<target datatype="html"> Your address comes from the Personal Records Database (BRP) and cannot be changed here. Change it at your municipality. </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">43</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.telefoonLabel" datatype="html">
<source>Telefoonnummer</source>
<target datatype="html">Phone number</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.telefoonPlaceholder" datatype="html">
<source>0612345678</source>
<target datatype="html">0612345678</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.failed" datatype="html"> <trans-unit id="changeRequest.failed" datatype="html">
<source>Het indienen is niet gelukt:</source> <source>Het indienen is niet gelukt:</source>
<target datatype="html">Submission failed:</target> <target datatype="html">Submission failed:</target>
+44 -16
View File
@@ -17,7 +17,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">49,51</context> <context context-type="linenumber">84,86</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/layout/wizard-shell/wizard-shell.component.ts</context> <context context-type="sourcefile">src/app/shared/layout/wizard-shell/wizard-shell.component.ts</context>
@@ -1589,13 +1589,6 @@
<context context-type="linenumber">93</context> <context context-type="linenumber">93</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="validation.straat" datatype="html">
<source>Vul straat en huisnummer in.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/domain/change-request.machine.ts</context>
<context context-type="linenumber">43</context>
</context-group>
</trans-unit>
<trans-unit id="validation.straat2" datatype="html"> <trans-unit id="validation.straat2" datatype="html">
<source>Vul een straat en huisnummer in.</source> <source>Vul een straat en huisnummer in.</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -1691,6 +1684,13 @@
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="validation.telefoon" datatype="html">
<source>Voer een geldig telefoonnummer in, bijv. 0612345678.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/domain/value-objects/telefoonnummer.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="validation.uren" datatype="html"> <trans-unit id="validation.uren" datatype="html">
<source>Vul een geheel aantal in (0 of meer).</source> <source>Vul een geheel aantal in (0 of meer).</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -1783,45 +1783,73 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.success" datatype="html"> <trans-unit id="changeRequest.success" datatype="html">
<source> Uw adreswijziging is ontvangen (referentie <x id="INTERPOLATION" equiv-text="{{ referentie() }}"/>). U ontvangt binnen 5 werkdagen bericht. </source> <source> Uw wijziging is ontvangen (referentie <x id="INTERPOLATION" equiv-text="{{ referentie() }}"/>). U ontvangt binnen 5 werkdagen bericht. </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">33,35</context> <context context-type="linenumber">56,58</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.nieuwe" datatype="html"> <trans-unit id="changeRequest.nieuwe" datatype="html">
<source>Nieuwe wijziging doorgeven</source> <source>Nieuwe wijziging doorgeven</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">41,43</context> <context context-type="linenumber">64,66</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.heading" datatype="html"> <trans-unit id="changeRequest.heading" datatype="html">
<source>Adreswijziging doorgeven</source> <source>Contactgegevens wijzigen</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">45,46</context> <context context-type="linenumber">68,70</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.brpAdresLabel" datatype="html">
<source>Adres (BRP)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">72,73</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.brpAdresBron" datatype="html">
<source> Uw adres komt uit de Basisregistratie Personen en kan hier niet worden gewijzigd. Wijzig het bij uw gemeente. </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">75,78</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.telefoonLabel" datatype="html">
<source>Telefoonnummer</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">89,91</context>
</context-group>
</trans-unit>
<trans-unit id="changeRequest.telefoonPlaceholder" datatype="html">
<source>0612345678</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">101,102</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.failed" datatype="html"> <trans-unit id="changeRequest.failed" datatype="html">
<source>Het indienen is niet gelukt:</source> <source>Het indienen is niet gelukt:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">61,62</context> <context context-type="linenumber">108,109</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.submit" datatype="html"> <trans-unit id="changeRequest.submit" datatype="html">
<source>Wijziging indienen</source> <source>Wijziging indienen</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">86</context> <context context-type="linenumber">136</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="changeRequest.submitBezig" datatype="html"> <trans-unit id="changeRequest.submitBezig" datatype="html">
<source>Bezig met indienen…</source> <source>Bezig met indienen…</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context> <context context-type="sourcefile">src/app/registratie/ui/change-request-form/change-request-form.component.ts</context>
<context context-type="linenumber">87</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="dashboard.heading" datatype="html"> <trans-unit id="dashboard.heading" datatype="html">