CUB-35: build add/edit filament modal with Angular Material Dialog
Some checks failed
Dev Build / build-test (pull_request) Failing after 2m28s
Some checks failed
Dev Build / build-test (pull_request) Failing after 2m28s
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
computed,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } 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 { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
import { Filament } from '../../models/filament.model';
|
||||
import {
|
||||
MaterialBase,
|
||||
MaterialFinish,
|
||||
MaterialModifier,
|
||||
} from '../../models/material.model';
|
||||
import {
|
||||
FilamentService,
|
||||
CreateFilamentRequest,
|
||||
UpdateFilamentRequest,
|
||||
} from '../../services/filament.service';
|
||||
|
||||
/** Data passed into the dialog from the opener. */
|
||||
export interface FilamentDialogData {
|
||||
/** If provided, the dialog opens in edit mode with pre-populated fields. */
|
||||
filament?: Filament;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-filament-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
templateUrl: './filament-dialog.component.html',
|
||||
styleUrl: './filament-dialog.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilamentDialogComponent {
|
||||
private readonly dialogRef = inject(MatDialogRef<FilamentDialogComponent>);
|
||||
private readonly data = inject<FilamentDialogData>(MAT_DIALOG_DATA);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly filamentService = inject(FilamentService);
|
||||
|
||||
/** Whether this dialog is in edit mode (has existing filament data). */
|
||||
readonly isEditMode = computed(() => !!this.data.filament);
|
||||
|
||||
/** Dialog title based on mode. */
|
||||
readonly dialogTitle = computed(() =>
|
||||
this.isEditMode() ? 'Edit Filament' : 'Add Filament'
|
||||
);
|
||||
|
||||
// ── Lookup data signals ──────────────────────────────────
|
||||
|
||||
/** All material bases for the base material dropdown. */
|
||||
readonly materialBases = signal<MaterialBase[]>([]);
|
||||
|
||||
/** Material finishes filtered by selected base material. */
|
||||
readonly filteredFinishes = signal<MaterialFinish[]>([]);
|
||||
|
||||
/** Material modifiers filtered by selected base material. */
|
||||
readonly filteredModifiers = signal<MaterialModifier[]>([]);
|
||||
|
||||
/** Whether material lookups are loading. */
|
||||
readonly lookupsLoading = signal(true);
|
||||
|
||||
/** Whether the save operation is in progress. */
|
||||
readonly saving = signal(false);
|
||||
|
||||
/** Server error message, if any. */
|
||||
readonly serverError = signal<string | null>(null);
|
||||
|
||||
// ── Form ─────────────────────────────────────────────────
|
||||
|
||||
readonly form: FormGroup = this.fb.group({
|
||||
materialBaseId: ['', Validators.required],
|
||||
materialFinishId: ['', Validators.required],
|
||||
materialModifierId: [null],
|
||||
brand: ['', [Validators.required, Validators.maxLength(200)]],
|
||||
colorName: ['', [Validators.required, Validators.maxLength(200)]],
|
||||
colorHex: ['#000000', [Validators.required, Validators.pattern(/^#[0-9A-Fa-f]{6}$/)]],
|
||||
weightTotalGrams: [1000, [Validators.required, Validators.min(0.01), Validators.max(100000)]],
|
||||
weightRemainingGrams: [1000, [Validators.required, Validators.min(0), Validators.max(100000)]],
|
||||
filamentDiameterMm: [1.75, [Validators.required, Validators.min(0.1), Validators.max(10)]],
|
||||
spoolSerial: ['', [Validators.required, Validators.maxLength(200)]],
|
||||
purchasePrice: [null, [Validators.min(0), Validators.max(1000000)]],
|
||||
purchaseDate: [null],
|
||||
isActive: [true],
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.loadLookups();
|
||||
this.patchFormIfEditing();
|
||||
this.setupCascadingFilters();
|
||||
}
|
||||
|
||||
// ── Data loading ─────────────────────────────────────────
|
||||
|
||||
/** Load material bases, finishes, and modifiers for dropdowns. */
|
||||
private loadLookups(): void {
|
||||
this.lookupsLoading.set(true);
|
||||
this.filamentService.getMaterialBases().subscribe({
|
||||
next: (bases) => {
|
||||
this.materialBases.set(bases);
|
||||
this.lookupsLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.lookupsLoading.set(false);
|
||||
this.serverError.set('Failed to load material options. Please try again.');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Pre-populate form fields when editing an existing filament. */
|
||||
private patchFormIfEditing(): void {
|
||||
if (this.data.filament) {
|
||||
const f = this.data.filament;
|
||||
this.form.patchValue({
|
||||
materialBaseId: f.materialBaseId,
|
||||
materialFinishId: f.materialFinishId,
|
||||
materialModifierId: f.materialModifierId,
|
||||
brand: f.brand,
|
||||
colorName: f.colorName,
|
||||
colorHex: f.colorHex,
|
||||
weightTotalGrams: f.weightTotalGrams,
|
||||
weightRemainingGrams: f.weightRemainingGrams,
|
||||
filamentDiameterMm: f.filamentDiameterMm,
|
||||
spoolSerial: f.spoolSerial,
|
||||
purchasePrice: f.purchasePrice,
|
||||
purchaseDate: f.purchaseDate ? new Date(f.purchaseDate) : null,
|
||||
isActive: f.isActive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Set up cascading filter: when base material changes, reload finishes & modifiers. */
|
||||
private setupCascadingFilters(): void {
|
||||
this.form.get('materialBaseId')!.valueChanges.subscribe((baseId: string | null) => {
|
||||
// Clear dependent selections when base changes
|
||||
this.form.get('materialFinishId')!.setValue('');
|
||||
this.form.get('materialModifierId')!.setValue(null);
|
||||
this.filteredFinishes.set([]);
|
||||
this.filteredModifiers.set([]);
|
||||
|
||||
if (!baseId) return;
|
||||
|
||||
this.filamentService.getMaterialFinishes(baseId).subscribe({
|
||||
next: (finishes) => this.filteredFinishes.set(finishes),
|
||||
error: () => this.filteredFinishes.set([]),
|
||||
});
|
||||
|
||||
this.filamentService.getMaterialModifiers(baseId).subscribe({
|
||||
next: (modifiers) => this.filteredModifiers.set(modifiers),
|
||||
error: () => this.filteredModifiers.set([]),
|
||||
});
|
||||
});
|
||||
|
||||
// If editing, trigger the cascading load for the pre-selected base
|
||||
if (this.data.filament) {
|
||||
const baseId = this.data.filament.materialBaseId;
|
||||
// We need to load finishes and modifiers for the pre-selected base
|
||||
// but also re-select the original finish and modifier after loading
|
||||
this.filamentService.getMaterialFinishes(baseId).subscribe({
|
||||
next: (finishes) => {
|
||||
this.filteredFinishes.set(finishes);
|
||||
// Re-patch finish after load
|
||||
this.form.get('materialFinishId')!.setValue(this.data.filament!.materialFinishId);
|
||||
},
|
||||
});
|
||||
this.filamentService.getMaterialModifiers(baseId).subscribe({
|
||||
next: (modifiers) => {
|
||||
this.filteredModifiers.set(modifiers);
|
||||
// Re-patch modifier after load
|
||||
this.form.get('materialModifierId')!.setValue(this.data.filament!.materialModifierId);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Actions ──────────────────────────────────────────────
|
||||
|
||||
/** Cancel and close the dialog without saving. */
|
||||
cancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
/** Submit the form — creates or updates the filament. */
|
||||
save(): void {
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cross-field validation: remaining weight must not exceed total weight
|
||||
const total = this.form.value.weightTotalGrams;
|
||||
const remaining = this.form.value.weightRemainingGrams;
|
||||
if (remaining > total) {
|
||||
this.form.get('weightRemainingGrams')!.setErrors({ exceedsTotal: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving.set(true);
|
||||
this.serverError.set(null);
|
||||
|
||||
const formValue = this.form.value;
|
||||
const request: CreateFilamentRequest | UpdateFilamentRequest = {
|
||||
materialBaseId: formValue.materialBaseId,
|
||||
materialFinishId: formValue.materialFinishId,
|
||||
materialModifierId: formValue.materialModifierId || null,
|
||||
brand: formValue.brand.trim(),
|
||||
colorName: formValue.colorName.trim(),
|
||||
colorHex: formValue.colorHex,
|
||||
weightTotalGrams: formValue.weightTotalGrams,
|
||||
weightRemainingGrams: formValue.weightRemainingGrams,
|
||||
filamentDiameterMm: formValue.filamentDiameterMm,
|
||||
spoolSerial: formValue.spoolSerial.trim(),
|
||||
purchasePrice: formValue.purchasePrice ?? null,
|
||||
purchaseDate: formValue.purchaseDate
|
||||
? new Date(formValue.purchaseDate).toISOString()
|
||||
: null,
|
||||
isActive: formValue.isActive,
|
||||
};
|
||||
|
||||
if (this.isEditMode()) {
|
||||
const id = this.data.filament!.id;
|
||||
this.filamentService.updateFilament(id, request).subscribe({
|
||||
next: (updated) => {
|
||||
this.saving.set(false);
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving.set(false);
|
||||
this.serverError.set(
|
||||
err?.error?.error || err?.message || 'Failed to update filament. Please try again.'
|
||||
);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.filamentService.createFilament(request).subscribe({
|
||||
next: (created) => {
|
||||
this.saving.set(false);
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (err) => {
|
||||
this.saving.set(false);
|
||||
this.serverError.set(
|
||||
err?.error?.error || err?.message || 'Failed to create filament. Please try again.'
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user