277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
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.'
|
|
);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
} |