Skip to main content

Overview

The frontend uses Angular 18’s standalone components architecture. All components are self-contained, declare their own dependencies, and follow a consistent pattern for inputs, outputs, and lifecycle management.

Component Architecture

Standalone Components

All components use the standalone pattern (no NgModules):
@Component({
  selector: 'app-dashboard-map',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './dashboard-map.component.html',
  styleUrl: './dashboard-map.component.scss'
})
export class DashboardMapComponent { }

Dependency Injection

Services are injected using the modern inject() function:
import { inject } from '@angular/core';

private readonly productService = inject(ProductService);
private readonly storeService = inject(StoreService);

Component Communication

Components communicate through:
  • @Input(): Parent-to-child data flow
  • @Output(): Child-to-parent events via EventEmitter
  • Services: Shared state and business logic

Key Components

AppComponent

Location: src/app/app.component.ts The root component that orchestrates the dashboard:
export class AppComponent implements OnInit, OnDestroy {
  globalMetrics: GlobalMetrics | null = null;
  detailedMetrics: DetailedMetrics | null = null;
  assignments: StockAssignment[] = [];
  warehouses: Warehouse[] = [];
  stores: Store[] = [];
  products: Product[] = [];
  filters: DashboardFilters = { /* ... */ };
  activeTab: DashboardTab = 'map';
  
  private readonly destroy$ = new Subject<void>();
}
Responsibilities:
  • Load initial data using forkJoin() for parallel requests
  • Manage filters and active tab state
  • Coordinate data flow between child components
  • Handle loading and error states
  • Clean up subscriptions on destroy
Data Loading Pattern:
forkJoin({
  warehouses: this.warehouseService.getAll(),
  stores: this.storeService.getAll(),
  products: this.productService.getAll(),
  globalMetrics: this.metricsService.getGlobalMetrics()
})
.pipe(
  takeUntil(this.destroy$),
  finalize(() => { this.loading = false; })
)
.subscribe({ /* ... */ });

DashboardMapComponent

Location: src/app/dashboard-map/dashboard-map.component.ts Purpose: Interactive map visualization using Leaflet Inputs:
  • filters: DashboardFilters - Current filter state
  • assignments: StockAssignment[] - Stock assignments to display
  • warehouses: Warehouse[] - Warehouse locations
  • stores: Store[] - Store locations
Key Features:

Leaflet Integration

import * as L from 'leaflet';
import 'leaflet.markercluster';
import 'leaflet-polylinedecorator';

private L = (window as any).L as typeof L;
private map!: L.Map;
private warehouseMarkers!: L.MarkerClusterGroup;
private storeMarkers!: L.MarkerClusterGroup;

Map Initialization

private initMap(): void {
  this.map = this.L.map('map', {
    zoomControl: false,
    attributionControl: false,
    maxBounds: [[-90, -180], [90, 180]],
    minZoom: 2,
    maxZoom: 19
  }).setView([40, 0], 2);
  
  // CARTO light basemap
  this.L.tileLayer(
    'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
  ).addTo(this.map);
}

Marker Clustering

this.warehouseMarkers = this.L.markerClusterGroup({
  removeOutsideVisibleBounds: false,
  iconCreateFunction: (cluster: L.MarkerCluster) => {
    const childCount = cluster.getChildCount();
    return this.L.divIcon({
      html: `<div><span>${childCount}</span></div>`,
      className: 'marker-cluster marker-cluster-warehouse',
      iconSize: new this.L.Point(40, 40)
    });
  }
});

Route Visualization

  • Polylines connect warehouses to stores
  • Arrow decorators show direction
  • Interactive hover and click states
  • Popups display assignment details
private createRouteLineGroup(
  from: [number, number],
  to: [number, number],
  group: StockAssignment[],
  fromId: string,
  toId: string
): L.Polyline {
  const polyline = this.L.polyline([from, to], {
    color: '#673ab7',
    weight: 2,
    opacity: 0.13
  }).bindPopup(this.getPopupContent(group, fromId, toId));
  
  // Add arrow decorator
  const decorator = this.L.polylineDecorator(polyline, {
    patterns: [{
      offset: '50%',
      symbol: this.L.Symbol.arrowHead({ /* ... */ })
    }]
  });
  
  return polyline;
}

DashboardTableComponent

Location: src/app/dashboard-table/dashboard-table.component.ts Purpose: Tabular view of stock assignments with virtual scrolling Inputs:
  • assignments: StockAssignment[] - Assignments to display
  • warehouses: Warehouse[] - For country lookup
  • stores: Store[] - For country lookup
Key Features:

Virtual Scrolling

Uses Angular CDK for performance with large datasets:
import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';

@ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport;

ngOnChanges(): void {
  setTimeout(() => {
    this.viewport.checkViewportSize();
    this.viewport.scrollToOffset(0, 'smooth');
  });
}

Performance Optimization

// TrackBy function for efficient list rendering
trackByAssignmentId(index: number, assignment: StockAssignment): string {
  return assignment.id;
}

