+
+
+ @if (loading()) {
+
+ sync
+ Loading inventory...
+
+ }
+
+
+ @else if (error()) {
+
+ error_outline
+ {{ error() }}
+
+
+ }
+
+
+ @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
new file mode 100644
index 0000000..6921714
--- /dev/null
+++ b/frontend/src/app/components/inventory-summary/inventory-summary.component.scss
@@ -0,0 +1,257 @@
+/**
+ * Inventory Summary Component Styles
+ * Touch-optimized for kiosk (Raspberry Pi 5) and mobile PWA
+ * Matches the existing dark theme from app.scss
+ */
+
+// Touch-optimized sizing
+$touch-target-min: 48px;
+$kiosk-font-primary: 24px;
+$mobile-font-primary: 18px;
+$spacing-unit: 8px;
+
+// Status colors — high contrast for workshop/bright environments
+$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: stretch;
+ gap: $spacing-unit * 2;
+ padding: $spacing-unit * 2;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: thin;
+
+ @media (max-width: 600px) {
+ flex-wrap: wrap;
+ padding: $spacing-unit;
+ gap: $spacing-unit;
+ }
+}
+
+// Health status indicator
+.health-status {
+ display: flex;
+ align-items: center;
+ gap: $spacing-unit;
+ padding: $spacing-unit $spacing-unit * 2;
+ border-radius: 24px;
+ min-height: $touch-target-min;
+ white-space: nowrap;
+ transition: background-color 0.3s ease;
+
+ &.healthy {
+ background-color: rgba($color-healthy, 0.15);
+ color: $color-healthy;
+ }
+
+ &.low {
+ background-color: rgba($color-low, 0.15);
+ color: $color-low;
+ }
+
+ &.critical {
+ background-color: rgba($color-critical, 0.15);
+ color: $color-critical;
+ }
+
+ .health-text {
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+
+ @media (max-width: 480px) {
+ font-size: 12px;
+ }
+ }
+
+ mat-icon {
+ font-size: 20px !important;
+ width: 20px !important;
+ height: 20px !important;
+ }
+}
+
+// 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;
+ }
+ }
+}
+
+// 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
new file mode 100644
index 0000000..9c89055
--- /dev/null
+++ b/frontend/src/app/components/inventory-summary/inventory-summary.component.ts
@@ -0,0 +1,176 @@
+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