CUB-43: Add inventory dashboard summary component
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m19s

This commit is contained in:
2026-04-27 21:10:16 -04:00
committed by rex-bot
parent 5ede6a8eb6
commit b7e61fab8a
6 changed files with 485 additions and 268 deletions

View File

@@ -1,176 +1,52 @@
import { Injectable, signal, computed } from '@angular/core';
import { Filament, classifyStockLevel } from '../models/filament.model';
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subscription } from 'rxjs';
import { signal } from '@angular/core';
import { Filament } 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.
* API base URL — matches the Extrudex backend.
* TODO: Move to environment config when environments are set up.
*/
@Injectable({
providedIn: 'root',
})
const API_BASE_URL = '/api/filaments';
/**
* Service for managing filament inventory data.
*
* Provides:
* - A reactive `filaments` signal for components to bind to
* - REST methods for GET, POST, DELETE endpoints
* - Real-time updates via SignalR should be layered on top when the hub is ready
*/
@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: '',
},
]);
private readonly http = inject(HttpClient);
/** Public read-only view of all filaments */
readonly filaments = this._filaments.asReadonly();
/** Reactive filament data — components read from this signal */
readonly filaments = signal<Filament[]>([]);
/**
* 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;
/** Fetch all filament spools and update the signal */
getFilaments(): Observable<Filament[]> {
const req = this.http.get<Filament[]>(API_BASE_URL);
req.subscribe({
next: (data) => this.filaments.set(data),
error: (err) => console.error('Failed to load filaments:', err),
});
return req;
}
/**
* 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));
/** Fetch a single filament by ID */
getFilament(id: string): Observable<Filament> {
return this.http.get<Filament>(`${API_BASE_URL}/${id}`);
}
/** Computed: count of active spools */
readonly activeCount = computed(() =>
this._filaments().filter((f) => f.isActive).length
);
/** Set filament data directly — used by components or SignalR handlers */
setFilaments(data: Filament[]): void {
this.filaments.set(data);
}
/** 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
);
}
/** Delete a filament spool by ID */
deleteFilament(id: string): Observable<void> {
return this.http.delete<void>(`${API_BASE_URL}/${id}`);
}
}