import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, inject, signal, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { FilamentService } from '../../services/filament.service'; import { classifyStockLevel, } from '../../models/filament.model'; import { Subscription } from 'rxjs'; /** * Inventory Dashboard Summary — shows filament inventory at a glance. * * Displays: * - Total filament spool count * - Low stock count (spools ≤25% remaining, i.e. "low" or "critical") * - Estimated total filament value (sum of purchase prices for active spools) * * Data is sourced from the shared FilamentService signal, * which is loaded on init and can be refreshed via refresh(). */ @Component({ selector: 'app-inventory-summary', standalone: true, imports: [ CommonModule, MatIconModule, MatChipsModule, MatTooltipModule, MatProgressBarModule, ], templateUrl: './inventory-summary.component.html', styleUrls: ['./inventory-summary.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class InventorySummaryComponent implements OnInit, OnDestroy { private readonly filamentService = inject(FilamentService); private subscription: Subscription | null = null; /** All filament data — reactive signal from shared service */ readonly filaments = this.filamentService.filaments; /** Loading state */ readonly loading = signal(true); /** Error state */ readonly error = signal(null); /** Computed: total number of filament spools */ readonly totalCount = computed(() => this.filaments().length); /** Computed: count of active spools */ readonly activeCount = computed( () => this.filaments().filter((f) => f.isActive).length ); /** Computed: count of low/critical stock spools (≤25% remaining) */ readonly lowStockCount = computed( () => this.filaments().filter( (f) => classifyStockLevel(f) === 'low' || classifyStockLevel(f) === 'critical' ).length ); /** Computed: count of critically low spools (≤10% remaining) */ readonly criticalCount = computed( () => this.filaments().filter((f) => classifyStockLevel(f) === 'critical') .length ); /** Computed: estimated total value of active spools */ readonly totalValue = computed(() => this.filaments() .filter((f) => f.isActive && f.purchasePrice !== null) .reduce((sum, f) => sum + (f.purchasePrice ?? 0), 0) ); /** Computed: total remaining weight across all spools in grams */ readonly totalRemainingGrams = computed(() => this.filaments().reduce((sum, f) => sum + f.weightRemainingGrams, 0) ); /** Computed: total capacity weight across all spools in grams */ readonly totalCapacityGrams = computed(() => this.filaments().reduce((sum, f) => sum + f.weightTotalGrams, 0) ); /** Computed: overall remaining percentage */ readonly overallRemainingPercent = computed(() => { const capacity = this.totalCapacityGrams(); if (capacity <= 0) return 0; return Math.round( (this.totalRemainingGrams() / capacity) * 100 ); }); /** Computed: whether to show a low-stock alert */ readonly hasLowStock = computed(() => this.lowStockCount() > 0); /** Computed: whether to show a critical-stock alert */ readonly hasCritical = computed(() => this.criticalCount() > 0); /** Computed: status label for the inventory health */ readonly healthLabel = computed(() => { if (this.hasCritical()) return 'Critical Stock'; if (this.hasLowStock()) return 'Low Stock Alert'; return 'Stock Healthy'; }); /** Computed: health status color class */ readonly healthClass = computed(() => { if (this.hasCritical()) return 'critical'; if (this.hasLowStock()) return 'low'; return 'healthy'; }); ngOnInit(): void { this.loadFilaments(); } ngOnDestroy(): void { this.subscription?.unsubscribe(); } /** Load filament data from the API via FilamentService */ loadFilaments(): void { this.loading.set(true); this.error.set(null); this.subscription = this.filamentService.getFilaments().subscribe({ next: () => { this.loading.set(false); }, error: (err) => { console.error('Failed to load filaments:', err); this.error.set('Failed to load inventory data'); this.loading.set(false); }, }); } /** Refresh data — called externally when data changes (e.g., SignalR notification) */ refresh(): void { this.loadFilaments(); } /** Format weight for display */ formatWeight(grams: number): string { if (grams >= 1000) { return `${(grams / 1000).toFixed(1)}kg`; } return `${Math.round(grams)}g`; } /** Format currency for display */ formatCurrency(value: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value); } }