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); private readonly data = inject(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([]); /** Material finishes filtered by selected base material. */ readonly filteredFinishes = signal([]); /** Material modifiers filtered by selected base material. */ readonly filteredModifiers = signal([]); /** 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(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.' ); }, }); } } }