CUB-43: Add inventory dashboard summary component
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m19s
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m19s
This commit is contained in:
@@ -1,24 +1,33 @@
|
||||
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 {
|
||||
classifyStockLevel,
|
||||
} from '../../models/filament.model';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
* InventorySummaryComponent — dashboard summary for filament inventory metrics.
|
||||
* Inventory Dashboard Summary — shows filament inventory at a glance.
|
||||
*
|
||||
* Displays three key metrics driven by FilamentService:
|
||||
* - Total filament count (active spools)
|
||||
* - Low stock count (spools classified as 'low' or 'critical')
|
||||
* 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)
|
||||
*
|
||||
* All values update dynamically whenever FilamentService data changes.
|
||||
* Data is sourced from the shared FilamentService signal,
|
||||
* which is loaded on init and can be refreshed via refresh().
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-inventory-summary',
|
||||
@@ -26,56 +35,142 @@ import { classifyStockLevel } from '../../models/filament.model';
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
MatChipsModule,
|
||||
MatTooltipModule,
|
||||
MatProgressBarModule,
|
||||
],
|
||||
templateUrl: './inventory-summary.component.html',
|
||||
styleUrl: './inventory-summary.component.scss',
|
||||
styleUrls: ['./inventory-summary.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InventorySummaryComponent {
|
||||
export class InventorySummaryComponent implements OnInit, OnDestroy {
|
||||
private readonly filamentService = inject(FilamentService);
|
||||
private subscription: Subscription | null = null;
|
||||
|
||||
/** Computed: total number of active filament spools */
|
||||
readonly totalFilamentCount = computed(() =>
|
||||
this.filamentService.filaments().filter((f) => f.isActive).length
|
||||
/** All filament data — reactive signal from shared service */
|
||||
readonly filaments = this.filamentService.filaments;
|
||||
|
||||
/** Loading state */
|
||||
readonly loading = signal<boolean>(true);
|
||||
|
||||
/** Error state */
|
||||
readonly error = signal<string | null>(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 spools at low or critical stock levels */
|
||||
readonly lowStockCount = computed(() =>
|
||||
this.filamentService.filaments().filter((f) => {
|
||||
const level = classifyStockLevel(f);
|
||||
return level === 'low' || level === 'critical';
|
||||
}).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 spools at critical stock level only */
|
||||
readonly criticalStockCount = computed(() =>
|
||||
this.filamentService.filaments().filter(
|
||||
(f) => 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 all active spools with a recorded price */
|
||||
readonly estimatedTotalValue = computed(() =>
|
||||
this.filamentService
|
||||
.filaments()
|
||||
/** 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: whether there are low-stock spools to highlight */
|
||||
/** 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 there are critical-stock spools */
|
||||
readonly hasCriticalStock = computed(() => this.criticalStockCount() > 0);
|
||||
/** Computed: whether to show a critical-stock alert */
|
||||
readonly hasCritical = computed(() => this.criticalCount() > 0);
|
||||
|
||||
/** Format a currency value for display */
|
||||
/** 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: 2,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user