import { Component, computed, input, output } from '@angular/core'; import { ButtonComponent } from '@shared/ui/atoms/button/button.component'; import { ChangeCounts, StamColumn, StamRow, StamTable, activeOn } from '@beheer/domain/stamdata'; interface DisplayRow { row: StamRow; index: number; } /** * Organism: the GENERIC stamdata grid. It renders entirely from the reflected column * schema — one input per column type (native `date`/`number`, `enum` select, text) — so a * new stamdata table needs zero UI code here. The "geldig op" control filters to the rows * valid on a date (read-only preview); edits and download work on the full set. Emits * intent; the store owns state (CLAUDE.md §1). */ @Component({ selector: 'app-stamdata-table-editor', imports: [ButtonComponent], styles: [ ` :host { display: block; } .toolbar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: end; margin-block-end: 1rem; } .field label { display: block; font-size: 0.85em; color: var(--rhc-color-foreground-subtle); } table { inline-size: 100%; } .err { color: var(--rhc-color-rood-500); font-size: 0.85em; } .footer { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin-block-start: 1rem; } .counts { color: var(--rhc-color-foreground-subtle); } .hint { margin-block-start: 0.5rem; color: var(--rhc-color-foreground-subtle); font-size: 0.9em; } `, ], template: `
@if (tables().length > 1) {
} @if (table().temporal) {
@if (previewing()) { {{ showAll }} } }
@if (previewing()) {

{{ previewNote }}

} @for (col of table().columns; track col.name) { } @for (item of display(); track item.index) { @for (col of table().columns; track col.name) { } }
{{ col.name }}{{ previewing() ? '' : actionsLabel }}
@if (col.type === 'enum') { } @else { } @if (!previewing()) { @if (table().temporal) { {{ expireLabel }} } {{ removeLabel }} } @if (errors()[item.index]) { {{ errors()[item.index] }} }
@if (!previewing()) {

{{ applyHint }}

} `, }) export class StamdataTableEditorComponent { table = input.required(); rows = input.required(); errors = input.required(); counts = input.required(); previewDate = input(''); canDownload = input(false); canUndo = input(false); canRedo = input(false); tables = input([]); selectedTableId = input(null); selectTable = output(); cellEdited = output<{ row: number; column: string; value: string }>(); rowAdded = output(); rowRemoved = output(); previewDateChanged = output(); download = output(); undo = output(); redo = output(); protected previewing = computed(() => this.previewDate() !== ''); protected display = computed(() => this.rows() .map((row, index) => ({ row, index })) .filter(({ row }) => !this.previewing() || activeOn(this.table(), row, this.previewDate())), ); protected inputType(col: StamColumn): string { return col.type === 'date' ? 'date' : col.type === 'number' ? 'number' : 'text'; } protected cellLabel(col: StamColumn, index: number): string { return `${col.name} — rij ${index + 1}`; } protected asValue(e: Event): string { return (e.target as HTMLInputElement | HTMLSelectElement).value; } private addedWord = $localize`:@@beheer.added:toegevoegd`; private editedWord = $localize`:@@beheer.edited:gewijzigd`; private removedWord = $localize`:@@beheer.removed:verwijderd`; protected countsLabel = computed(() => { const c = this.counts(); return `${c.added} ${this.addedWord} · ${c.edited} ${this.editedWord} · ${c.removed} ${this.removedWord}`; }); protected tableLabel = $localize`:@@beheer.table:Tabel`; protected peildatumLabel = $localize`:@@beheer.peildatum:Toon geldig op`; protected showAll = $localize`:@@beheer.showAll:Toon alles`; protected previewNote = $localize`:@@beheer.previewNote:Voorbeeld: alleen de rijen die op deze datum geldig zijn. Bewerken staat uit.`; protected actionsLabel = $localize`:@@beheer.actions:Acties`; protected removeLabel = $localize`:@@beheer.remove:Verwijderen`; protected expireLabel = $localize`:@@beheer.expire:Sluiten per vandaag`; private removeConfirm = $localize`:@@beheer.removeConfirm: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.`; /** Deletions can orphan a reference (the CI gate catches it); confirm first. */ protected onRemove(index: number) { if (confirm(this.removeConfirm)) this.rowRemoved.emit(index); } /** Steer temporal tables toward expiring (close the validity per today) over hard delete — preserves history and can't orphan a reference that was valid earlier. */ protected onExpire(index: number) { const col = this.table().columns.find((c) => /geldigtot/i.test(c.name)); if (col) this.cellEdited.emit({ row: index, column: col.name, value: this.today }); } private today = new Date().toISOString().slice(0, 10); protected undoLabel = $localize`:@@beheer.undo:Ongedaan maken`; protected redoLabel = $localize`:@@beheer.redo:Opnieuw uitvoeren`; protected addRowLabel = $localize`:@@beheer.addRow:Rij toevoegen`; protected downloadLabel = $localize`:@@beheer.download:Download JSON`; protected applyHint = $localize`:@@beheer.applyHint:Wijzigingen worden als JSON-bestand gedownload en via een pull request toegepast — de build (CI) controleert ze.`; }