CUB-34: add filament filter bar with material type, color, and low stock filters
Some checks failed
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / build-test (pull_request) Failing after 54s
Dev Build / notify-failure (pull_request) Successful in 6s

This commit is contained in:
2026-04-27 15:08:31 -04:00
parent 8a2f97d2cd
commit d207c49ffd
5 changed files with 445 additions and 3 deletions

View File

@@ -1,6 +1,12 @@
<!-- Filament Inventory Table — with low stock indicators -->
<!-- Filament Inventory Table — with filters and low stock indicators -->
<div class="filament-table-container" role="region" aria-label="Filament inventory">
<!-- Filter Bar -->
<app-filament-filter
[filaments]="allFilaments()"
(filterChange)="onFilterChange($event)"
aria-label="Filter filament inventory" />
<!-- Low Stock Alert Banner — shown when critical or low stock spools exist -->
@if (criticalCount() > 0) {
<div class="alert-banner critical" role="alert">
@@ -16,7 +22,7 @@
<!-- Filament Table -->
<table mat-table
[dataSource]="sortedFilaments()"
[dataSource]="filteredFilaments()"
matSort
(matSortChange)="sortData($event)"
class="filament-table"
@@ -113,7 +119,15 @@
</tr>
</table>
<!-- Empty state -->
<!-- Filtered empty state -->
@if (filteredFilaments().length === 0 && filaments().length > 0) {
<div class="empty-state" role="status">
<mat-icon aria-hidden="true">filter_alt_off</mat-icon>
<p>No filaments match the current filters</p>
</div>
}
<!-- No data empty state -->
@if (filaments().length === 0) {
<div class="empty-state" role="status">
<mat-icon aria-hidden="true">inventory_2</mat-icon>

View File

@@ -12,6 +12,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSortModule, Sort } from '@angular/material/sort';
import { FilamentFilterComponent, FilamentFilterState } from '../filament-filter/filament-filter.component';
import {
Filament,
StockLevel,
@@ -40,6 +41,7 @@ export type FilamentColumn =
MatProgressBarModule,
MatTooltipModule,
MatSortModule,
FilamentFilterComponent,
],
templateUrl: './filament-table.component.html',
styleUrl: './filament-table.component.scss',
@@ -70,9 +72,24 @@ export class FilamentTableComponent {
/** Default columns for template binding */
readonly columns = this._displayedColumns;
/** Current filter state */
readonly filterState = signal<FilamentFilterState>({
materialBaseNames: [],
colorSearch: '',
lowStockOnly: false,
activeOnly: false,
});
/** Sorted filament data */
readonly sortedFilaments = signal<Filament[]>([]);
/** Computed: filtered + sorted filament data for display */
readonly filteredFilaments = computed(() => {
const data = this.sortedFilaments();
const filters = this.filterState();
return data.filter((f) => this.matchesFilter(f, filters));
});
/** Computed: count of low/critical spools */
readonly lowStockCount = computed(() =>
this.filaments().filter(
@@ -211,6 +228,9 @@ export class FilamentTableComponent {
this.sortedFilaments.set([...data]);
}
/** All filament data — for the filter component to derive material options */
readonly allFilaments = this.filaments;
/** Handle sort changes from MatSort */
sortData(sort: Sort): void {
const data = [...this.filaments()];
@@ -252,6 +272,46 @@ export class FilamentTableComponent {
this.sortedFilaments.set(sorted);
}
/** Handle filter changes from FilamentFilterComponent */
onFilterChange(state: FilamentFilterState): void {
this.filterState.set(state);
}
/** Check if a filament matches the current filter state */
private matchesFilter(filament: Filament, filters: FilamentFilterState): boolean {
// Material filter — empty means all
if (
filters.materialBaseNames.length > 0 &&
!filters.materialBaseNames.includes(filament.materialBaseName)
) {
return false;
}
// Color search — empty means all
if (
filters.colorSearch &&
!filament.colorName.toLowerCase().includes(filters.colorSearch) &&
!filament.colorHex.toLowerCase().includes(filters.colorSearch)
) {
return false;
}
// Low stock filter — show only critical/low
if (filters.lowStockOnly) {
const level = classifyStockLevel(filament);
if (level !== 'critical' && level !== 'low') {
return false;
}
}
// Active only filter
if (filters.activeOnly && !filament.isActive) {
return false;
}
return true;
}
/** Template helper: get remaining percent */
getRemainingPercent = getRemainingPercent;