diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d0614db..b31e340 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,6 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@angular/animations": "^21.2.10", "@angular/cdk": "^21.2.8", "@angular/common": "^21.2.0", "@angular/compiler": "^21.2.0", @@ -327,21 +326,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/animations": { - "version": "21.2.10", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.10.tgz", - "integrity": "sha512-sIzAcxwtRCJ/fu0tK4mo1ooiEaDxJ+Nl6s9nK1D1NP1em12VX03Jx8CMixp/kVtgh4mZnm1x6psBB0FUz3U3Ug==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/core": "21.2.10" - } - }, "node_modules/@angular/build": { "version": "21.2.8", "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index 02eaba4..1e2cedb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,6 @@ "private": true, "packageManager": "npm@11.11.0", "dependencies": { - "@angular/animations": "^21.2.10", "@angular/cdk": "^21.2.8", "@angular/common": "^21.2.0", "@angular/compiler": "^21.2.0", diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index e66aad8..cb1270e 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,15 +1,11 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; -import { provideHttpClient, withFetch } from '@angular/common/http'; -import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), - provideRouter(routes), - provideHttpClient(withFetch()), - provideAnimationsAsync(), + provideRouter(routes) ] -}; \ No newline at end of file +}; diff --git a/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.html b/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.html deleted file mode 100644 index 71fe1c6..0000000 --- a/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.html +++ /dev/null @@ -1,78 +0,0 @@ - -

- - Delete Filament Spool? -

- - -

- You are about to permanently remove this filament spool from inventory. -

- - -
-
- Material - {{ filament.materialBaseName }}{{ filament.materialFinishName ? ' — ' + filament.materialFinishName : '' }}{{ filament.materialModifierName ? ' (' + filament.materialModifierName + ')' : '' }} -
- -
- Brand - {{ filament.brand }} -
- -
- Color - - - - {{ filament.colorName }} - -
- -
- Serial - {{ filament.spoolSerial }} -
- -
- Remaining - {{ formatWeight(filament.weightRemainingGrams) }} / {{ formatWeight(filament.weightTotalGrams) }} -
- -
- Status - - - {{ filament.isActive ? 'Active' : 'Inactive' }} - - -
-
- -

- - This action cannot be undone. -

