Menu

Change language

GitHub repo

Dispatch Assignment Board

blocklogistics

Last-mile route dispatching, delivery driver stop sequencing, and vehicle payload load balancing: live dispatch overview KPIs, zone filtering, AI route auto-sequencing, and driver route cards with vehicle payload load balancing, live progress tracking, interactive stops timeline, driver communications, and telemetry actions.

Installation

$npx uipkge-ng@latest add dispatch-assignment-board
Manifest:https://uipkge.dev/r/angular/dispatch-assignment-board.jsonInstalls to:src/app/components/blocks/dispatch-assignment-board/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

RouteStop
interface RouteStop {
  id: string
  sequence: number
  locationName: string
  address: string
  status: StopStatus
  deliveredTime?: string
  estimatedTime?: string
  packagesCount: number
  packageType: string
  notes?: string
  recipient?: string
}
DriverRoute
interface DriverRoute {
  id: string
  routeCode: string
  routeName: string
  zone: string
  driver: {
    name: string
    avatar?: string
    initials: string
    phone: string
    status: 'on_route' | 'break' | 'loading'
    radioChannel: string
  }
  vehicle: {
    name: string
    type: string
    plateNumber: string
    powerLevel: string
    powerType: 'electric' | 'diesel'
  }
  completedStops: number
  totalStops: number
  currentPayloadLbs: number
  maxPayloadLbs: number
  shiftWindow: string
  estimatedCompletion: string
  telemetry: {
    speed: string
    heading: string
    currentLocation: string
    lastPing: string
    signal: string
  }
  stops: RouteStop[]
}
ActiveModalState
interface ActiveModalState {
  type: 'gps' | 'call' | 'reassign'
  route: DriverRoute
  targetRouteId?: string
}

