import { Component, inject, signal } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { ApiService } from '../../core/services/api.service'; import { CrudConfig, FieldConfig, SelectOption } from './crud-config'; interface DialogData { config: CrudConfig; entity: Record | null; } @Component({ selector: 'app-crud-form-dialog', imports: [ ReactiveFormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatDatepickerModule, MatButtonModule, MatIconModule, ], templateUrl: './crud-form-dialog.component.html', styleUrl: './crud-form-dialog.component.scss', }) export class CrudFormDialogComponent { private fb = inject(FormBuilder); private api = inject(ApiService); readonly data = inject(MAT_DIALOG_DATA); private ref = inject(MatDialogRef); readonly isEdit = !!this.data.entity; readonly form: FormGroup; readonly options = signal>({}); constructor() { const group: Record = {}; for (const f of this.data.config.fields) { const validators = []; if (f.required) validators.push(Validators.required); if (f.min != null) validators.push(Validators.min(f.min)); if (f.max != null) validators.push(Validators.max(f.max)); if (f.maxLength != null) validators.push(Validators.maxLength(f.maxLength)); group[f.key] = [this.initialValue(f), validators]; } this.form = this.fb.group(group); // Load lookup-backed select options. for (const f of this.data.config.fields) { if (f.options) { this.options.update((o) => ({ ...o, [f.key]: f.options! })); } else if (f.lookup) { this.api.lookup(f.lookup).subscribe((items) => { this.options.update((o) => ({ ...o, [f.key]: items.map((i) => ({ value: i.id, label: i.name })) })); }); } } } private initialValue(f: FieldConfig): any { const raw = this.data.entity?.[f.key]; if (raw == null || raw === '') return f.type === 'select' ? null : null; if (f.type === 'date') return new Date(raw); if (f.type === 'datetime') return this.toLocalInput(new Date(raw)); return raw; } private toLocalInput(d: Date): string { const p = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`; } private toDateOnly(d: Date): string { const p = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; } optionsFor(key: string): SelectOption[] { return this.options()[key] ?? []; } save() { if (this.form.invalid) { this.form.markAllAsTouched(); return; } const payload: Record = {}; for (const f of this.data.config.fields) { let v = this.form.value[f.key]; if (v === '' || v === undefined) v = null; if (v != null && f.type === 'date' && v instanceof Date) v = this.toDateOnly(v); if (v != null && f.type === 'number') v = Number(v); payload[f.key] = v; } this.ref.close(payload); } cancel() { this.ref.close(null); } }