CUB-43: add inventory dashboard summary component with FilamentService
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<!-- Inventory Summary Bar — filament metrics at a glance -->
|
||||
<section class="inventory-summary" role="status" aria-label="Inventory summary">
|
||||
|
||||
<!-- Total Filament Count -->
|
||||
<div class="summary-item" matTooltip="Total active spools in inventory" matTooltipPosition="below">
|
||||
<mat-icon aria-hidden="true">inventory_2</mat-icon>
|
||||
<span class="metric-value">{{ totalFilamentCount() }}</span>
|
||||
<span class="metric-label">Spools</span>
|
||||
</div>
|
||||
|
||||
<!-- Low Stock Count -->
|
||||
<div class="summary-item low-stock"
|
||||
[class.has-alerts]="hasLowStock()"
|
||||
[class.has-critical]="hasCriticalStock()"
|
||||
matTooltip="Spools below 25% remaining" matTooltipPosition="below">
|
||||
<mat-icon aria-hidden="true">{{ hasCriticalStock() ? 'error' : hasLowStock() ? 'warning' : 'check_circle' }}</mat-icon>
|
||||
<span class="metric-value">{{ lowStockCount() }}</span>
|
||||
<span class="metric-label">Low Stock</span>
|
||||
</div>
|
||||
|
||||
<!-- Estimated Total Value -->
|
||||
<div class="summary-item" matTooltip="Estimated total value of active spools" matTooltipPosition="below">
|
||||
<mat-icon aria-hidden="true">payments</mat-icon>
|
||||
<span class="metric-value">{{ formatCurrency(estimatedTotalValue()) }}</span>
|
||||
<span class="metric-label">Value</span>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Inventory Summary Component Styles
|
||||
* Touch-optimized for kiosk (Raspberry Pi 5) and mobile PWA
|
||||
* Consistent with dashboard-summary component styling
|
||||
*/
|
||||
|
||||
// Touch-optimized sizing
|
||||
$touch-target-min: 48px;
|
||||
$kiosk-font-primary: 20px;
|
||||
$mobile-font-primary: 16px;
|
||||
$spacing-unit: 8px;
|
||||
|
||||
// Status colors — high contrast for workshop/bright environments
|
||||
$color-healthy: #4ade70; // Green — stock OK
|
||||
$color-low: #facc15; // Amber — low stock
|
||||
$color-critical: #f87171; // Red — critical stock
|
||||
|
||||
.inventory-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-unit * 2;
|
||||
padding: $spacing-unit * 2;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: $spacing-unit;
|
||||
gap: $spacing-unit;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-unit;
|
||||
min-height: $touch-target-min;
|
||||
padding: $spacing-unit $spacing-unit * 2;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: $spacing-unit;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 22px !important;
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: $kiosk-font-primary;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: $mobile-font-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Low stock states
|
||||
.low-stock {
|
||||
&.has-alerts {
|
||||
background-color: rgba($color-low, 0.15);
|
||||
|
||||
mat-icon {
|
||||
color: $color-low;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-critical {
|
||||
background-color: rgba($color-critical, 0.15);
|
||||
|
||||
mat-icon {
|
||||
color: $color-critical;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FilamentService } from '../../services/filament.service';
|
||||
import { classifyStockLevel } from '../../models/filament.model';
|
||||
|
||||
/**
|
||||
* InventorySummaryComponent — dashboard summary for filament inventory metrics.
|
||||
*
|
||||
* Displays three key metrics driven by FilamentService:
|
||||
* - Total filament count (active spools)
|
||||
* - Low stock count (spools classified as 'low' or 'critical')
|
||||
* - Estimated total filament value (sum of purchase prices for active spools)
|
||||
*
|
||||
* All values update dynamically whenever FilamentService data changes.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-inventory-summary',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
templateUrl: './inventory-summary.component.html',
|
||||
styleUrl: './inventory-summary.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InventorySummaryComponent {
|
||||
private readonly filamentService = inject(FilamentService);
|
||||
|
||||
/** Computed: total number of active filament spools */
|
||||
readonly totalFilamentCount = computed(() =>
|
||||
this.filamentService.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 spools at critical stock level only */
|
||||
readonly criticalStockCount = computed(() =>
|
||||
this.filamentService.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()
|
||||
.filter((f) => f.isActive && f.purchasePrice !== null)
|
||||
.reduce((sum, f) => sum + (f.purchasePrice ?? 0), 0)
|
||||
);
|
||||
|
||||
/** Computed: whether there are low-stock spools to highlight */
|
||||
readonly hasLowStock = computed(() => this.lowStockCount() > 0);
|
||||
|
||||
/** Computed: whether there are critical-stock spools */
|
||||
readonly hasCriticalStock = computed(() => this.criticalStockCount() > 0);
|
||||
|
||||
/** Format a currency value for display */
|
||||
formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user