// Map caching for fast lookups
warehouseCountryMap = new Map<string, string>();
storeCountryMap = new Map<string, string>();

DashboardMetricsComponent

Location: src/app/dashboard-metrics/dashboard-metrics.component.ts Purpose: Display global metrics summary Inputs:
  • metrics: GlobalMetrics | null - Global metrics data
Imports:
  • FormatNumberPipe - Format large numbers (e.g., 1.2M)
  • FormatDistancePipe - Format distances (e.g., 1,234 km)
Usage:
<app-dashboard-metrics [metrics]="globalMetrics" />

DashboardMetricsDetailComponent

Location: src/app/dashboard-metrics-detail/dashboard-metrics-detail.component.ts Purpose: Detailed metrics with Chart.js visualizations Inputs:
  • metrics: DetailedMetrics | null - Filtered metrics data

Chart.js Integration

import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts';

@Component({
  providers: [provideCharts(withDefaultRegisterables())]
})
export class DashboardMetricsDetailComponent {
  public chartOptions = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: { legend: { display: false } }
  };
}

Chart Types

Warehouse Distribution (Horizontal Bar):
get warehouseChartData() {
  return {
    labels: this.metrics.unitsByWarehouse.map(w => w.warehouseId),
    datasets: [{
      data: this.metrics.unitsByWarehouse.map(w => w.totalUnits),
      backgroundColor: CHART_COLORS,
      borderWidth: 0
    }]
  };
}
Capacity Utilization (Doughnut):
get capacityChartData() {
  const percentage = this.metrics.capacityUtilization.percentage;
  return {
    labels: ['Used', 'Available'],
    datasets: [{
      data: [percentage, 100 - percentage],
      backgroundColor: [this.getCapacityColor(percentage), '#E5E7EB']
    }]
  };
}
Top Products (Vertical Bar):
get topProductsChartData() {
  return {
    labels: this.metrics.topProducts.map(p => p.productId),
    datasets: [{
      data: this.metrics.topProducts.map(p => p.totalQuantity),
      backgroundColor: CHART_COLORS
    }]
  };
}

DashboardFiltersComponent

Location: src/app/dashboard-filters/dashboard-filters.component.ts Purpose: Filter controls for warehouses, stores, and products Inputs:
  • warehouses: Warehouse[] - Available warehouses
  • stores: Store[] - Available stores
  • products: Product[] - Available products
  • filters: DashboardFilters - Current filter state
Outputs:
  • filtersChange: EventEmitter<DashboardFilters> - Filter updates
Integration: Uses @ng-select/ng-select for advanced dropdown functionality:
import { NgSelectModule } from '@ng-select/ng-select';

@Component({
  imports: [CommonModule, FormsModule, NgSelectModule]
})
export class DashboardFiltersComponent {
  @Output() filtersChange = new EventEmitter<DashboardFilters>();
  
  onFiltersChange() {
    this.filtersChange.emit({ ...this.filters });
  }
}

DashboardTabsComponent

Location: src/app/dashboard-tabs/dashboard-tabs.component.ts Purpose: Tab navigation (Map, Table, Metrics) Inputs:
  • activeTab: DashboardTab - Current active tab
Outputs:
  • tabChange: EventEmitter<DashboardTab> - Tab change events
Usage:
export type DashboardTab = 'map' | 'table' | 'metrics';

onSetTab(tab: DashboardTab) {
  this.activeTab = tab;
  this.tabChange.emit(tab);
}

Component Communication Patterns

Parent-to-Child (Input)

// Parent
<app-dashboard-map
  [filters]="filters"
  [assignments]="assignments"
  [warehouses]="warehouses"
  [stores]="stores"
/>

// Child
@Input() filters!: DashboardFilters;
@Input() assignments: StockAssignment[] = [];

Child-to-Parent (Output)

// Child
@Output() filtersChange = new EventEmitter<DashboardFilters>();

onFiltersChange() {
  this.filtersChange.emit({ ...this.filters });
}

// Parent
<app-dashboard-filters
  (filtersChange)="onFiltersChange($event)"
/>

Service-Based Communication

For cross-component data sharing, services provide centralized state:
// Load data once, share across components
forkJoin({
  warehouses: this.warehouseService.getAll(),
  stores: this.storeService.getAll()
}).subscribe(({ warehouses, stores }) => {
  // Pass to multiple child components
});

Lifecycle Management

Memory Leak Prevention

All components use takeUntil() for subscription cleanup:
private readonly destroy$ = new Subject<void>();

ngOnInit() {
  this.service.getData()
    .pipe(takeUntil(this.destroy$))
    .subscribe(data => { /* ... */ });
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

Change Detection

Components implement OnChanges to react to input changes:
ngOnChanges(changes: SimpleChanges): void {
  if (changes['assignments']) {
    this.updateView();
  }
}

Styling

Components use SCSS with component-scoped styles:
@Component({
  styleUrl: './dashboard-map.component.scss'
})
Global styles are defined in src/styles.scss.

Next Steps

Build docs developers (and LLMs) love