merge(dev): Re-apply CUB-34 changes after merge conflict resolution
Some checks failed
Dev Build / build-test (pull_request) Failing after 54s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 3s

This commit is contained in:
2026-04-27 17:02:25 -04:00
parent 5a577e1871
commit c05b9dd87d
10 changed files with 76 additions and 460 deletions

View File

@@ -1,6 +1,12 @@
<!-- Filament Inventory Table — with low stock indicators and delete actions -->
<!-- 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"
@@ -106,36 +112,22 @@
</td>
</ng-container>
<!-- Actions Column — delete button -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let filament">
<button mat-icon-button
type="button"
color="warn"
[attr.aria-label]="'Delete ' + filament.materialBaseName + ' ' + filament.colorName"
matTooltip="Delete spool"
matTooltipPosition="above"
[disabled]="deleting() === filament.id"
(click)="onDeleteClick(filament)">
@if (deleting() === filament.id) {
<mat-icon aria-hidden="true">hourglass_empty</mat-icon>
} @else {
<mat-icon aria-hidden="true">delete</mat-icon>
}
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns()"></tr>
<tr mat-row *matRowDef="let row; columns: columns();"
[class.row-critical]="classifyStockLevel(row) === 'critical'"
[class.row-low]="classifyStockLevel(row) === 'low'"
[class.row-deleting]="deleting() === row.id">
[class.row-low]="classifyStockLevel(row) === 'low'">
</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

@@ -235,20 +235,6 @@ mat-chip {
}
}
// Actions column
.actions-cell {
display: flex;
align-items: center;
justify-content: center;
}
// Row being deleted — subtle fade
:host ::ng-deep .row-deleting {
opacity: 0.5;
pointer-events: none;
transition: opacity 0.3s ease;
}
// Empty state
.empty-state {
display: flex;

View File

@@ -3,7 +3,6 @@ import {
Component,
Input,
computed,
inject,
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
@@ -13,21 +12,13 @@ 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 { MatButtonModule } from '@angular/material/button';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { FilamentFilterComponent, FilamentFilterState } from '../filament-filter/filament-filter.component';
import {
Filament,
StockLevel,
getRemainingPercent,
classifyStockLevel,
} from '../../models/filament.model';
import { FilamentService } from '../../services/filament.service';
import {
DeleteFilamentDialogComponent,
DeleteFilamentDialogData,
} from '../delete-filament-dialog/delete-filament-dialog.component';
/** Display column definitions for the filament table */
export type FilamentColumn =
@@ -37,8 +28,7 @@ export type FilamentColumn =
| 'serial'
| 'remaining'
| 'stockLevel'
| 'status'
| 'actions';
| 'status';
@Component({
selector: 'app-filament-table',
@@ -51,26 +41,17 @@ export type FilamentColumn =
MatProgressBarModule,
MatTooltipModule,
MatSortModule,
MatButtonModule,
MatDialogModule,
MatSnackBarModule,
FilamentFilterComponent,
],
templateUrl: './filament-table.component.html',
styleUrl: './filament-table.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilamentTableComponent {
private readonly dialog = inject(MatDialog);
private readonly snackBar = inject(MatSnackBar);
private readonly filamentService = inject(FilamentService);
/** Filament data input — reactive signal for live updates */
readonly filaments = signal<Filament[]>([]);
/** Whether a delete operation is in progress */
readonly deleting = signal<string | null>(null);
/** Columns to display — defaults to all columns including actions */
/** Columns to display — defaults to all columns */
@Input()
set displayedColumns(cols: FilamentColumn[]) {
this._displayedColumns.set(cols);
@@ -86,15 +67,29 @@ export class FilamentTableComponent {
'remaining',
'stockLevel',
'status',
'actions',
]);
/** 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(
@@ -233,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()];
@@ -274,50 +272,44 @@ export class FilamentTableComponent {
this.sortedFilaments.set(sorted);
}
/**
* Open the delete confirmation dialog for a filament spool.
* On confirm: calls DELETE endpoint and removes the row on success.
* On cancel: dialog dismissed, no action taken.
*/
onDeleteClick(filament: Filament): void {
const dialogData: DeleteFilamentDialogData = { filament };
const dialogRef = this.dialog.open(DeleteFilamentDialogComponent, {
data: dialogData,
width: '480px',
disableClose: true,
});
/** Handle filter changes from FilamentFilterComponent */
onFilterChange(state: FilamentFilterState): void {
this.filterState.set(state);
}
dialogRef.afterClosed().subscribe((confirmed: boolean | undefined) => {
if (!confirmed) {
return; // User cancelled — no action
/** 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;
}
}
// Mark as deleting for UI feedback
this.deleting.set(filament.id);
// Active only filter
if (filters.activeOnly && !filament.isActive) {
return false;
}
this.filamentService.deleteFilament(filament.id).subscribe({
next: () => {
// Remove the deleted filament from local data
const updated = this.filaments().filter((f) => f.id !== filament.id);
this.updateFilaments(updated);
this.deleting.set(null);
this.snackBar.open(
`Deleted ${filament.materialBaseName}${filament.colorName}`,
'Dismiss',
{ duration: 4000 }
);
},
error: () => {
this.deleting.set(null);
this.snackBar.open(
`Failed to delete ${filament.materialBaseName}${filament.colorName}. Please try again.`,
'Dismiss',
{ duration: 6000 }
);
},
});
});
return true;
}
/** Template helper: get remaining percent */