-
- - - - - \ No newline at end of file diff --git a/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.scss b/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.scss deleted file mode 100644 index 6789bf4..0000000 --- a/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.scss +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Delete Filament Dialog Styles - * Touch-optimized confirmation dialog for spool removal - */ - -$spacing-unit: 8px; -$color-critical: #ef4444; -$color-inactive: #94a3b8; -$color-active: #22c55e; - -// Dialog title -h2[mat-dialog-title] { - display: flex; - align-items: center; - gap: $spacing-unit; - color: $color-critical; - - mat-icon { - font-size: 24px !important; - width: 24px !important; - height: 24px !important; - } -} - -// Description text -.dialog-description { - margin: 0 0 $spacing-unit * 2; - font-size: 14px; - line-height: 1.5; - color: var(--mat-sys-on-surface); -} - -// Spool details card -.spool-details { - display: flex; - flex-direction: column; - gap: $spacing-unit; - padding: $spacing-unit * 1.5; - background-color: var(--mat-sys-surface-container); - border-radius: 8px; - margin-bottom: $spacing-unit * 2; -} - -.detail-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: $spacing-unit * 0.5 0; - font-size: 14px; - - &:not(:last-child) { - border-bottom: 1px solid var(--mat-sys-outline-variant); - padding-bottom: $spacing-unit; - } -} - -.detail-label { - font-weight: 500; - color: var(--mat-sys-on-surface-variant); - flex-shrink: 0; -} - -.detail-value { - font-weight: 400; - color: var(--mat-sys-on-surface); - text-align: right; -} - -// Color swatch inline -.color-swatch-inline { - display: inline-block; - width: 18px; - height: 18px; - border-radius: 50%; - border: 1.5px solid rgba(0, 0, 0, 0.12); - vertical-align: middle; - margin-right: 4px; -} - -.color-value { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 4px; -} - -// Serial value — monospace -.serial-value { - font-family: 'JetBrains Mono', 'Roboto Mono', monospace; - font-size: 13px; - letter-spacing: 0.02em; -} - -// Status badge — matches filament table styling -.status-badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 10px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - - &.active { - background-color: rgba($color-active, 0.12); - color: $color-active; - } - - &.inactive { - background-color: rgba($color-inactive, 0.12); - color: $color-inactive; - } -} - -// Warning text -.dialog-warning { - display: flex; - align-items: center; - gap: $spacing-unit; - margin: 0; - font-size: 13px; - color: $color-critical; - - mat-icon { - font-size: 18px !important; - width: 18px !important; - height: 18px !important; - } -} - -// Dialog action buttons -mat-dialog-actions { - padding-top: $spacing-unit * 2; - - .cancel-button { - min-width: 80px; - } - - .confirm-button { - min-width: 120px; - - mat-icon { - font-size: 18px !important; - width: 18px !important; - height: 18px !important; - margin-right: 4px; - } - } -} \ No newline at end of file diff --git a/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.ts b/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.ts deleted file mode 100644 index d16a87a..0000000 --- a/frontend/src/app/components/delete-filament-dialog/delete-filament-dialog.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { - MAT_DIALOG_DATA, - MatDialogRef, - MatDialogModule, -} from '@angular/material/dialog'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatChipsModule } from '@angular/material/chips'; - -import { Filament } from '../../models/filament.model'; - -/** - * Data passed into the delete confirmation dialog. - */ -export interface DeleteFilamentDialogData { - filament: Filament; -} - -/** - * Delete confirmation dialog for filament spool removal. - * - * Displays spool details (material, brand, color, serial, remaining weight) - * and requires the user to confirm before deletion proceeds. - * Cancel dismisses the dialog with no action. - */ -@Component({ - selector: 'app-delete-filament-dialog', - standalone: true, - imports: [ - CommonModule, - MatDialogModule, - MatButtonModule, - MatIconModule, - MatChipsModule, - ], - templateUrl: './delete-filament-dialog.component.html', - styleUrl: './delete-filament-dialog.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DeleteFilamentDialogComponent { - private readonly dialogRef = inject( - MatDialogRef - ); - readonly data: DeleteFilamentDialogData = inject(MAT_DIALOG_DATA); - - /** The filament spool being considered for deletion */ - readonly filament = this.data.filament; - - /** Format weight for display in dialog */ - formatWeight(grams: number): string { - if (grams >= 1000) { - return `${(grams / 1000).toFixed(1)}kg`; - } - return `${Math.round(grams)}g`; - } - - /** Cancel — close dialog with false (no deletion) */ - onCancel(): void { - this.dialogRef.close(false); - } - - /** Confirm — close dialog with true (proceed with deletion) */ - onConfirm(): void { - this.dialogRef.close(true); - } -} \ No newline at end of file diff --git a/frontend/src/app/components/filament-filter/filament-filter.component.html b/frontend/src/app/components/filament-filter/filament-filter.component.html new file mode 100644 index 0000000..fd93087 --- /dev/null +++ b/frontend/src/app/components/filament-filter/filament-filter.component.html @@ -0,0 +1,76 @@ + + \ No newline at end of file diff --git a/frontend/src/app/components/filament-filter/filament-filter.component.scss b/frontend/src/app/components/filament-filter/filament-filter.component.scss new file mode 100644 index 0000000..8ebbaaf --- /dev/null +++ b/frontend/src/app/components/filament-filter/filament-filter.component.scss @@ -0,0 +1,134 @@ +/** + * Filament Filter Bar Styles + * Responsive filter layout for kiosk and mobile + */ + +$spacing-unit: 8px; + +.filament-filter-bar { + display: flex; + align-items: center; + gap: $spacing-unit * 2; + flex-wrap: wrap; + padding: $spacing-unit * 2 0; + margin-bottom: $spacing-unit * 2; +} + +// Form field sizing +.filter-field { + flex: 0 1 auto; + min-width: 160px; + + &.material-filter { + min-width: 200px; + } + + &.color-filter { + min-width: 180px; + } + + // Reduce vertical spacing inside filter fields + .mat-mdc-form-field-subscript-wrapper { + display: none; // No hint/error text needed for filters + } +} + +// Selected material chips +.selected-chips { + flex-wrap: wrap; + gap: 4px; +} + +.filter-chip { + font-size: 12px !important; + min-height: 24px !important; + + mat-icon { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; + } +} + +// Active filter icon +.filter-active-icon { + color: var(--mat-sys-primary); + font-size: 18px !important; + width: 18px !important; + height: 18px !important; +} + +// Checkbox styling +.filter-checkbox { + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; + user-select: none; + touch-action: manipulation; // Prevent zoom on double-tap + + .checkbox-icon { + font-size: 18px !important; + width: 18px !important; + height: 18px !important; + color: var(--mat-sys-on-surface-variant); + transition: color 0.2s ease; + + &.active { + color: var(--mat-sys-primary); + } + } +} + +// Clear filters button +.clear-filters-btn { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + + mat-icon { + font-size: 18px !important; + width: 18px !important; + height: 18px !important; + } +} + +// Responsive: stack filters vertically on small screens +@media (max-width: 768px) { + .filament-filter-bar { + flex-direction: column; + align-items: stretch; + gap: $spacing-unit; + } + + .filter-field { + width: 100%; + min-width: unset; + + &.material-filter, + &.color-filter { + min-width: unset; + } + } + + .filter-checkbox { + padding: $spacing-unit 0; + } + + .clear-filters-btn { + align-self: flex-start; + } +} + +// Extra-small screens (phone portrait) +@media (max-width: 480px) { + .filament-filter-bar { + padding: $spacing-unit 0; + margin-bottom: $spacing-unit; + } + + .filter-checkbox { + font-size: 13px; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/filament-filter/filament-filter.component.ts b/frontend/src/app/components/filament-filter/filament-filter.component.ts new file mode 100644 index 0000000..7559afc --- /dev/null +++ b/frontend/src/app/components/filament-filter/filament-filter.component.ts @@ -0,0 +1,158 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + computed, + signal, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { + Filament, + StockLevel, + classifyStockLevel, +} from '../../models/filament.model'; + +/** Filter state emitted by the filament filter component */ +export interface FilamentFilterState { + /** Selected material base names — empty means all */ + materialBaseNames: string[]; + + /** Color search text — empty string means all */ + colorSearch: string; + + /** Whether to show only low/critical stock */ + lowStockOnly: boolean; + + /** Whether to show only active spools */ + activeOnly: boolean; +} + +/** + * FilamentFilterComponent — Filter bar for the filament inventory list. + * + * Provides: + * - Material type multi-select filter + * - Color name text search + * - Low stock toggle (shows only critical/low spools) + * - Active-only toggle + * - Clear all filters action + */ +@Component({ + selector: 'app-filament-filter', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatSelectModule, + MatInputModule, + MatCheckboxModule, + MatIconModule, + MatChipsModule, + MatButtonModule, + MatTooltipModule, + ], + templateUrl: './filament-filter.component.html', + styleUrl: './filament-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FilamentFilterComponent { + /** Filament data input — used to derive material options */ + @Input() set filaments(value: Filament[]) { + this._filaments.set(value); + const materials = [...new Set(value.map((f) => f.materialBaseName))].sort(); + this.materialOptions.set(materials); + } + get filaments(): Filament[] { + return this._filaments(); + } + private readonly _filaments = signal([]); + + /** Available material base names derived from filament data */ + readonly materialOptions = signal([]); + + /** Selected material base names */ + readonly selectedMaterials = signal([]); + + /** Color search text */ + readonly colorSearch = signal(''); + + /** Low stock only toggle */ + readonly lowStockOnly = signal(false); + + /** Active only toggle */ + readonly activeOnly = signal(false); + + /** Computed: whether any filters are active */ + readonly hasActiveFilters = computed( + () => + this.selectedMaterials().length > 0 || + this.colorSearch().trim().length > 0 || + this.lowStockOnly() || + this.activeOnly() + ); + + /** Emits the current filter state whenever filters change */ + @Output() readonly filterChange = new EventEmitter(); + + /** Handle material selection change */ + onMaterialChange(selected: string[]): void { + this.selectedMaterials.set(selected); + this.emitFilterState(); + } + + /** Handle color search input */ + onColorSearchChange(value: string): void { + this.colorSearch.set(value); + this.emitFilterState(); + } + + /** Handle low stock toggle */ + onLowStockToggle(checked: boolean): void { + this.lowStockOnly.set(checked); + this.emitFilterState(); + } + + /** Handle active-only toggle */ + onActiveOnlyToggle(checked: boolean): void { + this.activeOnly.set(checked); + this.emitFilterState(); + } + + /** Remove a single material chip */ + removeMaterial(material: string): void { + const updated = this.selectedMaterials().filter((m) => m !== material); + this.selectedMaterials.set(updated); + this.emitFilterState(); + } + + /** Clear all filters */ + clearAll(): void { + this.selectedMaterials.set([]); + this.colorSearch.set(''); + this.lowStockOnly.set(false); + this.activeOnly.set(false); + this.emitFilterState(); + } + + /** Emit the current filter state */ + private emitFilterState(): void { + this.filterChange.emit({ + materialBaseNames: this.selectedMaterials(), + colorSearch: this.colorSearch().trim().toLowerCase(), + lowStockOnly: this.lowStockOnly(), + activeOnly: this.activeOnly(), + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/filament-table/filament-table.component.html b/frontend/src/app/components/filament-table/filament-table.component.html index e0aa433..5beccd4 100644 --- a/frontend/src/app/components/filament-table/filament-table.component.html +++ b/frontend/src/app/components/filament-table/filament-table.component.html @@ -1,6 +1,12 @@ - +
+ + + @if (criticalCount() > 0) {