feat: Admin Area General Settings and Categories 1.14

This commit is contained in:
OpenVelo Agent
2026-07-22 18:38:12 +00:00
parent 63f2746e56
commit 493e4b244d
5 changed files with 455 additions and 298 deletions
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AdminCategory } from '../../../core/services/admin.service';
@@ -15,6 +15,41 @@ export interface CategoryFormSubmit {
iconFile?: File;
}
export type CategoryFormGroup = FormGroup<{
name: FormControl<string>;
abbreviation: FormControl<string>;
description: FormControl<string>;
}>;
export function syncCategoryForm(
form: CategoryFormGroup,
mode: CategoryFormMode,
category: AdminCategory | null,
): { abbreviationReadonly: boolean; iconPreview: string | null } {
if (mode === 'edit' && category) {
form.controls.name.setValue(category.name ?? '', { emitEvent: false });
form.controls.abbreviation.setValue(category.abbreviation ?? '', { emitEvent: false });
form.controls.description.setValue(category.description ?? '', { emitEvent: false });
form.controls.name.markAsUntouched();
form.controls.name.markAsPristine();
form.controls.abbreviation.markAsUntouched();
form.controls.abbreviation.markAsPristine();
form.controls.description.markAsUntouched();
form.controls.description.markAsPristine();
return { abbreviationReadonly: !!category.isSystem, iconPreview: category.iconPath || null };
}
form.controls.name.setValue('', { emitEvent: false });
form.controls.abbreviation.setValue('', { emitEvent: false });
form.controls.description.setValue('', { emitEvent: false });
form.controls.name.markAsUntouched();
form.controls.name.markAsPristine();
form.controls.abbreviation.markAsUntouched();
form.controls.abbreviation.markAsPristine();
form.controls.description.markAsUntouched();
form.controls.description.markAsPristine();
return { abbreviationReadonly: false, iconPreview: null };
}
@Component({
selector: 'app-category-form-modal',
standalone: true,
@@ -69,6 +104,7 @@ export interface CategoryFormSubmit {
export class CategoryFormModalComponent {
private readonly fb = inject(FormBuilder);
private readonly destroyRef = inject(DestroyRef);
private readonly cdr = inject(ChangeDetectorRef);
readonly open = input(false);
readonly mode = input<CategoryFormMode>('create');
@@ -82,31 +118,23 @@ export class CategoryFormModalComponent {
readonly submitting = signal(false);
readonly abbreviationReadonly = signal(false);
readonly form = this.fb.nonNullable.group({
readonly form: CategoryFormGroup = this.fb.nonNullable.group({
name: this.fb.nonNullable.control('', [Validators.required, Validators.maxLength(120)]),
abbreviation: this.fb.nonNullable.control('', [Validators.required, Validators.minLength(2), Validators.maxLength(6)]),
description: this.fb.nonNullable.control(''),
});
constructor() {
effect(() => {
const c = this.category();
const m = this.mode();
if (m === 'edit' && c) {
this.form.patchValue({
name: c.name,
abbreviation: c.abbreviation,
description: c.description,
});
this.abbreviationReadonly.set(c.isSystem);
this.iconPreview.set(c.iconPath || null);
} else {
this.form.patchValue({ name: '', abbreviation: '', description: '' });
this.abbreviationReadonly.set(false);
this.iconPreview.set(null);
}
this.iconFile.set(null);
});
effect(
() => {
const result = syncCategoryForm(this.form, this.mode(), this.category());
this.abbreviationReadonly.set(result.abbreviationReadonly);
this.iconPreview.set(result.iconPreview);
this.iconFile.set(null);
this.cdr.markForCheck();
},
{ allowSignalWrites: true },
);
}
onFile(ev: Event): void {