-
-
- inventory_2
- {{ totalFilamentCount() }}
- Spools
-
+
+ @if (loading()) {
+
+ sync
+ Loading inventory...
+
+ }
-
-
- {{ hasCriticalStock() ? 'error' : hasLowStock() ? 'warning' : 'check_circle' }}
- {{ lowStockCount() }}
- Low Stock
-
+
+ @else if (error()) {
+
+ error_outline
+ {{ error() }}
+
+
+ }
-
-
- payments
- {{ formatCurrency(estimatedTotalValue()) }}
- Value
-
+
+ @else {
+
+
+
+ @switch (healthClass()) {
+ @case ('critical') { error }
+ @case ('low') { warning }
+ @default { check_circle }
+ }
+
+ {{ healthLabel() }}
+
+
+
+
inventory_2
+
+ {{ totalCount() }}
+ Total Spools
+
+ @if (activeCount() < totalCount()) {
+
{{ activeCount() }} active
+ }
+
+
+
+
+
+ @if (hasCritical()) { error }
+ @else if (hasLowStock()) { warning }
+ @else { check_circle }
+
+
+ {{ lowStockCount() }}
+ Low Stock
+
+ @if (hasCritical()) {
+
{{ criticalCount() }} critical
+ }
+
+
+
+
+
payments
+
+ {{ formatCurrency(totalValue()) }}
+ Est. Value
+
+
+
+
+
+
line_weight
+
+
+
+
+
+
+ }
\ No newline at end of file
diff --git a/frontend/src/app/components/inventory-summary/inventory-summary.component.scss b/frontend/src/app/components/inventory-summary/inventory-summary.component.scss
index 65226f3..6921714 100644
--- a/frontend/src/app/components/inventory-summary/inventory-summary.component.scss
+++ b/frontend/src/app/components/inventory-summary/inventory-summary.component.scss
@@ -1,93 +1,257 @@
/**
* Inventory Summary Component Styles
* Touch-optimized for kiosk (Raspberry Pi 5) and mobile PWA
- * Consistent with dashboard-summary component styling
+ * Matches the existing dark theme from app.scss
*/
// Touch-optimized sizing
$touch-target-min: 48px;
-$kiosk-font-primary: 20px;
-$mobile-font-primary: 16px;
+$kiosk-font-primary: 24px;
+$mobile-font-primary: 18px;
$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
+$color-healthy: #4ade70; // Green
+$color-low: #fbbf24; // Amber/Yellow
+$color-critical: #f87171; // Red
+$color-bg: #1a1a2e; // Matches app.scss
+$color-text: #e0e0e0;
+$color-text-muted: rgba(255, 255, 255, 0.7);
+$color-card-bg: rgba(255, 255, 255, 0.05);
+$color-card-border: rgba(255, 255, 255, 0.1);
.inventory-summary {
display: flex;
- align-items: center;
+ align-items: stretch;
gap: $spacing-unit * 2;
padding: $spacing-unit * 2;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
+ scrollbar-width: thin;
- @media (max-width: 480px) {
+ @media (max-width: 600px) {
+ flex-wrap: wrap;
padding: $spacing-unit;
gap: $spacing-unit;
}
}
-.summary-item {
+// Health status indicator
+.health-status {
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);
+ border-radius: 24px;
+ min-height: $touch-target-min;
white-space: nowrap;
transition: background-color 0.3s ease;
- @media (max-width: 480px) {
- padding: $spacing-unit;
- border-radius: 8px;
+ &.healthy {
+ background-color: rgba($color-healthy, 0.15);
+ color: $color-healthy;
}
- mat-icon {
- font-size: 22px !important;
- width: 22px !important;
- height: 22px !important;
+ &.low {
+ background-color: rgba($color-low, 0.15);
+ color: $color-low;
}
- .metric-value {
- font-size: $kiosk-font-primary;
+ &.critical {
+ background-color: rgba($color-critical, 0.15);
+ color: $color-critical;
+ }
+
+ .health-text {
+ font-size: 14px;
font-weight: 600;
- line-height: 1.2;
+ letter-spacing: 0.02em;
@media (max-width: 480px) {
- font-size: $mobile-font-primary;
+ font-size: 12px;
}
}
- .metric-label {
- font-size: 12px;
- color: rgba(255, 255, 255, 0.7);
- text-transform: uppercase;
- letter-spacing: 0.05em;
+ mat-icon {
+ font-size: 20px !important;
+ width: 20px !important;
+ height: 20px !important;
+ }
+}
- @media (max-width: 480px) {
- font-size: 10px;
+// Metric card
+.metric-card {
+ display: flex;
+ align-items: center;
+ gap: $spacing-unit;
+ padding: $spacing-unit $spacing-unit * 2;
+ background-color: $color-card-bg;
+ border: 1px solid $color-card-border;
+ border-radius: 12px;
+ min-height: $touch-target-min;
+ white-space: nowrap;
+ transition: border-color 0.2s ease, background-color 0.2s ease;
+
+ &:hover {
+ border-color: rgba(255, 255, 255, 0.2);
+ background-color: rgba(255, 255, 255, 0.08);
+ }
+
+ @media (max-width: 480px) {
+ padding: $spacing-unit;
+ }
+
+ &.has-alert {
+ border-color: rgba($color-low, 0.4);
+ }
+
+ &.has-critical {
+ border-color: rgba($color-critical, 0.5);
+ background-color: rgba($color-critical, 0.08);
+ }
+}
+
+.metric-icon {
+ color: $color-text-muted;
+ font-size: 22px !important;
+ width: 22px !important;
+ height: 22px !important;
+
+ .has-alert & {
+ color: $color-low;
+ }
+
+ .has-critical & {
+ color: $color-critical;
+ }
+}
+
+.metric-content {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.metric-value {
+ font-size: $kiosk-font-primary;
+ font-weight: 700;
+ line-height: 1.2;
+ color: $color-text;
+
+ @media (max-width: 480px) {
+ font-size: $mobile-font-primary;
+ }
+}
+
+.metric-label {
+ font-size: 11px;
+ color: $color-text-muted;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.metric-detail {
+ font-size: 11px;
+ color: $color-text-muted;
+ margin-left: $spacing-unit;
+
+ &.critical-detail {
+ color: $color-critical;
+ font-weight: 600;
+ }
+}
+
+// Stock bar card
+.stock-bar-card {
+ flex: 1 1 200px;
+ min-width: 180px;
+}
+
+.stock-bar-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.stock-bar-header {
+ display: flex;
+ align-items: baseline;
+ gap: $spacing-unit;
+ margin-bottom: 4px;
+}
+
+// Progress bar color classes
+::ng-deep .mat-mdc-progress-bar {
+ &.healthy .mdc-linear-progress__bar-inner {
+ background-color: $color-healthy !important;
+ }
+
+ &.low .mdc-linear-progress__bar-inner {
+ background-color: $color-low !important;
+ }
+
+ &.critical .mdc-linear-progress__bar-inner {
+ background-color: $color-critical !important;
+ }
+}
+
+// Loading state
+.summary-loading {
+ display: flex;
+ align-items: center;
+ gap: $spacing-unit;
+ padding: $spacing-unit * 2;
+ color: $color-text-muted;
+ font-size: 14px;
+
+ .spin {
+ animation: spin 1s linear infinite;
+ }
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+// Error state
+.summary-error {
+ display: flex;
+ align-items: center;
+ gap: $spacing-unit;
+ padding: $spacing-unit * 2;
+ background-color: rgba($color-critical, 0.1);
+ border: 1px solid rgba($color-critical, 0.3);
+ border-radius: 12px;
+ color: $color-critical;
+ font-size: 14px;
+
+ .retry-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: transparent;
+ border: 1px solid rgba($color-critical, 0.4);
+ color: $color-critical;
+ padding: 4px 12px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 13px;
+ min-height: $touch-target-min - 8px;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: rgba($color-critical, 0.15);
+ }
+
+ mat-icon {
+ font-size: 16px !important;
+ width: 16px !important;
+ height: 16px !important;
}
}
}
-// 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;
- }
- }
+// Summary item base
+.summary-item {
+ flex-shrink: 0;
}
\ No newline at end of file
diff --git a/frontend/src/app/components/inventory-summary/inventory-summary.component.ts b/frontend/src/app/components/inventory-summary/inventory-summary.component.ts
index 9ada5ec..9c89055 100644
--- a/frontend/src/app/components/inventory-summary/inventory-summary.component.ts
+++ b/frontend/src/app/components/inventory-summary/inventory-summary.component.ts
@@ -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