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