{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "last-mile-courier-dispatch",
  "title": "Last Mile Courier Dispatch",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/last-mile-courier-dispatch/last-mile-courier-dispatch.component.ts",
      "content": "// Last-mile courier dispatch. Route telemetry KPIs, a live GPS map with sequenced drop pins\n// and the courier van, and a delivery manifest whose \"Confirm POD\" logs proof of delivery\n// and moves the next scheduled stop en route.\nimport {\n  type AfterViewInit,\n  Component,\n  type ElementRef,\n  Input,\n  type OnDestroy,\n  type QueryList,\n  ViewChild,\n  ViewChildren,\n  computed,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport type * as mapboxgl from 'mapbox-gl'\nimport { BatteryCharging, LucideAngularModule, Navigation, Phone, Radio, Truck } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardFooterComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiMapComponent, type MapLayerInput, type MapSourceInput } from '@/ui/map/map.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\n\nexport interface DeliveryStop {\n  id: string\n  stopNumber: number\n  trackingCode: string\n  recipient: string\n  address: string\n  parcels: number\n  timeWindow: string\n  eta: string\n  status: 'delivered' | 'en_route' | 'scheduled' | 'failed'\n  podType?: 'signature' | 'photo_scanned' | 'front_door'\n  podTimestamp?: string\n  coords?: [number, number]\n}\n\nexport interface LastMileDispatchProps {\n  routeId?: string\n  driverName?: string\n  vehicleCode?: string\n  className?: string\n  accessToken?: string\n}\n\nexport const DEFAULT_DELIVERY_STOPS: DeliveryStop[] = [\n  {\n    id: 'stp-1',\n    stopNumber: 1,\n    trackingCode: 'TRK-9941-8812',\n    recipient: 'Apex Logistics HQ',\n    address: '742 Evergreen Terrace, Sector 4',\n    parcels: 3,\n    timeWindow: '08:30 - 09:30',\n    eta: '08:52 AM',\n    status: 'delivered',\n    podType: 'signature',\n    podTimestamp: '08:54 AM (Signed by J. Doe)',\n    coords: [-73.991, 40.743],\n  },\n  {\n    id: 'stp-2',\n    stopNumber: 2,\n    trackingCode: 'TRK-9941-8813',\n    recipient: 'David K. Miller',\n    address: '1204 Pinehurst Ave, Apt 3B',\n    parcels: 1,\n    timeWindow: '09:30 - 10:30',\n    eta: '09:45 AM',\n    status: 'delivered',\n    podType: 'photo_scanned',\n    podTimestamp: '09:48 AM (Doorstep Verified)',\n    coords: [-73.987, 40.747],\n  },\n  {\n    id: 'stp-3',\n    stopNumber: 3,\n    trackingCode: 'TRK-9941-8814',\n    recipient: 'Global Tech Labs',\n    address: '450 Innovation Parkway, Suite 100',\n    parcels: 4,\n    timeWindow: '10:00 - 11:30',\n    eta: '10:25 AM',\n    status: 'en_route',\n    coords: [-73.982, 40.751],\n  },\n  {\n    id: 'stp-4',\n    stopNumber: 4,\n    trackingCode: 'TRK-9941-8815',\n    recipient: 'Sarah Jenkins',\n    address: '88 Riverfront Blvd',\n    parcels: 2,\n    timeWindow: '11:30 - 12:30',\n    eta: '11:45 AM',\n    status: 'scheduled',\n    coords: [-73.978, 40.755],\n  },\n  {\n    id: 'stp-5',\n    stopNumber: 5,\n    trackingCode: 'TRK-9941-8816',\n    recipient: 'Metro Health Clinic',\n    address: '310 Health Sciences Way',\n    parcels: 1,\n    timeWindow: '12:30 - 14:00',\n    eta: '01:10 PM',\n    status: 'scheduled',\n    coords: [-73.971, 40.758],\n  },\n]\n\n/** The live courier van position (the React block's static van MapMarker). */\nexport const COURIER_VAN_LNG_LAT: [number, number] = [-73.984, 40.749]\n\nconst courierRoutePaint = {\n  'line-color': '#0284c7',\n  'line-width': 3,\n  'line-dasharray': [2, 2],\n}\n\n/**\n * Angular port of the React LastMileCourierDispatch block: route KPIs, a live GPS corridor\n * map and a delivery manifest with Proof of Delivery logging.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-last-mile-courier-dispatch, [ui-last-mile-courier-dispatch]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"last-mile-courier-dispatch\"',\n    '[class]': 'hostClass',\n  },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardFooterComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiMapComponent,\n    UiTableBodyComponent,\n    UiTableCellComponent,\n    UiTableComponent,\n    UiTableHeadComponent,\n    UiTableHeaderComponent,\n    UiTableRowComponent,\n  ],\n  template: `\n    <!-- Header / Dispatch Telemetry Overview -->\n    <div ui-card>\n      <div ui-card-header class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n        <div class=\"space-y-1\">\n          <div class=\"flex items-center gap-2\">\n            <span class=\"text-foreground font-mono text-sm font-semibold\">{{ routeId }}</span>\n            <span ui-badge variant=\"outline\" class=\"font-mono text-xs\">\n              {{ vehicleCode }}\n            </span>\n            <span ui-badge variant=\"secondary\" class=\"gap-1 text-xs\">\n              <lucide-icon [img]=\"Radio\" class=\"text-foreground size-3\" />\n              Live Telemetry Active\n            </span>\n          </div>\n          <h3 ui-card-title class=\"text-xl\">Last-Mile Courier Dispatch &amp; POD Telemetry</h3>\n          <p ui-card-description>\n            Courier: <span class=\"text-foreground font-medium\">{{ driverName }}</span> · Urban Route Dispatch Matrix\n          </p>\n        </div>\n\n        <div class=\"flex items-center gap-2\">\n          <button ui-button variant=\"outline\" size=\"sm\" class=\"gap-1.5\">\n            <lucide-icon [img]=\"Phone\" class=\"size-3.5\" />\n            Contact Driver\n          </button>\n          <button ui-button variant=\"default\" size=\"sm\" class=\"gap-1.5\">\n            <lucide-icon [img]=\"Navigation\" class=\"size-3.5\" />\n            Live GPS Map\n          </button>\n        </div>\n      </div>\n\n      <div ui-card-content class=\"border-border grid grid-cols-1 gap-4 border-t pt-4 sm:grid-cols-4\">\n        <!-- Progress Metric -->\n        <div class=\"border-border space-y-1 rounded-lg border p-3\">\n          <div class=\"text-muted-foreground text-xs font-medium\">ROUTE COMPLETION</div>\n          <div class=\"flex items-baseline justify-between pt-1\">\n            <div class=\"font-mono text-xl font-semibold\">{{ completedCount() }} / {{ totalStops() }}</div>\n            <span class=\"text-muted-foreground font-mono text-xs\">{{ completionPercent() }}%</span>\n          </div>\n        </div>\n\n        <!-- On-time SLA -->\n        <div class=\"border-border space-y-1 rounded-lg border p-3\">\n          <div class=\"text-muted-foreground text-xs font-medium\">ON-TIME WINDOW SLA</div>\n          <div class=\"pt-1 font-mono text-xl font-semibold\">{{ onTimeRate }}</div>\n        </div>\n\n        <!-- Vehicle Telemetry -->\n        <div class=\"border-border space-y-1 rounded-lg border p-3\">\n          <div class=\"text-muted-foreground flex items-center gap-1 text-xs font-medium\">\n            <lucide-icon [img]=\"BatteryCharging\" class=\"size-3.5\" />\n            <span>EV BATTERY / RANGE</span>\n          </div>\n          <div class=\"pt-1 font-mono text-xl font-semibold\">74% · 142 km</div>\n        </div>\n\n        <!-- Next Stop ETA -->\n        <div class=\"border-border space-y-1 rounded-lg border p-3\">\n          <div class=\"text-muted-foreground text-xs font-medium\">CURRENT TARGET ETA</div>\n          <div class=\"pt-1 font-mono text-xl font-semibold\">10:25 AM (Stop #3)</div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Live GPS Route Map -->\n    <div ui-card class=\"overflow-hidden\">\n      <div ui-card-header class=\"border-border/70 border-b pb-3\">\n        <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n          <div>\n            <h3 ui-card-title class=\"flex items-center gap-2 text-base font-semibold\">\n              <lucide-icon [img]=\"Navigation\" class=\"text-info size-4\" />\n              <span>Live GPS Telemetry &amp; Corridor Map</span>\n            </h3>\n            <p ui-card-description>\n              Real-time courier positioning with sequenced drop waypoints and completion status.\n            </p>\n          </div>\n          <div class=\"flex items-center gap-2\">\n            <span class=\"bg-success inline-flex size-2 animate-ping rounded-full\"></span>\n            <span class=\"text-muted-foreground font-mono text-xs\">GPS Synced · 3s ago</span>\n          </div>\n        </div>\n      </div>\n      <div class=\"relative h-72 w-full sm:h-80\">\n        <div\n          ui-map\n          variant=\"dark\"\n          [accessToken]=\"accessToken ?? ''\"\n          [center]=\"[-73.982, 40.75]\"\n          [zoom]=\"13.8\"\n          class=\"size-full\"\n          [sources]=\"routeSources()\"\n          [layers]=\"routeLayers\"\n          (created)=\"onMapCreated($event)\"\n        >\n          <!-- Marker elements: handed to mapbox-gl Markers once the map exists, which moves each\n               inner #stopMarker / #vanMarker node onto the map; the outer node stays for @for. -->\n          <div class=\"hidden\">\n            <!-- Delivery Stop Markers -->\n            @for (stop of stops(); track stop.id) {\n              <div>\n                <div #stopMarker>\n                  <div class=\"flex flex-col items-center\">\n                    <div [class]=\"stopMarkerClass(stop)\">\n                      {{ stop.stopNumber }}\n                    </div>\n                    <span\n                      class=\"border-border/80 bg-background/90 py-0.2 mt-0.5 rounded border px-1 font-mono text-xs font-medium shadow-xs backdrop-blur-xs\"\n                    >\n                      {{ firstWord(stop.recipient) }}\n                    </span>\n                  </div>\n                </div>\n              </div>\n            }\n\n            <!-- Live Courier Van Marker -->\n            <div>\n              <div #vanMarker class=\"z-30\">\n                <div class=\"relative flex size-9 items-center justify-center\">\n                  <span class=\"bg-info absolute inline-flex size-full animate-ping rounded-full opacity-60\"></span>\n                  <div\n                    class=\"bg-info relative flex size-8 items-center justify-center rounded-full border-2 border-white text-white shadow-lg\"\n                  >\n                    <lucide-icon [img]=\"Truck\" class=\"size-4\" />\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Dispatch Sequence Table -->\n    <div ui-card>\n      <div ui-card-header class=\"pb-3\">\n        <h3 ui-card-title class=\"text-base font-semibold\">Delivery Manifest &amp; Proof of Delivery (POD)</h3>\n        <p ui-card-description>\n          Ordered drop sequence with recipient signature capture and delivery window timestamps.\n        </p>\n      </div>\n\n      <div ui-card-content class=\"p-0\">\n        <div ui-table>\n          <thead ui-table-header>\n            <tr ui-table-row>\n              <th ui-table-head class=\"w-[60px]\">Stop</th>\n              <th ui-table-head class=\"w-[180px]\">Tracking ID</th>\n              <th ui-table-head class=\"w-[260px]\">Recipient &amp; Address</th>\n              <th ui-table-head>Window / ETA</th>\n              <th ui-table-head class=\"text-right\">Parcels</th>\n              <th ui-table-head>Status &amp; Proof of Delivery</th>\n              <th ui-table-head class=\"w-[100px] text-right\">Action</th>\n            </tr>\n          </thead>\n          <tbody ui-table-body>\n            @for (stop of stops(); track stop.id) {\n              <tr ui-table-row [class]=\"stop.status === 'en_route' ? 'bg-muted/40 font-medium' : ''\">\n                <!-- Stop Number -->\n                <td ui-table-cell class=\"text-muted-foreground font-mono text-xs\">#0{{ stop.stopNumber }}</td>\n\n                <!-- Tracking Code -->\n                <td ui-table-cell>\n                  <div class=\"text-foreground font-mono text-xs font-semibold\">{{ stop.trackingCode }}</div>\n                </td>\n\n                <!-- Recipient & Address -->\n                <td ui-table-cell>\n                  <div class=\"text-foreground font-medium\">{{ stop.recipient }}</div>\n                  <div class=\"text-muted-foreground truncate text-xs\">{{ stop.address }}</div>\n                </td>\n\n                <!-- Time Window & ETA -->\n                <td ui-table-cell>\n                  <div class=\"text-foreground text-xs\">{{ stop.timeWindow }}</div>\n                  <div class=\"text-muted-foreground font-mono text-xs\">ETA: {{ stop.eta }}</div>\n                </td>\n\n                <!-- Parcels Count -->\n                <td ui-table-cell class=\"text-right font-mono text-sm\">{{ stop.parcels }} pkg</td>\n\n                <!-- Status & Proof of Delivery -->\n                <td ui-table-cell>\n                  <div class=\"flex flex-col gap-1\">\n                    <div class=\"flex items-center gap-2\">\n                      <span ui-badge [variant]=\"statusVariant(stop.status)\" class=\"capitalize\">\n                        {{ statusLabel(stop.status) }}\n                      </span>\n                    </div>\n                    @if (stop.podTimestamp) {\n                      <span class=\"text-muted-foreground font-mono text-xs\">{{ stop.podTimestamp }}</span>\n                    }\n                  </div>\n                </td>\n\n                <!-- Action -->\n                <td ui-table-cell class=\"text-right\">\n                  @if (stop.status === 'en_route') {\n                    <button ui-button size=\"sm\" variant=\"default\" class=\"h-7 text-xs\" (click)=\"markDelivered(stop.id)\">\n                      Confirm POD\n                    </button>\n                  }\n                  @if (stop.status === 'delivered') {\n                    <span class=\"text-muted-foreground text-xs\">Logged</span>\n                  }\n                  @if (stop.status === 'scheduled') {\n                    <span class=\"text-muted-foreground text-xs\">Queued</span>\n                  }\n                </td>\n              </tr>\n            }\n          </tbody>\n        </div>\n      </div>\n\n      <div\n        ui-card-footer\n        class=\"border-border text-muted-foreground flex items-center justify-between border-t py-3 text-xs\"\n      >\n        <div>Geotagged proof of delivery records are synced automatically to customer tracking portals.</div>\n        <div class=\"text-foreground font-medium\">\n          Route summary: <span class=\"font-mono\">{{ totalStops() }}</span> total consignments\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiLastMileCourierDispatchComponent implements AfterViewInit, OnDestroy {\n  @Input() routeId = 'RTE-METRO-409'\n  @Input() driverName = 'Elena Rostova'\n  @Input() vehicleCode = 'VAN-EV-09 (Ford E-Transit)'\n  @Input() accessToken?: string\n  @Input('class') className?: string\n\n  @ViewChildren('stopMarker') private stopMarkerEls?: QueryList<ElementRef<HTMLDivElement>>\n  @ViewChild('vanMarker') private vanMarkerEl?: ElementRef<HTMLDivElement>\n\n  protected readonly BatteryCharging = BatteryCharging\n  protected readonly Navigation = Navigation\n  protected readonly Phone = Phone\n  protected readonly Radio = Radio\n  protected readonly Truck = Truck\n\n  readonly stops = signal<DeliveryStop[]>(DEFAULT_DELIVERY_STOPS.map((s) => ({ ...s })))\n\n  readonly completedCount = computed(() => this.stops().filter((s) => s.status === 'delivered').length)\n  readonly totalStops = computed(() => this.stops().length)\n  readonly completionPercent = computed(() => Math.round((this.completedCount() / this.totalStops()) * 100))\n  readonly onTimeRate = '98.4%'\n\n  /** The route polyline (the React block's MapSource) through every stop, in drop order. */\n  readonly routeSources = computed<MapSourceInput[]>(() => [\n    {\n      id: 'courier-route-source',\n      type: 'geojson',\n      data: {\n        type: 'Feature',\n        properties: {},\n        geometry: {\n          type: 'LineString',\n          coordinates: this.stops()\n            .map((s) => s.coords)\n            .filter((c): c is [number, number] => Boolean(c)),\n        },\n      },\n    },\n  ])\n\n  /** The dashed route line (the React block's MapLayer inside the MapSource). */\n  readonly routeLayers: MapLayerInput[] = [\n    { id: 'courier-route-line', type: 'line', source: 'courier-route-source', paint: courierRoutePaint },\n  ]\n\n  private map: mapboxgl.Map | null = null\n  private markers: mapboxgl.Marker[] = []\n  private destroyed = false\n\n  get hostClass(): string {\n    return cn('block w-full space-y-6', this.className)\n  }\n\n  ngAfterViewInit(): void {\n    // A new stops list re-renders the marker elements; put the new set on the map.\n    this.stopMarkerEls?.changes.subscribe(() => void this.syncMarkers())\n  }\n\n  ngOnDestroy(): void {\n    this.destroyed = true\n    this.clearMarkers()\n    this.map = null\n  }\n\n  stopMarkerClass(stop: DeliveryStop): string {\n    return cn(\n      'border-background flex size-6 items-center justify-center rounded-full border-2 font-mono text-xs font-bold shadow-md',\n      stop.status === 'delivered'\n        ? 'bg-success text-white'\n        : stop.status === 'en_route'\n          ? 'bg-warning text-white ring-2 ring-amber-400/50'\n          : 'bg-muted text-muted-foreground',\n    )\n  }\n\n  statusVariant(status: DeliveryStop['status']): BadgeVariant {\n    return status === 'delivered' ? 'default' : status === 'en_route' ? 'secondary' : 'outline'\n  }\n\n  statusLabel(status: DeliveryStop['status']): string {\n    return status.replace('_', ' ')\n  }\n\n  firstWord(text: string): string {\n    return text.split(' ')[0] ?? ''\n  }\n\n  /** Log POD for the stop and move the next scheduled stop after it en route. */\n  markDelivered(id: string): void {\n    this.stops.update((prev) => {\n      let foundTarget = false\n      return prev.map((stop) => {\n        if (stop.id === id) {\n          foundTarget = true\n          return {\n            ...stop,\n            status: 'delivered' as const,\n            podType: 'signature' as const,\n            podTimestamp: 'Just now (Signed)',\n          }\n        }\n        if (foundTarget && stop.status === 'scheduled') {\n          foundTarget = false\n          return { ...stop, status: 'en_route' as const }\n        }\n        return stop\n      })\n    })\n  }\n\n  onMapCreated(map: mapboxgl.Map): void {\n    this.map = map\n    void this.syncMarkers()\n  }\n\n  /** One mapbox-gl Marker per stop plus the van, anchored at their centre (React MapMarker anchor=\"center\"). */\n  private async syncMarkers(): Promise<void> {\n    const map = this.map\n    if (!map) return\n    const stopEls = this.stopMarkerEls?.toArray() ?? []\n    const vanEl = this.vanMarkerEl?.nativeElement\n    const mod = await import('mapbox-gl')\n    const mapbox = mod.default ?? mod\n    if (this.destroyed || map !== this.map) return\n    this.clearMarkers()\n    const stops = this.stops()\n    this.markers = stopEls.flatMap((el, i) => {\n      const coords = stops[i]?.coords\n      if (!coords) return []\n      return [new mapbox.Marker({ element: el.nativeElement, anchor: 'center' }).setLngLat(coords).addTo(map)]\n    })\n    if (vanEl) {\n      this.markers.push(\n        new mapbox.Marker({ element: vanEl, anchor: 'center' }).setLngLat(COURIER_VAN_LNG_LAT).addTo(map),\n      )\n    }\n  }\n\n  private clearMarkers(): void {\n    for (const m of this.markers) m.remove()\n    this.markers = []\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/last-mile-courier-dispatch/last-mile-courier-dispatch.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/last-mile-courier-dispatch/index.ts",
      "content": "export {\n  UiLastMileCourierDispatchComponent,\n  DEFAULT_DELIVERY_STOPS,\n  COURIER_VAN_LNG_LAT,\n  type DeliveryStop,\n  type LastMileDispatchProps,\n} from './last-mile-courier-dispatch.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/last-mile-courier-dispatch/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular",
    "mapbox-gl"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/table.json",
    "https://uipkge.dev/r/angular/map.json"
  ],
  "description": "Last-mile courier dispatch matrix with real-time route progress, ETA telemetry, and Proof of Delivery (POD) logging.",
  "categories": [
    "dashboard",
    "data-display"
  ]
}