UIPackage
Menu

Framework

Change language

Boilerplate 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.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/dispatch-assignment-board.json
Named registry:npx shadcn-vue@latest add @uipkge/dispatch-assignment-boardInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
initialZonestring'Metro Los Angeles - North Zone'optional
initialFilter
'all''in_transit''high_load''near_complete'
'all'optional
classHTMLAttributes['class']optional

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 (4)

  • app/components/blocks/DispatchAssignmentBoard.vue29.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { computed, ref } from 'vue'
    import {
      ArrowRightLeft,
      BatteryCharging,
      Check,
      Clock,
      Download,
      Fuel,
      Gauge,
      MapPin,
      Navigation,
      PackageCheck,
      Phone,
      Search,
      CheckCircle2,
      ShieldCheck,
      Truck,
      X,
    } from 'lucide-vue-next'
    import DispatchActionModal from './DispatchActionModal.vue'
    import { DRIVER_ROUTES_DATA } from './dispatch-data'
    import type { ActiveModalState, DriverRoute, RouteStop, StopStatus } from './dispatch-assignment-board-types'
    export type { ActiveModalState, DriverRoute, RouteStop, StopStatus } from './dispatch-assignment-board-types'
    import { cn } from '@/lib/utils'
    import { Avatar, AvatarFallback } from '@/components/ui/avatar'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
    import { Progress } from '@/components/ui/progress'
    import {
      Select,
      SelectContent,
      SelectGroup,
      SelectItem,
      SelectLabel,
      SelectTrigger,
      SelectValue,
    } from '@/components/ui/select'
    import { Separator } from '@/components/ui/separator'
    
    const props = withDefaults(
      defineProps<{
        initialZone?: string
        initialFilter?: 'all' | 'in_transit' | 'high_load' | 'near_complete'
        class?: HTMLAttributes['class']
      }>(),
      {
        initialZone: 'Metro Los Angeles - North Zone',
        initialFilter: 'all',
      },
    )
    
    const selectedZone = ref(props.initialZone)
    const filterType = ref<'all' | 'in_transit' | 'high_load' | 'near_complete'>(props.initialFilter)
    const searchQuery = ref('')
    const isOptimizing = ref(false)
    const notificationBanner = ref<{ title: string; message: string; type: 'success' | 'info' } | null>(null)
    const activeModal = ref<{
      type: 'gps' | 'call' | 'reassign'
      route: DriverRoute
      targetRouteId?: string
    } | null>(null)
    
    const filteredRoutes = computed(() => {
      return DRIVER_ROUTES_DATA.filter((route) => {
        // Zone filter
        if (selectedZone.value !== 'all' && route.zone !== selectedZone.value) {
          return false
        }
    
        // Category / Quick filter
        if (filterType.value === 'in_transit' && route.driver.status !== 'on_route') {
          return false
        }
        if (filterType.value === 'high_load' && route.currentPayloadLbs / route.maxPayloadLbs < 0.8) {
          return false
        }
        if (filterType.value === 'near_complete' && route.completedStops / route.totalStops < 0.75) {
          return false
        }
    
        // Search query
        if (searchQuery.value.trim()) {
          const q = searchQuery.value.toLowerCase()
          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
      })
    })
    
    function handleAutoOptimize() {
      isOptimizing.value = true
      notificationBanner.value = null
    
      setTimeout(() => {
        isOptimizing.value = false
        notificationBanner.value = {
          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)
    }
    
    function handleExportManifests() {
      notificationBanner.value = {
        title: 'Dispatch Manifests Exported',
        message: 'Generated PDF loading manifests and CSV stop sequences for 4 routes (180 total stops).',
        type: 'info',
      }
    }
    
    function openModal(type: 'gps' | 'call' | 'reassign', route: DriverRoute) {
      activeModal.value = {
        type,
        route,
        targetRouteId: DRIVER_ROUTES_DATA.find((r) => r.id !== route.id)?.id,
      }
    }
    
    function closeModal() {
      activeModal.value = null
    }
    
    function resetFilters() {
      selectedZone.value = 'all'
      searchQuery.value = ''
      filterType.value = 'all'
    }
    
    function handleConfirmReassign() {
      if (activeModal.value) {
        const route = activeModal.value.route
        notificationBanner.value = {
          title: 'Stops Successfully Reassigned',
          message: `Pending stop from ${route.routeCode} transferred. Driver GPS routes and manifest synced.`,
          type: 'success',
        }
        closeModal()
      }
    }
    </script>
    
    <template>
      <div :class="cn('text-foreground w-full space-y-6', props.class)" data-slot="dispatch-assignment-board">
        <!-- 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 class="bg-success relative inline-flex h-2 w-2 rounded-full" />
                </span>
                <span>Live Dispatch Operations</span>
              </div>
              <div class="text-muted-foreground flex items-center gap-1 text-xs">
                <Clock class="h-3.5 w-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>
    
          <!-- Header Controls -->
          <div class="flex flex-wrap items-center gap-2.5">
            <div class="w-full sm:w-64">
              <Select v-model="selectedZone">
                <SelectTrigger class="h-9 w-full text-xs font-medium" aria-label="Select Dispatch Zone">
                  <SelectValue placeholder="Select Zone" />
                </SelectTrigger>
                <SelectContent>
                  <SelectGroup>
                    <SelectLabel class="text-xs">Dispatch Zones</SelectLabel>
                    <SelectItem value="all" class="text-xs">All Dispatch Zones</SelectItem>
                    <SelectItem value="Metro Los Angeles - North Zone" class="text-xs">
                      Metro Los Angeles - North Zone
                    </SelectItem>
                    <SelectItem value="Metro Los Angeles - South Zone" class="text-xs">
                      Metro Los Angeles - South Zone
                    </SelectItem>
                    <SelectItem value="San Fernando Valley Hub" class="text-xs"> San Fernando Valley Hub </SelectItem>
                    <SelectItem value="Orange County Central" class="text-xs"> Orange County Central </SelectItem>
                  </SelectGroup>
                </SelectContent>
              </Select>
            </div>
    
            <Button
              aria-label="Download attachment"
              variant="outline"
              size="sm"
              class="h-9 gap-1.5 text-xs font-medium"
              @click="handleExportManifests"
            >
              <Download class="h-3.5 w-3.5" />
              <span>Export Manifests</span>
            </Button>
    
            <Button
              size="sm"
              class="h-9 gap-1.5 text-xs font-medium shadow-xs"
              :disabled="isOptimizing"
              @click="handleAutoOptimize"
            >
              <Navigation :class="cn('h-3.5 w-3.5', isOptimizing && 'text-primary-foreground animate-spin')" />
              <span>{{ isOptimizing ? 'Optimizing Routes...' : 'Auto-Optimize Routes' }}</span>
            </Button>
          </div>
        </div>
    
        <!-- Notification Banner -->
        <div
          v-if="notificationBanner"
          :class="
            cn(
              'animate-in fade-in-50 flex items-start justify-between rounded-lg border p-3.5 text-xs transition-colors duration-200',
              notificationBanner.type === 'success'
                ? 'border-success/30 bg-success/10 text-success'
                : 'border-primary/30 bg-primary/10 text-foreground',
            )
          "
        >
          <div class="flex items-start gap-2.5">
            <CheckCircle2 v-if="notificationBanner.type === 'success'" class="text-success mt-0.5 h-4 w-4 shrink-0" />
            <PackageCheck v-else class="text-primary mt-0.5 h-4 w-4 shrink-0" />
            <div>
              <span class="font-semibold">{{ notificationBanner.title }}:</span>
              <span class="text-muted-foreground dark:text-foreground/80 ml-1">{{ notificationBanner.message }}</span>
            </div>
          </div>
          <button
            aria-label="Dismiss notification"
            type="button"
            class="text-muted-foreground hover:text-foreground transition-colors"
            @click="notificationBanner = null"
          >
            <X class="h-4 w-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">
          <!-- KPI 1: Active Routes -->
          <Card class="border-border shadow-xs">
            <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
              <CardTitle class="text-muted-foreground text-xs font-medium">Total Routes Active</CardTitle>
              <div class="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded-md">
                <Truck class="h-4 w-4" />
              </div>
            </CardHeader>
            <CardContent 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">
                <Badge
                  variant="outline"
                  class="border-success/20 bg-success/10 text-success h-5 px-1.5 text-xs font-medium"
                >
                  6 In Transit
                </Badge>
                <span>2 loading at hub</span>
              </div>
            </CardContent>
          </Card>
    
          <!-- KPI 2: Stops Remaining & Progress -->
          <Card class="border-border shadow-xs">
            <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
              <CardTitle class="text-muted-foreground text-xs font-medium">Total Stops Remaining</CardTitle>
              <div class="bg-info/10 text-info flex h-8 w-8 items-center justify-center rounded-md">
                <PackageCheck class="h-4 w-4" />
              </div>
            </CardHeader>
            <CardContent 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 text-info font-mono text-xs font-semibold tabular-nums">78.8%</span>
              </div>
              <Progress :model-value="78.8" class="h-1.5" />
              <p class="text-muted-foreground text-xs">
                <span class="text-foreground font-mono font-semibold tabular-nums">38</span> stops pending completion
              </p>
            </CardContent>
          </Card>
    
          <!-- KPI 3: On-Time Rate -->
          <Card class="border-border shadow-xs">
            <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
              <CardTitle class="text-muted-foreground text-xs font-medium">On-Time Delivery Rate</CardTitle>
              <div class="bg-success/10 text-success flex h-8 w-8 items-center justify-center rounded-md">
                <ShieldCheck class="h-4 w-4" />
              </div>
            </CardHeader>
            <CardContent class="space-y-1.5">
              <div class="flex items-baseline gap-2">
                <span class="text-success text-success font-mono text-2xl font-bold tracking-tight tabular-nums"
                  >97.4%</span
                >
                <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
                </Badge>
              </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>
            </CardContent>
          </Card>
    
          <!-- KPI 4: Cargo Weight Dispatched -->
          <Card class="border-border shadow-xs">
            <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
              <CardTitle class="text-muted-foreground text-xs font-medium">Total Cargo Weight Dispatched</CardTitle>
              <div class="bg-warning/10 text-warning flex h-8 w-8 items-center justify-center rounded-md">
                <Gauge class="h-4 w-4" />
              </div>
            </CardHeader>
            <CardContent 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>
                <Badge
                  variant="outline"
                  class="border-warning/20 bg-warning/10 text-warning h-5 px-1.5 text-xs font-medium"
                >
                  82% capacity
                </Badge>
              </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>
              <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>
            </CardContent>
          </Card>
        </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
              size="sm"
              :variant="filterType === 'all' ? 'default' : 'outline'"
              class="h-8 text-xs font-medium"
              @click="filterType = 'all'"
            >
              All Routes ({{ DRIVER_ROUTES_DATA.length }})
            </Button>
            <Button
              size="sm"
              :variant="filterType === 'in_transit' ? 'default' : 'outline'"
              class="h-8 text-xs font-medium"
              @click="filterType = 'in_transit'"
            >
              In Transit (4)
            </Button>
            <Button
              size="sm"
              :variant="filterType === 'high_load' ? 'default' : 'outline'"
              class="h-8 text-xs font-medium"
              @click="filterType = 'high_load'"
            >
              High Load (>80%)
            </Button>
            <Button
              size="sm"
              :variant="filterType === 'near_complete' ? 'default' : 'outline'"
              class="h-8 text-xs font-medium"
              @click="filterType = 'near_complete'"
            >
              Near Completion (>70%)
            </Button>
          </div>
    
          <div class="relative w-full sm:w-72">
            <Search class="text-muted-foreground absolute top-2.5 left-2.5 h-3.5 w-3.5" />
            <input
              v-model="searchQuery"
              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"
            />
            <button
              aria-label="Clear search"
              v-if="searchQuery"
              type="button"
              class="text-muted-foreground hover:text-foreground absolute top-2 right-2"
              @click="searchQuery = ''"
            >
              <X class="h-3.5 w-3.5" />
            </button>
          </div>
        </div>
    
        <!-- Empty State when filtered out -->
        <div
          v-if="filteredRoutes.length === 0"
          class="border-border flex flex-col items-center justify-center rounded-xl border border-dashed py-12 text-center"
        >
          <Truck class="text-muted-foreground/50 mb-3 h-10 w-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 variant="outline" size="sm" class="mt-4 h-8 text-xs" @click="resetFilters"> Reset Filters </Button>
        </div>
    
        <!-- Driver Routes Grid / Multi-Card Board (4 Driver Route Cards) -->
        <div v-else class="grid grid-cols-1 gap-6 xl:grid-cols-2">
          <Card
            v-for="route in filteredRoutes"
            :key="route.id"
            class="border-border flex flex-col shadow-xs transition-[color,background-color,border-color,box-shadow] hover:shadow-sm"
          >
            <!-- Card Header: Route ID, Status, Driver and Vehicle snapshot -->
            <CardHeader 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>
                    <CardTitle class="text-foreground text-sm font-bold sm:text-base">
                      {{ route.routeName }}
                    </CardTitle>
                  </div>
                  <p class="text-muted-foreground text-xs">{{ route.zone }}</p>
                </div>
    
                <div class="flex items-center gap-1.5">
                  <Badge
                    variant="outline"
                    class="border-success/20 bg-success/10 text-success h-5 px-2 text-xs font-medium"
                  >
                    In Transit · On Schedule
                  </Badge>
                </div>
              </div>
    
              <Separator />
    
              <!-- Driver Profile & Vehicle Metadata -->
              <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">
                  <Avatar class="border-border h-9 w-9 border">
                    <AvatarFallback class="bg-primary/10 text-primary text-xs font-bold">
                      {{ route.driver.initials }}
                    </AvatarFallback>
                  </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" />
                    </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">
                      <Truck class="text-muted-foreground h-3.5 w-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">
                        <BatteryCharging v-if="route.vehicle.powerType === 'electric'" class="text-success h-3 w-3" />
                        <Fuel v-else class="text-info h-3 w-3" />
                        {{ route.vehicle.powerLevel }}
                      </span>
                    </div>
                  </div>
                </div>
              </div>
            </CardHeader>
    
            <CardContent class="flex-1 space-y-4">
              <!-- Route Progress & Payload Load Balance Overview -->
              <div class="border-border bg-card grid grid-cols-1 gap-3 rounded-lg border p-3 sm:grid-cols-2">
                <!-- Stops Progress -->
                <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"
                        >({{ Math.round((route.completedStops / route.totalStops) * 100) }}%)</span
                      >
                    </span>
                  </div>
                  <Progress :model-value="(route.completedStops / route.totalStops) * 100" class="h-1.5" />
                  <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>
    
                <!-- Payload Weight Utilization -->
                <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="h-full rounded-full transition-[width,background-color] duration-500"
                      :class="route.currentPayloadLbs / route.maxPayloadLbs > 0.85 ? 'bg-warning' : 'bg-primary'"
                      :style="`width: ${(route.currentPayloadLbs / route.maxPayloadLbs) * 100}%`"
                    />
                  </div>
                  <div
                    class="text-muted-foreground flex items-center justify-between gap-x-2 font-mono text-xs tabular-nums"
                  >
                    <span>{{ Math.round((route.currentPayloadLbs / route.maxPayloadLbs) * 100) }}% capacity</span>
                    <span
                      :class="
                        route.currentPayloadLbs / route.maxPayloadLbs > 0.85
                          ? 'text-warning font-medium'
                          : 'text-muted-foreground'
                      "
                    >
                      {{ route.maxPayloadLbs - route.currentPayloadLbs }} lbs available
                    </span>
                  </div>
                </div>
              </div>
    
              <!-- Stops Timeline Section -->
              <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">
                    {{ route.stops.filter((s) => s.status === 'delivered').length }} done ·
                    {{ route.stops.filter((s) => s.status === 'in_progress').length }} active ·
                    {{ route.stops.filter((s) => s.status === 'pending').length }} pending
                  </span>
                </div>
    
                <!-- Timeline List -->
                <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"
                >
                  <div
                    v-for="stop in route.stops"
                    :key="stop.id"
                    class="hover:border-border hover:bg-muted/30 relative flex items-start gap-3 rounded-lg border border-transparent p-2 transition-colors"
                    :class="stop.status === 'in_progress' && 'border-primary/30 bg-primary/5 dark:bg-primary/10'"
                  >
                    <!-- Status Icon Node -->
                    <div class="relative z-10 flex shrink-0 items-center justify-center">
                      <!-- Delivered Check Icon -->
                      <div
                        v-if="stop.status === 'delivered'"
                        class="ring-background bg-success/15 text-success text-success flex h-6 w-6 items-center justify-center rounded-full ring-2"
                      >
                        <Check class="h-3.5 w-3.5 stroke-3" />
                      </div>
    
                      <!-- In Progress Pulsing Map Pin -->
                      <div
                        v-else-if="stop.status === 'in_progress'"
                        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"
                      >
                        <MapPin class="h-3.5 w-3.5 animate-bounce" />
                      </div>
    
                      <!-- Pending Clock / Circle Node -->
                      <div
                        v-else
                        class="border-border bg-muted text-muted-foreground ring-background flex h-6 w-6 items-center justify-center rounded-full border ring-2"
                      >
                        <Clock class="h-3 w-3" />
                      </div>
                    </div>
    
                    <!-- Stop Details -->
                    <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>
    
                        <!-- Stop Status Timestamp / ETA -->
                        <div class="flex items-center gap-1">
                          <Badge
                            v-if="stop.status === 'delivered'"
                            variant="outline"
                            class="border-success/20 bg-success/10 text-success text-success h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums"
                          >
                            Delivered {{ stop.deliveredTime }}
                          </Badge>
                          <Badge
                            v-else-if="stop.status === 'in_progress'"
                            class="bg-primary text-primary-foreground h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums"
                          >
                            In Progress · ETA {{ stop.estimatedTime }}
                          </Badge>
                          <Badge
                            v-else
                            variant="secondary"
                            class="text-muted-foreground h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums"
                          >
                            Pending
                          </Badge>
                        </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>
                        <span v-if="stop.recipient" class="text-muted-foreground">· {{ stop.recipient }}</span>
                        <span v-if="stop.notes" class="text-warning font-medium">· {{ stop.notes }}</span>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </CardContent>
    
            <!-- Card Footer: Quick Action Buttons -->
            <CardFooter 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
                    variant="outline"
                    size="sm"
                    class="h-8 gap-1.5 text-xs font-medium"
                    @click="openModal('reassign', route)"
                  >
                    <ArrowRightLeft class="h-3.5 w-3.5" />
                    <span>Reassign Stops</span>
                  </Button>
    
                  <Button
                    variant="outline"
                    size="sm"
                    class="h-8 gap-1.5 text-xs font-medium"
                    @click="openModal('call', route)"
                  >
                    <Phone class="h-3.5 w-3.5" />
                    <span>Call Driver</span>
                  </Button>
                </div>
    
                <Button
                  variant="default"
                  size="sm"
                  class="h-8 gap-1.5 text-xs font-medium shadow-xs"
                  @click="openModal('gps', route)"
                >
                  <Navigation class="h-3.5 w-3.5" />
                  <span>View Live GPS</span>
                </Button>
              </div>
            </CardFooter>
          </Card>
        </div>
    
        <!-- Action Modals (GPS telemetry, direct call, reassign stops) -->
        <DispatchActionModal
          v-model="activeModal"
          :all-routes="DRIVER_ROUTES_DATA"
          @confirm-reassign="handleConfirmReassign"
          @open-modal="openModal"
        />
      </div>
    </template>
    
  • app/components/blocks/DispatchActionModal.vue12.2 kB
  • app/components/blocks/dispatch-assignment-board-types.ts1.1 kB
  • app/components/blocks/dispatch-data.ts8.4 kB

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