CUB-43: add inventory dashboard summary component with FilamentService

This commit is contained in:
2026-04-27 18:11:30 -04:00
parent f70495a85c
commit e56aa3ba39
7 changed files with 396 additions and 124 deletions

View File

@@ -0,0 +1,176 @@
import { Injectable, signal, computed } from '@angular/core';
import { Filament, classifyStockLevel } from '../models/filament.model';
/**
* FilamentService — shared reactive state for filament inventory.
*
* Provides a single source of truth for filament data across components.
* Both FilamentTableComponent and InventorySummaryComponent read from this service.
* Data is updated via setFilaments() — called by SignalR handlers or HTTP load.
*/
@Injectable({
providedIn: 'root',
})
export class FilamentService {
/** Primary data store — reactive signal */
private readonly _filaments = signal<Filament[]>([
{
id: '1',
materialBaseId: 'm1',
materialBaseName: 'PLA',
materialFinishId: 'f1',
materialFinishName: 'Basic',
materialModifierId: null,
materialModifierName: null,
brand: 'Bambu Lab',
colorName: 'White',
colorHex: '#F5F5F5',
weightTotalGrams: 1000,
weightRemainingGrams: 850,
filamentDiameterMm: 1.75,
spoolSerial: 'SN-001',
purchasePrice: 25.00,
purchaseDate: '2026-01-15T00:00:00Z',
isActive: true,
createdAt: '2026-01-15T00:00:00Z',
updatedAt: '2026-04-20T00:00:00Z',
qrCodeUrl: '',
},
{
id: '2',
materialBaseId: 'm2',
materialBaseName: 'PETG',
materialFinishId: 'f2',
materialFinishName: 'Matte',
materialModifierId: 'mod1',
materialModifierName: 'Carbon Fiber',
brand: 'Polymaker',
colorName: 'Fire Engine Red',
colorHex: '#FF0000',
weightTotalGrams: 1000,
weightRemainingGrams: 80,
filamentDiameterMm: 1.75,
spoolSerial: 'SN-002',
purchasePrice: 35.00,
purchaseDate: '2026-02-01T00:00:00Z',
isActive: true,
createdAt: '2026-02-01T00:00:00Z',
updatedAt: '2026-04-25T00:00:00Z',
qrCodeUrl: '',
},
{
id: '3',
materialBaseId: 'm1',
materialBaseName: 'PLA',
materialFinishId: 'f1',
materialFinishName: 'Basic',
materialModifierId: null,
materialModifierName: null,
brand: 'eSun',
colorName: 'Sky Blue',
colorHex: '#87CEEB',
weightTotalGrams: 1000,
weightRemainingGrams: 200,
filamentDiameterMm: 1.75,
spoolSerial: 'SN-003',
purchasePrice: 20.00,
purchaseDate: '2026-03-10T00:00:00Z',
isActive: true,
createdAt: '2026-03-10T00:00:00Z',
updatedAt: '2026-04-26T00:00:00Z',
qrCodeUrl: '',
},
{
id: '4',
materialBaseId: 'm3',
materialBaseName: 'ABS',
materialFinishId: 'f1',
materialFinishName: 'Basic',
materialModifierId: null,
materialModifierName: null,
brand: 'Hatchbox',
colorName: 'Black',
colorHex: '#1A1A1A',
weightTotalGrams: 1000,
weightRemainingGrams: 450,
filamentDiameterMm: 1.75,
spoolSerial: 'SN-004',
purchasePrice: 22.00,
purchaseDate: null,
isActive: true,
createdAt: '2026-01-20T00:00:00Z',
updatedAt: '2026-04-18T00:00:00Z',
qrCodeUrl: '',
},
{
id: '5',
materialBaseId: 'm1',
materialBaseName: 'PLA',
materialFinishId: 'f3',
materialFinishName: 'Silk',
materialModifierId: null,
materialModifierName: null,
brand: 'Overturn',
colorName: 'Gold',
colorHex: '#FFD700',
weightTotalGrams: 500,
weightRemainingGrams: 15,
filamentDiameterMm: 1.75,
spoolSerial: 'SN-005',
purchasePrice: 28.00,
purchaseDate: null,
isActive: false,
createdAt: '2025-12-01T00:00:00Z',
updatedAt: '2026-04-01T00:00:00Z',
qrCodeUrl: '',
},
]);
/** Public read-only view of all filaments */
readonly filaments = this._filaments.asReadonly();
/**
* Replace the full filament list.
* Called by SignalR handlers, HTTP responses, or test setup.
*/
setFilaments(data: Filament[]): void {
this._filaments.set(data);
}
/**
* Update or insert a single filament by id.
* Called when a real-time SignalR update arrives for one spool.
*/
upsertFilament(updated: Filament): void {
this._filaments.update((current) => {
const idx = current.findIndex((f) => f.id === updated.id);
if (idx === -1) {
return [...current, updated];
}
const copy = [...current];
copy[idx] = updated;
return copy;
});
}
/**
* Remove a filament by id.
* Called when a spool is deleted via real-time event.
*/
removeFilament(id: string): void {
this._filaments.update((current) => current.filter((f) => f.id !== id));
}
/** Computed: count of active spools */
readonly activeCount = computed(() =>
this._filaments().filter((f) => f.isActive).length
);
/** Computed: count of low or critical stock spools */
readonly lowStockCount = computed(() =>
this._filaments().filter((f) => {
const level = classifyStockLevel(f);
return level === 'low' || level === 'critical';
}).length
);
}