95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
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<string, any> | 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<DialogData>(MAT_DIALOG_DATA);
|
|
private ref = inject(MatDialogRef<CrudFormDialogComponent>);
|
|
|
|
readonly isEdit = !!this.data.entity;
|
|
readonly form: FormGroup;
|
|
readonly options = signal<Record<string, SelectOption[]>>({});
|
|
|
|
constructor() {
|
|
const group: Record<string, any> = {};
|
|
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<string, any> = {};
|
|
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); }
|
|
}
|