Files installed (2)

  • src/app/components/blocks/dispatch-assignment-board/dispatch-assignment-board.component.ts57.6 kB
    // Last-mile route dispatching, delivery driver stop sequencing, and vehicle payload load
    // balancing: live dispatch overview KPIs, zone filtering, AI route auto-sequencing, and driver
    // route cards with vehicle payload load balancing, live progress tracking, interactive stops
    // timeline, driver communications, and telemetry actions. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'
    import {
      ArrowRightLeft,
      BatteryCharging,
      Check,
      CheckCircle2,
      Clock,
      Download,
      Fuel,
      Gauge,
      LucideAngularModule,
      MapPin,
      Navigation,
      PackageCheck,
      Phone,
      PhoneCall,
      Search,
      ShieldCheck,
      Truck,
      X,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardFooterComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectGroupComponent,
      UiSelectItemComponent,
      UiSelectLabelComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export type StopStatus = 'delivered' | 'in_progress' | 'pending'
    
    export interface RouteStop {
      id: string
      sequence: number
      locationName: string
      address: string
      status: StopStatus
      deliveredTime?: string
      estimatedTime?: string
      packagesCount: number
      packageType: string
      notes?: string
      recipient?: string
    }
    
    export interface DriverRoute {
      id: string
      routeCode: string
      routeName: string
      zone: string
      driver: {
        name: string
        avatar?: string
        initials: string
        phone: string
        status: 'on_route' | 'break' | 'loading'
        radioChannel: string
      }
      vehicle: {
        name: string
        type: string
        plateNumber: string
        powerLevel: string
        powerType: 'electric' | 'diesel'
      }
      completedStops: number
      totalStops: number
      currentPayloadLbs: number
      maxPayloadLbs: number
      shiftWindow: string
      estimatedCompletion: string
      telemetry: {
        speed: string
        heading: string
        currentLocation: string
        lastPing: string
        signal: string
      }
      stops: RouteStop[]
    }
    
    export interface ActiveModalState {
      type: 'gps' | 'call' | 'reassign'
      route: DriverRoute
      targetRouteId?: string
    }
    
    export const DRIVER_ROUTES_DATA: DriverRoute[] = [
      {
        id: 'route-01',
        routeCode: 'Route #01',
        routeName: 'Downtown Metro',
        zone: 'Metro Los Angeles - North Zone',
        driver: {
          name: 'Elena Rostova',
          initials: 'ER',
          phone: '+1 (213) 555-0182',
          status: 'on_route',
          radioChannel: 'Ch 4 · North Dispatch',
        },
        vehicle: {
          name: 'Ford Transit #204',
          type: 'EV Cargo Van',
          plateNumber: 'CA-8KDX92',
          powerLevel: '74% Battery',
          powerType: 'electric',
        },
        completedStops: 18,
        totalStops: 24,
        currentPayloadLbs: 1850,
        maxPayloadLbs: 2200,
        shiftWindow: '07:30 AM – 16:30 PM',
        estimatedCompletion: '15:45 PM',
        telemetry: {
          speed: '32 mph',
          heading: 'NW 315°',
          currentLocation: 'Grand Ave & 5th St, Los Angeles, CA 90071',
          lastPing: '12s ago',
          signal: '5G Active',
        },
        stops: [
          {
            id: 'r1-s1',
            sequence: 1,
            locationName: 'Acme Industrial Hub',
            address: '1240 S Santa Fe Ave',
            status: 'delivered',
            deliveredTime: '09:15 AM',
            packagesCount: 12,
            packageType: 'Cartons',
            recipient: 'Signed by J. Miller',
          },
          {
            id: 'r1-s2',
            sequence: 2,
            locationName: 'Apex Tech Campus',
            address: '800 Wilshire Blvd, Ste 400',
            status: 'delivered',
            deliveredTime: '10:45 AM',
            packagesCount: 8,
            packageType: 'Parcels',
            recipient: 'Dock 4B Security',
          },
          {
            id: 'r1-s3',
            sequence: 3,
            locationName: 'Metro Distribution Center',
            address: '1950 E 7th St',
            status: 'in_progress',
            estimatedTime: '14:15 PM',
            packagesCount: 15,
            packageType: 'Heavy Boxes',
            notes: 'Priority Rush · Forklift offload requested',
          },
          {
            id: 'r1-s4',
            sequence: 4,
            locationName: 'Pasadena Hospital Supply',
            address: '650 S Raymond Ave',
            status: 'pending',
            packagesCount: 2,
            packageType: 'Medical Coolers',
            notes: 'Cold-Chain · Temperature Log Required',
          },
        ],
      },
      {
        id: 'route-02',
        routeCode: 'Route #02',
        routeName: 'Westside & Santa Monica',
        zone: 'Metro Los Angeles - North Zone',
        driver: {
          name: 'Marcus Vance',
          initials: 'MV',
          phone: '+1 (310) 555-0194',
          status: 'on_route',
          radioChannel: 'Ch 2 · West Dispatch',
        },
        vehicle: {
          name: 'Mercedes Sprinter #118',
          type: 'High-Roof Van',
          plateNumber: 'CA-9TMR44',
          powerLevel: '68% Fuel',
          powerType: 'diesel',
        },
        completedStops: 22,
        totalStops: 26,
        currentPayloadLbs: 2450,
        maxPayloadLbs: 3000,
        shiftWindow: '08:00 AM – 17:00 PM',
        estimatedCompletion: '16:15 PM',
        telemetry: {
          speed: '28 mph',
          heading: 'W 270°',
          currentLocation: 'Ocean Ave & Colorado Ave, Santa Monica, CA',
          lastPing: '8s ago',
          signal: '5G Active',
        },
        stops: [
          {
            id: 'r2-s1',
            sequence: 1,
            locationName: 'Pacific Design Center',
            address: '8687 Melrose Ave',
            status: 'delivered',
            deliveredTime: '09:40 AM',
            packagesCount: 6,
            packageType: 'Crates',
            recipient: 'Direct Receipt · Bay 2',
          },
          {
            id: 'r2-s2',
            sequence: 2,
            locationName: 'Oceanview Medical Plaza',
            address: '1301 20th St, Floor 3',
            status: 'delivered',
            deliveredTime: '11:20 AM',
            packagesCount: 14,
            packageType: 'Packages',
            recipient: 'Receptionist Amy K.',
          },
          {
            id: 'r2-s3',
            sequence: 3,
            locationName: 'Santa Monica Pier Logistics',
            address: '200 Santa Monica Pier',
            status: 'in_progress',
            estimatedTime: '14:40 PM',
            packagesCount: 4,
            packageType: 'Cartons',
            notes: 'Dock Bay 2 · Access Gate Code #4491',
          },
          {
            id: 'r2-s4',
            sequence: 4,
            locationName: 'Silicon Beach Incubator',
            address: '12020 Ocean Park Blvd',
            status: 'pending',
            packagesCount: 5,
            packageType: 'Parcels',
            notes: 'Signature Required · 5th Floor Suite 500',
          },
        ],
      },
      {
        id: 'route-03',
        routeCode: 'Route #03',
        routeName: 'Pasadena & Glendale',
        zone: 'San Fernando Valley Hub',
        driver: {
          name: 'Sofia Chen',
          initials: 'SC',
          phone: '+1 (626) 555-0147',
          status: 'on_route',
          radioChannel: 'Ch 5 · Valley Dispatch',
        },
        vehicle: {
          name: 'Ram ProMaster #309',
          type: 'Electric Courier',
          plateNumber: 'CA-7KPL55',
          powerLevel: '82% Battery',
          powerType: 'electric',
        },
        completedStops: 14,
        totalStops: 20,
        currentPayloadLbs: 1620,
        maxPayloadLbs: 2100,
        shiftWindow: '08:15 AM – 16:45 PM',
        estimatedCompletion: '16:30 PM',
        telemetry: {
          speed: '38 mph',
          heading: 'E 090°',
          currentLocation: 'Colorado Blvd & Lake Ave, Pasadena, CA',
          lastPing: '18s ago',
          signal: '5G Active',
        },
        stops: [
          {
            id: 'r3-s1',
            sequence: 1,
            locationName: 'Glendale Aerospace Labs',
            address: '4300 San Fernando Rd',
            status: 'delivered',
            deliveredTime: '10:05 AM',
            packagesCount: 3,
            packageType: 'Heavy Units',
            recipient: 'Forklift Bay Delivery',
          },
          {
            id: 'r3-s2',
            sequence: 2,
            locationName: 'Foothill Commercial Park',
            address: '2210 Foothill Blvd',
            status: 'delivered',
            deliveredTime: '11:50 AM',
            packagesCount: 9,
            packageType: 'Boxes',
            recipient: 'Suite 200 Mailroom',
          },
          {
            id: 'r3-s3',
            sequence: 3,
            locationName: 'Huntington Research Institute',
            address: '1151 Oxford Rd',
            status: 'in_progress',
            estimatedTime: '14:25 PM',
            packagesCount: 6,
            packageType: 'Lab Samples',
            notes: 'Fragile · Bio-Sample Insulated Crate',
          },
          {
            id: 'r3-s4',
            sequence: 4,
            locationName: 'Old Town Commerce Plaza',
            address: '300 E Colorado Blvd',
            status: 'pending',
            packagesCount: 4,
            packageType: 'Packages',
            notes: 'Front Desk Drop · Notify Concierge',
          },
        ],
      },
      {
        id: 'route-04',
        routeCode: 'Route #04',
        routeName: 'South Bay Logistics',
        zone: 'Metro Los Angeles - South Zone',
        driver: {
          name: 'Tariq Mansour',
          initials: 'TM',
          phone: '+1 (424) 555-0133',
          status: 'on_route',
          radioChannel: 'Ch 3 · South Dispatch',
        },
        vehicle: {
          name: 'Freightliner Custom #412',
          type: 'Box Truck',
          plateNumber: 'CA-5WXZ91',
          powerLevel: '88% Fuel',
          powerType: 'diesel',
        },
        completedStops: 19,
        totalStops: 22,
        currentPayloadLbs: 3100,
        maxPayloadLbs: 3800,
        shiftWindow: '07:00 AM – 16:00 PM',
        estimatedCompletion: '15:20 PM',
        telemetry: {
          speed: '45 mph',
          heading: 'S 180°',
          currentLocation: 'I-405 Southbound at Carson St, CA',
          lastPing: '5s ago',
          signal: '5G Active',
        },
        stops: [
          {
            id: 'r4-s1',
            sequence: 1,
            locationName: 'Port of LA Cargo Terminal',
            address: '425 S Palos Verdes St',
            status: 'delivered',
            deliveredTime: '08:30 AM',
            packagesCount: 18,
            packageType: 'Pallets',
            recipient: 'Customs Manifest #882',
          },
          {
            id: 'r4-s2',
            sequence: 2,
            locationName: 'Torrance Industrial Park',
            address: '190th St & Western Ave',
            status: 'delivered',
            deliveredTime: '10:15 AM',
            packagesCount: 11,
            packageType: 'Crates',
            recipient: 'Security Gate 5',
          },
          {
            id: 'r4-s3',
            sequence: 3,
            locationName: 'Long Beach Tech Hub',
            address: '100 W Ocean Blvd',
            status: 'in_progress',
            estimatedTime: '14:50 PM',
            packagesCount: 8,
            packageType: 'Hardware Cases',
            notes: 'Server Equipment · Handle with Care',
          },
          {
            id: 'r4-s4',
            sequence: 4,
            locationName: 'Manhattan Beach Retail Depot',
            address: '3200 Sepulveda Blvd',
            status: 'pending',
            packagesCount: 6,
            packageType: 'Store Parcels',
            notes: 'Store Receiving · Rear Loading Dock',
          },
        ],
      },
    ]
    
    const ZONE_OPTIONS = [
      'Metro Los Angeles - North Zone',
      'Metro Los Angeles - South Zone',
      'San Fernando Valley Hub',
      'Orange County Central',
    ]
    
    export type DispatchFilter = 'all' | 'in_transit' | 'high_load' | 'near_complete'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-dispatch-assignment-board, [ui-dispatch-assignment-board]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiProgressComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectGroupComponent,
        UiSelectItemComponent,
        UiSelectLabelComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div data-slot="dispatch-assignment-board" [class]="rootClass">
          <!-- Header Section -->
          <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
            <div class="space-y-1">
              <div class="flex flex-wrap items-center gap-2">
                <div
                  class="bg-success/10 text-success flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold"
                >
                  <span class="relative flex h-2 w-2">
                    <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                    <span class="bg-success relative inline-flex h-2 w-2 rounded-full"></span>
                  </span>
                  <span>Live Dispatch Operations</span>
                </div>
                <div class="text-muted-foreground flex items-center gap-1 text-xs">
                  <lucide-icon [img]="Clock" class="size-3.5" />
                  <span class="font-mono tabular-nums">Today · Aug 21, 2026</span>
                </div>
              </div>
              <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
                Delivery Dispatch & Route Optimization
              </h1>
              <p class="text-muted-foreground text-xs sm:text-sm">
                Last-mile route sequencing, real-time stop execution, and vehicle payload load balancing.
              </p>
            </div>
    
            <div class="flex flex-wrap items-center gap-2.5">
              <div class="w-full sm:w-64">
                <ui-select [value]="selectedZone()" (valueChange)="selectedZone.set($event)">
                  <button ui-select-trigger class="h-9 w-full text-xs font-medium" aria-label="Select Dispatch Zone">
                    <ui-select-value placeholder="Select Zone" />
                  </button>
                  <ui-select-content>
                    <ui-select-group>
                      <ui-select-label class="text-xs">Dispatch Zones</ui-select-label>
                      <ui-select-item value="all" class="text-xs">All Dispatch Zones</ui-select-item>
                      @for (zone of zoneOptions; track zone) {
                        <ui-select-item [value]="zone" class="text-xs">{{ zone }}</ui-select-item>
                      }
                    </ui-select-group>
                  </ui-select-content>
                </ui-select>
              </div>
    
              <button
                ui-button
                aria-label="Download attachment"
                variant="outline"
                size="sm"
                class="h-9 gap-1.5 text-xs font-medium"
                (click)="handleExportManifests()"
              >
                <lucide-icon [img]="Download" class="size-3.5" />
                <span>Export Manifests</span>
              </button>
    
              <button
                ui-button
                size="sm"
                class="h-9 gap-1.5 text-xs font-medium shadow-xs"
                [disabled]="isOptimizing()"
                (click)="handleAutoOptimize()"
              >
                <lucide-icon [img]="Navigation" [class]="optimizeIconClass" />
                <span>{{ isOptimizing() ? 'Optimizing Routes...' : 'Auto-Optimize Routes' }}</span>
              </button>
            </div>
          </div>
    
          <!-- Notification Banner -->
          @if (notificationBanner(); as banner) {
            <div [class]="bannerClass(banner.type)">
              <div class="flex items-start gap-2.5">
                @if (banner.type === 'success') {
                  <lucide-icon [img]="CheckCircle2" class="text-success mt-0.5 size-4 shrink-0" />
                } @else {
                  <lucide-icon [img]="PackageCheck" class="text-primary mt-0.5 size-4 shrink-0" />
                }
                <div>
                  <span class="font-semibold">{{ banner.title }}:</span>
                  <span class="text-muted-foreground dark:text-foreground/80 ml-1">{{ banner.message }}</span>
                </div>
              </div>
              <button
                aria-label="Dismiss notification"
                type="button"
                class="text-muted-foreground hover:text-foreground transition-colors"
                (click)="notificationBanner.set(null)"
              >
                <lucide-icon [img]="X" class="size-4" />
              </button>
            </div>
          }
    
          <!-- Dispatch Overview KPI Cards (4 Cards) -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Total Routes Active</h3>
                <div class="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded-md">
                  <lucide-icon [img]="Truck" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1.5">
                <div class="flex items-baseline gap-2">
                  <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">8</span>
                  <span class="text-muted-foreground text-xs font-medium">routes dispatched</span>
                </div>
                <div class="text-muted-foreground flex items-center gap-2 text-xs">
                  <span
                    ui-badge
                    variant="outline"
                    class="border-success/20 bg-success/10 text-success h-5 px-1.5 text-xs font-medium"
                  >
                    6 In Transit
                  </span>
                  <span>2 loading at hub</span>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Total Stops Remaining</h3>
                <div class="bg-info/10 text-info flex h-8 w-8 items-center justify-center rounded-md">
                  <lucide-icon [img]="PackageCheck" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                  <div class="flex items-baseline gap-1">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">142</span>
                    <span class="text-muted-foreground font-mono text-xs tabular-nums">/ 180 stops</span>
                  </div>
                  <span class="text-info font-mono text-xs font-semibold tabular-nums">78.8%</span>
                </div>
                <div ui-progress [value]="78.8" class="h-1.5"></div>
                <p class="text-muted-foreground text-xs">
                  <span class="text-foreground font-mono font-semibold tabular-nums">38</span> stops pending completion
                </p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">On-Time Delivery Rate</h3>
                <div class="bg-success/10 text-success flex h-8 w-8 items-center justify-center rounded-md">
                  <lucide-icon [img]="ShieldCheck" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1.5">
                <div class="flex items-baseline gap-2">
                  <span class="text-success font-mono text-2xl font-bold tracking-tight tabular-nums">97.4%</span>
                  <span
                    ui-badge
                    variant="outline"
                    class="border-success/20 bg-success/10 text-success h-5 px-1.5 text-xs font-medium"
                  >
                    +1.8% vs SLA
                  </span>
                </div>
                <p class="text-muted-foreground text-xs">
                  Target: <span class="text-foreground font-mono font-medium tabular-nums">≥95.0%</span> · 0 SLA breaches
                </p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Total Cargo Weight Dispatched</h3>
                <div class="bg-warning/10 text-warning flex h-8 w-8 items-center justify-center rounded-md">
                  <lucide-icon [img]="Gauge" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                  <div class="flex items-baseline gap-1">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">14,280</span>
                    <span class="text-muted-foreground text-xs">lbs</span>
                  </div>
                  <span
                    ui-badge
                    variant="outline"
                    class="border-warning/20 bg-warning/10 text-warning h-5 px-1.5 text-xs font-medium"
                  >
                    82% capacity
                  </span>
                </div>
                <div class="bg-muted relative h-1.5 w-full overflow-hidden rounded-full">
                  <div class="bg-warning h-full rounded-full transition-[width] duration-500" style="width: 82%"></div>
                </div>
                <p class="text-muted-foreground text-xs">
                  Payload limit: <span class="text-foreground font-mono font-medium tabular-nums">17,400 lbs</span> · Well
                  balanced
                </p>
              </div>
            </div>
          </div>
    
          <!-- Filter & Search Controls Bar -->
          <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
            <div class="flex flex-wrap items-center gap-1.5">
              <button
                ui-button
                size="sm"
                [variant]="filterType() === 'all' ? 'default' : 'outline'"
                class="h-8 text-xs font-medium"
                (click)="filterType.set('all')"
              >
                All Routes ({{ routes().length }})
              </button>
              <button
                ui-button
                size="sm"
                [variant]="filterType() === 'in_transit' ? 'default' : 'outline'"
                class="h-8 text-xs font-medium"
                (click)="filterType.set('in_transit')"
              >
                In Transit (4)
              </button>
              <button
                ui-button
                size="sm"
                [variant]="filterType() === 'high_load' ? 'default' : 'outline'"
                class="h-8 text-xs font-medium"
                (click)="filterType.set('high_load')"
              >
                High Load (&gt;80%)
              </button>
              <button
                ui-button
                size="sm"
                [variant]="filterType() === 'near_complete' ? 'default' : 'outline'"
                class="h-8 text-xs font-medium"
                (click)="filterType.set('near_complete')"
              >
                Near Completion (&gt;70%)
              </button>
            </div>
    
            <div class="relative w-full sm:w-72">
              <lucide-icon
                [img]="Search"
                class="text-muted-foreground pointer-events-none absolute top-2.5 left-2.5 size-3.5"
              />
              <input
                [value]="searchQuery()"
                (input)="searchQuery.set($any($event.target).value)"
                type="text"
                placeholder="Filter driver, route, or stop..."
                class="border-border bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring h-8 w-full rounded-md border pr-3 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-hidden"
              />
              @if (searchQuery()) {
                <button
                  type="button"
                  class="text-muted-foreground hover:text-foreground absolute top-2 right-2"
                  aria-label="Clear search"
                  (click)="searchQuery.set('')"
                >
                  <lucide-icon [img]="X" class="size-3.5" />
                </button>
              }
            </div>
          </div>
    
          <!-- Empty State when filtered out -->
          @if (filteredRoutes().length === 0) {
            <div
              class="border-border flex flex-col items-center justify-center rounded-xl border border-dashed py-12 text-center"
            >
              <lucide-icon [img]="Truck" class="text-muted-foreground/50 mb-3 size-10" />
              <h3 class="text-foreground text-sm font-semibold">No matching dispatch routes found</h3>
              <p class="text-muted-foreground mt-1 max-w-sm text-xs">
                Try resetting your zone selector or search query to see active drivers and vehicle assignments.
              </p>
              <button ui-button variant="outline" size="sm" class="mt-4 h-8 text-xs" (click)="resetFilters()">
                Reset Filters
              </button>
            </div>
          } @else {
            <div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
              @for (route of filteredRoutes(); track route.id) {
                <div
                  ui-card
                  class="border-border flex flex-col shadow-xs transition-[color,background-color,border-color,box-shadow] hover:shadow-sm"
                >
                  <div ui-card-header class="space-y-3 pb-3">
                    <div class="flex flex-wrap items-start justify-between gap-2">
                      <div class="space-y-0.5">
                        <div class="flex items-center gap-2">
                          <span class="text-primary font-mono text-xs font-semibold">{{ route.routeCode }}</span>
                          <h3 ui-card-title class="text-foreground text-sm font-bold sm:text-base">
                            {{ route.routeName }}
                          </h3>
                        </div>
                        <p class="text-muted-foreground text-xs">{{ route.zone }}</p>
                      </div>
                      <div class="flex items-center gap-1.5">
                        <span
                          ui-badge
                          variant="outline"
                          class="border-success/20 bg-success/10 text-success h-5 px-2 text-xs font-medium"
                        >
                          In Transit · On Schedule
                        </span>
                      </div>
                    </div>
    
                    <div ui-separator></div>
    
                    <div
                      class="bg-muted/40 flex flex-col gap-3 rounded-lg p-3 sm:flex-row sm:items-center sm:justify-between"
                    >
                      <div class="flex items-center gap-3">
                        <ui-avatar class="border-border h-9 w-9 border">
                          <ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-bold">
                            {{ route.driver.initials }}
                          </ui-avatar-fallback>
                        </ui-avatar>
                        <div class="space-y-0.5">
                          <div class="flex items-center gap-1.5">
                            <span class="text-foreground text-xs font-semibold">{{ route.driver.name }}</span>
                            <span class="bg-success inline-flex h-1.5 w-1.5 rounded-full" title="Active"></span>
                          </div>
                          <div class="text-muted-foreground flex items-center gap-2 text-xs">
                            <span class="font-mono tabular-nums">{{ route.driver.phone }}</span>
                            <span>·</span>
                            <span>{{ route.driver.radioChannel }}</span>
                          </div>
                        </div>
                      </div>
    
                      <div
                        class="border-border flex items-center justify-between gap-x-2 border-t pt-2 sm:border-t-0 sm:pt-0 sm:text-right"
                      >
                        <div class="space-y-0.5">
                          <div class="flex items-center gap-1.5 sm:justify-end">
                            <lucide-icon [img]="Truck" class="text-muted-foreground size-3.5" />
                            <span class="text-foreground text-xs font-medium">{{ route.vehicle.name }}</span>
                          </div>
                          <div class="text-muted-foreground flex items-center gap-1.5 text-xs sm:justify-end">
                            <span class="font-mono text-xs">{{ route.vehicle.plateNumber }}</span>
                            <span>·</span>
                            <span class="flex items-center gap-1 font-mono text-xs tabular-nums">
                              @if (route.vehicle.powerType === 'electric') {
                                <lucide-icon [img]="BatteryCharging" class="text-success size-3" />
                              } @else {
                                <lucide-icon [img]="Fuel" class="text-info size-3" />
                              }
                              {{ route.vehicle.powerLevel }}
                            </span>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
    
                  <div ui-card-content class="flex-1 space-y-4">
                    <div class="border-border bg-card grid grid-cols-1 gap-3 rounded-lg border p-3 sm:grid-cols-2">
                      <div class="space-y-1.5">
                        <div class="flex items-center justify-between gap-x-2 text-xs">
                          <span class="text-muted-foreground">Stops Completion</span>
                          <span class="text-foreground font-mono font-semibold tabular-nums">
                            {{ route.completedStops }} / {{ route.totalStops }}
                            <span class="text-muted-foreground font-normal"> ({{ stopPct(route) }}%) </span>
                          </span>
                        </div>
                        <div ui-progress [value]="stopPct(route)" class="h-1.5"></div>
                        <div
                          class="text-muted-foreground flex items-center justify-between gap-x-2 font-mono text-xs tabular-nums"
                        >
                          <span>Shift: {{ route.shiftWindow }}</span>
                          <span>ETA: {{ route.estimatedCompletion }}</span>
                        </div>
                      </div>
    
                      <div class="space-y-1.5">
                        <div class="flex items-center justify-between gap-x-2 text-xs">
                          <span class="text-muted-foreground">Payload Load Balance</span>
                          <span class="text-foreground font-mono font-semibold tabular-nums">
                            {{ route.currentPayloadLbs.toLocaleString() }} / {{ route.maxPayloadLbs.toLocaleString() }} lbs
                          </span>
                        </div>
                        <div class="bg-muted relative h-1.5 w-full overflow-hidden rounded-full">
                          <div [class]="payloadBarClass(route)" [style.width]="payloadPct(route) + '%'"></div>
                        </div>
                        <div
                          class="text-muted-foreground flex items-center justify-between gap-x-2 font-mono text-xs tabular-nums"
                        >
                          <span>{{ payloadPct(route) }}% capacity</span>
                          <span [class]="payloadAvailClass(route)">
                            {{ route.maxPayloadLbs - route.currentPayloadLbs }} lbs available
                          </span>
                        </div>
                      </div>
                    </div>
    
                    <div class="space-y-2">
                      <div class="flex items-center justify-between gap-x-2">
                        <h4 class="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
                          Stop Sequence & Delivery Execution
                        </h4>
                        <span class="text-muted-foreground font-mono text-xs tabular-nums">
                          {{ countBy(route, 'delivered') }} done · {{ countBy(route, 'in_progress') }} active ·
                          {{ countBy(route, 'pending') }} pending
                        </span>
                      </div>
    
                      <div
                        class="before:bg-border relative space-y-3 pl-2 before:absolute before:top-3 before:bottom-3 before:left-5 before:w-px"
                      >
                        @for (stop of route.stops; track stop.id) {
                          <div [class]="stopRowClass(stop.status)">
                            <div class="relative z-10 flex shrink-0 items-center justify-center">
                              @if (stop.status === 'delivered') {
                                <div
                                  class="ring-background bg-success/15 text-success flex h-6 w-6 items-center justify-center rounded-full ring-2"
                                >
                                  <lucide-icon [img]="Check" class="size-3.5 stroke-[3]" />
                                </div>
                              } @else if (stop.status === 'in_progress') {
                                <div
                                  class="bg-primary text-primary-foreground ring-primary/20 relative flex h-6 w-6 items-center justify-center rounded-full shadow-xs ring-4"
                                >
                                  <lucide-icon [img]="MapPin" class="size-3.5 animate-bounce" />
                                </div>
                              } @else {
                                <div
                                  class="border-border bg-muted text-muted-foreground ring-background flex h-6 w-6 items-center justify-center rounded-full border ring-2"
                                >
                                  <lucide-icon [img]="Clock" class="size-3" />
                                </div>
                              }
                            </div>
    
                            <div class="min-w-0 flex-1 space-y-0.5">
                              <div class="flex flex-wrap items-center justify-between gap-1">
                                <div class="flex items-center gap-1.5 truncate">
                                  <span class="text-muted-foreground font-mono text-xs font-semibold">
                                    #{{ stop.sequence }}
                                  </span>
                                  <span class="text-foreground truncate text-xs font-semibold">
                                    {{ stop.locationName }}
                                  </span>
                                </div>
                                <div class="flex items-center gap-1">
                                  @if (stop.status === 'delivered') {
                                    <span
                                      ui-badge
                                      variant="outline"
                                      class="border-success/20 bg-success/10 text-success h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums"
                                    >
                                      Delivered {{ stop.deliveredTime }}
                                    </span>
                                  } @else if (stop.status === 'in_progress') {
                                    <span
                                      ui-badge
                                      variant="default"
                                      class="h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums"
                                    >
                                      In Progress · ETA {{ stop.estimatedTime }}
                                    </span>
                                  } @else {
                                    <span
                                      ui-badge
                                      variant="secondary"
                                      class="text-muted-foreground h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums"
                                    >
                                      Pending
                                    </span>
                                  }
                                </div>
                              </div>
    
                              <p class="text-muted-foreground truncate text-xs">{{ stop.address }}</p>
    
                              <div class="text-muted-foreground flex flex-wrap items-center gap-2 pt-0.5 text-xs">
                                <span class="text-foreground font-mono font-medium tabular-nums">
                                  {{ stop.packagesCount }} {{ stop.packageType }}
                                </span>
                                @if (stop.recipient) {
                                  <span class="text-muted-foreground">· {{ stop.recipient }}</span>
                                }
                                @if (stop.notes) {
                                  <span class="text-warning font-medium">· {{ stop.notes }}</span>
                                }
                              </div>
                            </div>
                          </div>
                        }
                      </div>
                    </div>
                  </div>
    
                  <div ui-card-footer class="border-border border-t pt-3">
                    <div class="flex w-full flex-wrap items-center justify-between gap-2">
                      <div class="flex items-center gap-1.5">
                        <button
                          ui-button
                          variant="outline"
                          size="sm"
                          class="h-8 gap-1.5 text-xs font-medium"
                          (click)="openModal('reassign', route)"
                        >
                          <lucide-icon [img]="ArrowRightLeft" class="size-3.5" />
                          <span>Reassign Stops</span>
                        </button>
                        <button
                          ui-button
                          variant="outline"
                          size="sm"
                          class="h-8 gap-1.5 text-xs font-medium"
                          (click)="openModal('call', route)"
                        >
                          <lucide-icon [img]="Phone" class="size-3.5" />
                          <span>Call Driver</span>
                        </button>
                      </div>
                      <button
                        ui-button
                        variant="default"
                        size="sm"
                        class="h-8 gap-1.5 text-xs font-medium shadow-xs"
                        (click)="openModal('gps', route)"
                      >
                        <lucide-icon [img]="Navigation" class="size-3.5" />
                        <span>View Live GPS</span>
                      </button>
                    </div>
                  </div>
                </div>
              }
            </div>
          }
    
          <!-- Action Modals (GPS telemetry, direct call, reassign stops) -->
          @if (activeModal(); as modal) {
            <div
              class="animate-in fade-in fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs duration-150"
              (click)="onOverlayClick($event)"
            >
              @if (modal.type === 'gps') {
                <div class="border-border bg-card w-full max-w-lg space-y-4 rounded-xl border p-6 shadow-xl">
                  <div class="flex items-center justify-between gap-x-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded-md">
                        <lucide-icon [img]="Navigation" class="size-4" />
                      </div>
                      <div>
                        <h3 class="text-foreground text-sm font-bold">Live Telemetry & GPS Tracking</h3>
                        <p class="text-muted-foreground text-xs">
                          {{ modal.route.routeCode }} · {{ modal.route.driver.name }}
                        </p>
                      </div>
                    </div>
                    <button
                      aria-label="Close modal"
                      type="button"
                      class="text-muted-foreground hover:text-foreground rounded-md p-1"
                      (click)="closeModal()"
                    >
                      <lucide-icon [img]="X" class="size-4" />
                    </button>
                  </div>
    
                  <div ui-separator></div>
    
                  <div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
                    <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center">
                      <span class="text-muted-foreground text-xs">Current Speed</span>
                      <p class="text-foreground font-mono text-sm font-bold">{{ modal.route.telemetry.speed }}</p>
                    </div>
                    <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center">
                      <span class="text-muted-foreground text-xs">Heading</span>
                      <p class="text-foreground font-mono text-sm font-bold">{{ modal.route.telemetry.heading }}</p>
                    </div>
                    <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center">
                      <span class="text-muted-foreground text-xs">Power / Fuel</span>
                      <p class="text-success font-mono text-sm font-bold">{{ modal.route.vehicle.powerLevel }}</p>
                    </div>
                    <div class="border-border bg-muted/30 rounded-lg border p-2.5 text-center">
                      <span class="text-muted-foreground text-xs">GPS Ping</span>
                      <p class="text-foreground font-mono text-sm font-bold">{{ modal.route.telemetry.lastPing }}</p>
                    </div>
                  </div>
    
                  <div class="border-border bg-muted/20 space-y-1.5 rounded-lg border p-3">
                    <div class="flex items-center justify-between gap-x-2 text-xs">
                      <span class="text-muted-foreground">Current Coordinates & Street</span>
                      <span class="text-success font-mono font-medium">Signal: {{ modal.route.telemetry.signal }}</span>
                    </div>
                    <div class="flex items-start gap-2">
                      <lucide-icon [img]="MapPin" class="text-primary mt-0.5 size-4 shrink-0" />
                      <p class="text-foreground text-xs font-semibold">{{ modal.route.telemetry.currentLocation }}</p>
                    </div>
                  </div>
    
                  <div class="border-primary/20 bg-primary/5 space-y-1 rounded-lg border p-3">
                    <span class="text-primary text-xs font-semibold tracking-wide uppercase">Next Stop In Sequence</span>
                    <p class="text-foreground text-xs font-bold">{{ nextStopName(modal.route) }}</p>
                    <p class="text-muted-foreground text-xs">{{ nextStopAddress(modal.route) }}</p>
                  </div>
    
                  <div class="flex justify-end gap-2 pt-2">
                    <button
                      ui-button
                      aria-label="Close modal"
                      variant="outline"
                      size="sm"
                      class="h-8 text-xs"
                      (click)="closeModal()"
                    >
                      Close
                    </button>
                    <button ui-button size="sm" class="h-8 text-xs font-medium" (click)="openModal('call', modal.route)">
                      <lucide-icon [img]="Phone" class="mr-1.5 size-3.5" /> Call Driver
                    </button>
                  </div>
                </div>
              }
    
              @if (modal.type === 'call') {
                <div class="border-border bg-card w-full max-w-md space-y-4 rounded-xl border p-6 shadow-xl">
                  <div class="flex items-center justify-between gap-x-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-success/10 text-success flex h-8 w-8 items-center justify-center rounded-md">
                        <lucide-icon [img]="PhoneCall" class="size-4" />
                      </div>
                      <div>
                        <h3 class="text-foreground text-sm font-bold">Dispatch Driver Communications</h3>
                        <p class="text-muted-foreground text-xs">{{ modal.route.routeCode }} Direct Link</p>
                      </div>
                    </div>
                    <button
                      aria-label="Close modal"
                      type="button"
                      class="text-muted-foreground hover:text-foreground rounded-md p-1"
                      (click)="closeModal()"
                    >
                      <lucide-icon [img]="X" class="size-4" />
                    </button>
                  </div>
    
                  <div ui-separator></div>
    
                  <div class="border-border bg-muted/40 flex items-center gap-3 rounded-lg border p-3">
                    <ui-avatar class="border-border h-11 w-11 border">
                      <ui-avatar-fallback class="bg-primary/10 text-primary text-sm font-bold">
                        {{ modal.route.driver.initials }}
                      </ui-avatar-fallback>
                    </ui-avatar>
                    <div class="space-y-0.5">
                      <h4 class="text-foreground text-sm font-semibold">{{ modal.route.driver.name }}</h4>
                      <p class="text-muted-foreground font-mono text-xs tabular-nums">{{ modal.route.driver.phone }}</p>
                      <p class="text-muted-foreground text-xs">{{ modal.route.driver.radioChannel }} · Hands-Free Active</p>
                    </div>
                  </div>
    
                  <div class="space-y-2 text-xs">
                    <span class="text-muted-foreground font-medium">Quick Broadcast Dispatch Presets:</span>
                    <div class="grid grid-cols-2 gap-2">
                      <button
                        type="button"
                        class="border-border bg-background hover:border-primary/40 hover:bg-accent rounded-md border p-2 text-left transition-colors"
                        (click)="handleConfirmReassign()"
                      >
                        <span class="text-foreground block font-semibold">Traffic Alert</span>
                        <span class="text-muted-foreground text-xs">Reroute around I-10 slowdown</span>
                      </button>
                      <button
                        type="button"
                        class="border-border bg-background hover:border-primary/40 hover:bg-accent rounded-md border p-2 text-left transition-colors"
                        (click)="handleConfirmReassign()"
                      >
                        <span class="text-foreground block font-semibold">Priority Stop</span>
                        <span class="text-muted-foreground text-xs">Bump hospital drop to next</span>
                      </button>
                    </div>
                  </div>
    
                  <div class="flex justify-end gap-2 pt-2">
                    <button
                      ui-button
                      aria-label="Close modal"
                      variant="outline"
                      size="sm"
                      class="h-8 text-xs"
                      (click)="closeModal()"
                    >
                      Cancel
                    </button>
                    <button
                      ui-button
                      size="sm"
                      class="bg-success hover:bg-success/90 h-8 text-xs font-medium text-white"
                      (click)="handleConfirmReassign()"
                    >
                      <lucide-icon [img]="Phone" class="mr-1.5 size-3.5" /> Start Direct Call
                    </button>
                  </div>
                </div>
              }
    
              @if (modal.type === 'reassign') {
                <div class="border-border bg-card w-full max-w-lg space-y-4 rounded-xl border p-6 shadow-xl">
                  <div class="flex items-center justify-between gap-x-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-warning/10 text-warning flex h-8 w-8 items-center justify-center rounded-md">
                        <lucide-icon [img]="ArrowRightLeft" class="size-4" />
                      </div>
                      <div>
                        <h3 class="text-foreground text-sm font-bold">Reassign Pending Stops</h3>
                        <p class="text-muted-foreground text-xs">
                          Transfer workload from {{ modal.route.routeCode }} ({{ modal.route.driver.name }})
                        </p>
                      </div>
                    </div>
                    <button
                      aria-label="Close modal"
                      type="button"
                      class="text-muted-foreground hover:text-foreground rounded-md p-1"
                      (click)="closeModal()"
                    >
                      <lucide-icon [img]="X" class="size-4" />
                    </button>
                  </div>
    
                  <div ui-separator></div>
    
                  <div class="space-y-3 text-xs">
                    <div>
                      <label class="text-foreground mb-1 block font-semibold">Pending Stop to Transfer:</label>
                      <div class="border-border bg-muted/40 rounded-lg border p-3">
                        <div class="flex items-center justify-between gap-x-2">
                          <span class="text-foreground font-semibold">{{ pendingStopName(modal.route) }}</span>
                          <span ui-badge variant="outline" class="text-xs"
                            >{{ pendingStopPackages(modal.route) }} packages</span
                          >
                        </div>
                        <p class="text-muted-foreground mt-0.5 text-xs">{{ pendingStopAddress(modal.route) }}</p>
                      </div>
                    </div>
    
                    <div>
                      <label class="text-foreground mb-1 block font-semibold">Target Available Route / Driver:</label>
                      <div class="space-y-2">
                        @for (target of reassignTargets(modal.route.id); track target.id) {
                          <div
                            [class]="targetRowClass(modal.targetRouteId === target.id)"
                            (click)="selectTargetRoute(target.id)"
                          >
                            <div class="space-y-0.5">
                              <div class="flex items-center gap-1.5">
                                <span class="text-foreground font-semibold"
                                  >{{ target.routeCode }} · {{ target.routeName }}</span
                                >
                                <span class="text-muted-foreground text-xs">({{ target.driver.name }})</span>
                              </div>
                              <p class="text-muted-foreground text-xs">
                                Current Load: {{ target.currentPayloadLbs }} lbs ({{ payloadPct(target) }}%) ·
                                {{ target.totalStops - target.completedStops }} stops left
                              </p>
                            </div>
                            <div [class]="targetRadioClass(modal.targetRouteId === target.id)">
                              @if (modal.targetRouteId === target.id) {
                                <lucide-icon [img]="Check" class="size-3 stroke-3" />
                              }
                            </div>
                          </div>
                        }
                      </div>
                    </div>
                  </div>
    
                  <div class="flex justify-end gap-2 pt-2">
                    <button
                      ui-button
                      aria-label="Close modal"
                      variant="outline"
                      size="sm"
                      class="h-8 text-xs"
                      (click)="closeModal()"
                    >
                      Cancel
                    </button>
                    <button ui-button size="sm" class="h-8 text-xs font-medium" (click)="handleConfirmReassign()">
                      <lucide-icon [img]="Check" class="mr-1.5 size-3.5" /> Confirm Stop Reassignment
                    </button>
                  </div>
                </div>
              }
            </div>
          }
        </div>
      `,
    })
    export class UiDispatchAssignmentBoardComponent {
      protected readonly ArrowRightLeft = ArrowRightLeft
      protected readonly BatteryCharging = BatteryCharging
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Download = Download
      protected readonly Fuel = Fuel
      protected readonly Gauge = Gauge
      protected readonly MapPin = MapPin
      protected readonly Navigation = Navigation
      protected readonly PackageCheck = PackageCheck
      protected readonly Phone = Phone
      protected readonly PhoneCall = PhoneCall
      protected readonly Search = Search
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Truck = Truck
      protected readonly X = X
    
      protected readonly zoneOptions = ZONE_OPTIONS
    
      @Input() set initialZone(v: string | undefined) {
        if (v !== undefined) this.selectedZone.set(v)
      }
      @Input() set initialFilter(v: DispatchFilter | undefined) {
        if (v !== undefined) this.filterType.set(v)
      }
      @Input('class') className?: string
    
      readonly routes = signal<DriverRoute[]>(DRIVER_ROUTES_DATA)
      readonly selectedZone = signal('Metro Los Angeles - North Zone')
      readonly filterType = signal<DispatchFilter>('all')
      readonly searchQuery = signal('')
      readonly isOptimizing = signal(false)
      readonly notificationBanner = signal<{ title: string; message: string; type: 'success' | 'info' } | null>(null)
      readonly activeModal = signal<ActiveModalState | null>(null)
    
      private optimizeTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => clearTimeout(this.optimizeTimer))
      }
    
      readonly filteredRoutes = computed(() => {
        const zone = this.selectedZone()
        const filter = this.filterType()
        const q = this.searchQuery().trim().toLowerCase()
        return this.routes().filter((route) => {
          if (zone !== 'all' && route.zone !== zone) return false
          if (filter === 'in_transit' && route.driver.status !== 'on_route') return false
          if (filter === 'high_load' && route.currentPayloadLbs / route.maxPayloadLbs < 0.8) return false
          if (filter === 'near_complete' && route.completedStops / route.totalStops < 0.75) return false
          if (q) {
            const matchRoute = route.routeCode.toLowerCase().includes(q) || route.routeName.toLowerCase().includes(q)
            const matchDriver = route.driver.name.toLowerCase().includes(q)
            const matchVehicle =
              route.vehicle.name.toLowerCase().includes(q) || route.vehicle.plateNumber.toLowerCase().includes(q)
            const matchStop = route.stops.some(
              (s) => s.locationName.toLowerCase().includes(q) || s.address.toLowerCase().includes(q),
            )
            if (!matchRoute && !matchDriver && !matchVehicle && !matchStop) return false
          }
          return true
        })
      })
    
      get rootClass(): string {
        return cn('text-foreground w-full space-y-6', this.className)
      }
    
      get optimizeIconClass(): string {
        return cn('size-3.5', this.isOptimizing() && 'text-primary-foreground animate-spin')
      }
    
      bannerClass(type: 'success' | 'info'): string {
        return cn(
          'animate-in fade-in-50 flex items-start justify-between rounded-lg border p-3.5 text-xs transition-colors duration-200',
          type === 'success'
            ? 'border-success/30 bg-success/10 text-success'
            : 'border-primary/30 bg-primary/10 text-foreground',
        )
      }
    
      stopPct(route: DriverRoute): number {
        return Math.round((route.completedStops / route.totalStops) * 100)
      }
    
      payloadPct(route: DriverRoute): number {
        return Math.round((route.currentPayloadLbs / route.maxPayloadLbs) * 100)
      }
    
      payloadBarClass(route: DriverRoute): string {
        return cn(
          'h-full rounded-full transition-[width,background-color] duration-500',
          route.currentPayloadLbs / route.maxPayloadLbs > 0.85 ? 'bg-warning' : 'bg-primary',
        )
      }
    
      payloadAvailClass(route: DriverRoute): string {
        return route.currentPayloadLbs / route.maxPayloadLbs > 0.85 ? 'text-warning font-medium' : 'text-muted-foreground'
      }
    
      countBy(route: DriverRoute, status: StopStatus): number {
        return route.stops.filter((s) => s.status === status).length
      }
    
      stopRowClass(status: StopStatus): string {
        return cn(
          'hover:border-border hover:bg-muted/30 relative flex items-start gap-3 rounded-lg border border-transparent p-2 transition-colors',
          status === 'in_progress' && 'border-primary/30 bg-primary/5 dark:bg-primary/10',
        )
      }
    
      nextStopName(route: DriverRoute): string {
        return route.stops.find((s) => s.status === 'in_progress')?.locationName || 'Hub Return'
      }
    
      nextStopAddress(route: DriverRoute): string {
        return route.stops.find((s) => s.status === 'in_progress')?.address || ''
      }
    
      pendingStopName(route: DriverRoute): string {
        return route.stops.find((s) => s.status === 'pending')?.locationName || 'Stop #4 - Delivery Destination'
      }
    
      pendingStopPackages(route: DriverRoute): number {
        return route.stops.find((s) => s.status === 'pending')?.packagesCount || 2
      }
    
      pendingStopAddress(route: DriverRoute): string {
        return route.stops.find((s) => s.status === 'pending')?.address || ''
      }
    
      reassignTargets(excludeId: string): DriverRoute[] {
        return this.routes().filter((r) => r.id !== excludeId)
      }
    
      targetRowClass(selected: boolean): string {
        return cn(
          'flex cursor-pointer items-center justify-between rounded-lg border p-2.5 transition-colors',
          selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/30',
        )
      }
    
      targetRadioClass(selected: boolean): string {
        return cn(
          'border-border flex h-5 w-5 items-center justify-center rounded-full border',
          selected && 'border-primary bg-primary text-primary-foreground',
        )
      }
    
      handleAutoOptimize(): void {
        this.isOptimizing.set(true)
        this.notificationBanner.set(null)
        clearTimeout(this.optimizeTimer)
        this.optimizeTimer = setTimeout(() => {
          this.isOptimizing.set(false)
          this.notificationBanner.set({
            title: 'AI Route Optimization Complete',
            message: 'Re-sequenced 4 active routes · Saved 18.4 road miles · Estimated 22 min overall transit reduction.',
            type: 'success',
          })
        }, 750)
      }
    
      handleExportManifests(): void {
        this.notificationBanner.set({
          title: 'Dispatch Manifests Exported',
          message: 'Generated PDF loading manifests and CSV stop sequences for 4 routes (180 total stops).',
          type: 'info',
        })
      }
    
      openModal(type: 'gps' | 'call' | 'reassign', route: DriverRoute): void {
        this.activeModal.set({
          type,
          route,
          targetRouteId: this.routes().find((r) => r.id !== route.id)?.id,
        })
      }
    
      closeModal(): void {
        this.activeModal.set(null)
      }
    
      onOverlayClick(event: MouseEvent): void {
        if (event.target === event.currentTarget) this.closeModal()
      }
    
      selectTargetRoute(targetId: string): void {
        const modal = this.activeModal()
        if (modal) this.activeModal.set({ ...modal, targetRouteId: targetId })
      }
    
      handleConfirmReassign(): void {
        const modal = this.activeModal()
        if (modal) {
          this.notificationBanner.set({
            title: 'Stops Successfully Reassigned',
            message: `Pending stop from ${modal.route.routeCode} transferred. Driver GPS routes and manifest synced.`,
            type: 'success',
          })
          this.closeModal()
        }
      }
    
      resetFilters(): void {
        this.selectedZone.set('all')
        this.searchQuery.set('')
        this.filterType.set('all')
      }
    }
    
  • src/app/components/blocks/dispatch-assignment-board/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/dispatch-assignment-board.json