feat(stamdata): admin stamdata maintenance editor (beheer)
Realizes ADR-0004's "future low-code editor that commits a PR": an
admin-only stamdata maintenance editor built on the stamdata-as-code
foundation.
Backend: `professions` moves from a hardcoded C# dictionary to an embedded
`professions.json` data-file (typed as `ProfessionMapping`) with valid-time
(geldigVan/geldigTot, half-open). A generic, reflection-driven
StamdataCatalog/StamdataTable/StamdataFile describes every table so one
endpoint pair + one grid editor serve all of them; add a table in one line.
Two read-only, admin-gated endpoints (GET /stamdata, GET /stamdata/{table}
?peildatum=) — no runtime write path. Generic build gate
`Every_catalog_table_is_valid` (keys non-blank, no overlapping validity,
well-formed windows).
Frontend: new `beheer` context (route beheer/stamdata, capabilityGuard
'stamdata:edit'). A schema-driven grid editor edits rows locally; download()
emits {table}.json for the admin to commit as a reviewed PR (no mutation
command — the CI build + StamdataValidationTests stay the authority).
Full gate GREEN both sides; gen:api leaves no drift; new stamdata story
passes axe. See WP-29 + ADR-0004.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { Component, computed, effect, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
import { AccessStore } from '@shared/application/access.store';
|
||||
import { StamdataStore } from '@beheer/application/stamdata.store';
|
||||
import { StamdataTableEditorComponent } from '@beheer/ui/stamdata-table-editor/stamdata-table-editor.component';
|
||||
|
||||
/**
|
||||
* Page: thin container for the stamdata maintenance editor (ADR-0004). Deny-by-default
|
||||
* capability gate (`stamdata:edit`) — a denial alert for non-admins, the generic grid for
|
||||
* admins. Loads once the capability resolves; wires store commands to the organism.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-stamdata-page',
|
||||
imports: [PageShellComponent, AlertComponent, ButtonComponent, ...ASYNC, StamdataTableEditorComponent],
|
||||
template: `
|
||||
<app-page-shell [heading]="heading" [intro]="intro" backLink="/dashboard">
|
||||
@if (!access.ready()) {
|
||||
<!-- wait for /me before deciding — avoids flashing the denial to an admin -->
|
||||
} @else if (!canEdit()) {
|
||||
<app-alert type="error">{{ deniedText }}</app-alert>
|
||||
} @else {
|
||||
<app-async [data]="store.remoteData()">
|
||||
<ng-template appAsyncError>
|
||||
<app-alert type="error">{{ failedText }}</app-alert>
|
||||
<app-button variant="secondary" (click)="reload()">{{ retryText }}</app-button>
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (store.table(); as table) {
|
||||
<app-stamdata-table-editor
|
||||
[table]="table"
|
||||
[rows]="store.rows()"
|
||||
[errors]="store.errors()"
|
||||
[counts]="store.counts()"
|
||||
[previewDate]="store.previewDate()"
|
||||
[canDownload]="store.canDownload()"
|
||||
[tables]="store.tables()"
|
||||
[selectedTableId]="store.selectedTableId()"
|
||||
(selectTable)="store.selectTable($event)"
|
||||
(cellEdited)="store.editCell($event.row, $event.column, $event.value)"
|
||||
(rowAdded)="store.addRow()"
|
||||
(rowRemoved)="store.removeRow($event)"
|
||||
(previewDateChanged)="store.setPreviewDate($event)"
|
||||
(download)="store.download()"
|
||||
/>
|
||||
}
|
||||
</ng-template>
|
||||
</app-async>
|
||||
}
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class StamdataPage {
|
||||
protected store = inject(StamdataStore);
|
||||
protected access = inject(AccessStore);
|
||||
|
||||
protected canEdit = computed(() => this.access.can('stamdata:edit'));
|
||||
|
||||
protected heading = $localize`:@@beheer.page.heading:Stamdata onderhouden`;
|
||||
protected intro = $localize`:@@beheer.page.intro:Beheer de business-tabellen die de registratie stuurt. Wijzigingen worden als JSON gedownload en via een pull request toegepast; de build blijft de bewaker.`;
|
||||
protected deniedText = $localize`:@@beheer.page.denied:U hebt geen rechten om stamdata te onderhouden.`;
|
||||
protected failedText = $localize`:@@beheer.page.failed:De stamdata kon niet worden geladen.`;
|
||||
protected retryText = $localize`:@@beheer.page.retry:Opnieuw proberen`;
|
||||
|
||||
private loadRequested = false;
|
||||
constructor() {
|
||||
// Load once the capability resolves to `allowed` (a 403 GET would be wasted otherwise).
|
||||
// Depends only on canEdit() + a plain flag — never on the store model, so dispatching
|
||||
// `Loading` inside load() can't retrigger this effect (the WP-26 runaway-loop lesson).
|
||||
effect(() => {
|
||||
if (this.canEdit() && !this.loadRequested) {
|
||||
this.loadRequested = true;
|
||||
void this.store.load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected reload() {
|
||||
void this.store.load();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user