Wave Picking Console
blockdashboardBatch wave picking console with multi-order tote assignment, optimized pick route, and barcode verification.
Installation
$pnpm dlx uipkge-ng@latest add wave-picking-console$npx uipkge-ng@latest add wave-picking-console$yarn dlx uipkge-ng@latest add wave-picking-console$bunx uipkge-ng@latest add wave-picking-consoleManifest:
https://uipkge.dev/r/angular/wave-picking-console.jsonInstalls to:src/app/components/blocks/wave-picking-console/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.
PickIteminterface PickItem {
id: string
sequence: number
location: string
sku: string
name: string
barcode: string
requiredQty: number
pickedQty: number
toteNumber: string
orderNumber: string
status: 'pending' | 'picking' | 'picked' | 'shortage'
}Files installed (2)
src/app/components/blocks/wave-picking-console/wave-picking-console.component.ts12.6 kB
import { Component, Input, signal, computed, ChangeDetectionStrategy } from '@angular/core' import { Barcode, Check, LucideAngularModule, MapPin } 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 { UiInputComponent } from '@/ui/input/input.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface PickItem { id: string sequence: number location: string sku: string name: string barcode: string requiredQty: number pickedQty: number toteNumber: string orderNumber: string status: 'pending' | 'picking' | 'picked' | 'shortage' } export interface WavePickingConsoleProps { waveId?: string zoneName?: string pickerName?: string className?: string } /** * Angular port of the React WavePickingConsole block: a wave header with pick progress and * the active pick target (barcode scan, confirm pick, flag shortage) above the optimized * pick route table. Confirming a pick adds one unit to the active item and moves on to the * next pending item when it is complete. Edit the sample pick items below. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-wave-picking-console, [ui-wave-picking-console]', standalone: true, host: { '[attr.data-slot]': '"wave-picking-console"', '[class]': 'hostClass', }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiInputComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <!-- Top Wave Header Card --> <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">{{ waveId }}</span> <span ui-badge variant="outline" class="font-mono text-xs">{{ zoneName }}</span> <span ui-badge [variant]="progressPercentage() === 100 ? 'default' : 'secondary'" >{{ progressPercentage() }}% Picked</span > </div> <h3 ui-card-title class="text-xl">Batch Order Wave Picking Console</h3> <p ui-card-description> Assigned Picker: <span class="text-foreground font-medium">{{ pickerName }}</span> · Route Optimized </p> </div> <div class="flex items-center gap-3"> <div class="text-right"> <div class="text-muted-foreground text-xs">Pick Completion</div> <div class="font-mono text-lg font-bold">{{ totalUnitsPicked() }} / {{ totalUnitsToPick() }} Units</div> </div> </div> </div> <!-- Active Pick Target Banner --> @if (activeItem(); as active) { <div ui-card-content class="border-border border-t pt-4"> <div class="border-border bg-muted/40 rounded-lg border p-4"> <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <span class="bg-foreground text-background rounded px-2 py-0.5 font-mono text-xs font-semibold" >NEXT TARGET: {{ active.location }}</span > <span class="text-muted-foreground font-mono text-xs">TOTE: {{ active.toteNumber }}</span> <span class="text-muted-foreground font-mono text-xs">({{ active.orderNumber }})</span> </div> <div class="text-foreground text-lg font-semibold">{{ active.name }}</div> <div class="text-muted-foreground font-mono text-xs"> SKU: {{ active.sku }} · Scan Barcode: {{ active.barcode }} </div> </div> <!-- Fast Scan & Action Button Group --> <div class="flex items-center gap-2"> <div class="relative w-48"> <lucide-icon [img]="Barcode" class="text-muted-foreground pointer-events-none absolute top-2.5 left-2.5 size-4" /> <ui-input [value]="activeBarcode()" (valueChange)="activeBarcode.set($event)" placeholder="Scan SKU barcode..." aria-label="Scan SKU barcode" class="h-9 pl-8" (keydown.enter)="scanItem()" /> </div> <button ui-button size="sm" class="gap-1.5" (click)="scanItem()"> <lucide-icon [img]="Check" class="size-4" /> Confirm Pick </button> <button ui-button variant="outline" size="sm" class="text-destructive hover:bg-destructive/10" (click)="flagShortage(active.id)" > Flag Shortage </button> </div> </div> </div> </div> } </div> <!-- Optimized Route Table --> <div ui-card> <div ui-card-header class="pb-3"> <h3 ui-card-title class="text-base font-semibold">Optimized Pick Route Sequence</h3> <p ui-card-description> Pick items strictly in sequence to minimize travel distance across fulfillment aisles. </p> </div> <div ui-card-content class="p-0"> <table ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head class="w-[60px]">Seq</th> <th ui-table-head>Location Bin</th> <th ui-table-head class="w-[300px]">Product / SKU</th> <th ui-table-head>Tote & Order</th> <th ui-table-head class="text-right">Qty</th> <th ui-table-head>Status</th> </tr> </thead> <tbody ui-table-body> @for (item of pickItems(); track item.id) { <tr ui-table-row [class]="item.status === 'picking' ? 'bg-muted/50 font-medium' : ''"> <!-- Sequence Number --> <td ui-table-cell class="text-muted-foreground font-mono text-xs">#{{ item.sequence }}</td> <!-- Location Bin --> <td ui-table-cell> <div class="text-foreground flex items-center gap-1.5 font-mono text-xs font-semibold"> <lucide-icon [img]="MapPin" class="text-muted-foreground size-3.5" /> <span>{{ item.location }}</span> </div> </td> <!-- Product --> <td ui-table-cell> <div class="font-medium">{{ item.name }}</div> <div class="text-muted-foreground font-mono text-xs">{{ item.sku }}</div> </td> <!-- Tote & Order --> <td ui-table-cell> <div class="flex items-center gap-1.5 font-mono text-xs"> <span ui-badge variant="outline">{{ item.toteNumber }}</span> <span class="text-muted-foreground">{{ item.orderNumber }}</span> </div> </td> <!-- Qty Progress --> <td ui-table-cell class="text-right font-mono text-sm"> {{ item.pickedQty }} / {{ item.requiredQty }} </td> <!-- Status Badge --> <td ui-table-cell> <span ui-badge [variant]="statusVariant(item.status)" class="capitalize">{{ item.status }}</span> </td> </tr> } </tbody> </table> </div> <div ui-card-footer class="border-border text-muted-foreground flex items-center justify-between border-t py-3 text-xs" > <div>Completed totes are automatically routed to Conveyor Pack Station 04.</div> <div class="text-foreground font-medium"> Wave Target: <span class="font-mono">{{ pickItems().length }}</span> pick positions </div> </div> </div> `, }) export class UiWavePickingConsoleComponent { @Input() waveId = 'WAVE-B2C-904' @Input() zoneName = 'Zone 02 · Fast Mover Aisle' @Input() pickerName = 'Alex Mercer (ID: PK-41)' @Input('class') className?: string protected readonly Barcode = Barcode protected readonly Check = Check protected readonly MapPin = MapPin readonly activeBarcode = signal('') readonly pickItems = signal<PickItem[]>([ { id: 'pk-1', sequence: 1, location: 'A02-S1-B04', sku: 'SKU-AUDIO-ANC', name: 'Active Noise Canceling Headphones Matte Black', barcode: '079357319901', requiredQty: 2, pickedQty: 2, toteNumber: 'TOTE-01', orderNumber: 'ORD-99120', status: 'picked', }, { id: 'pk-2', sequence: 2, location: 'A02-S3-B11', sku: 'SKU-CABLE-BRAID', name: 'Braided Type-C Thunderbolt 4 Cable (2m)', barcode: '079357319902', requiredQty: 4, pickedQty: 1, toteNumber: 'TOTE-02', orderNumber: 'ORD-99124', status: 'picking', }, { id: 'pk-3', sequence: 3, location: 'A03-S2-B08', sku: 'SKU-DESK-PAD', name: 'Top-Grain Leather Desk Mat Midnight Gray', barcode: '079357319903', requiredQty: 1, pickedQty: 0, toteNumber: 'TOTE-01', orderNumber: 'ORD-99120', status: 'pending', }, { id: 'pk-4', sequence: 4, location: 'A04-S1-B02', sku: 'SKU-HUB-10IN1', name: 'USB-C Aluminum Desktop Docking Station', barcode: '079357319904', requiredQty: 3, pickedQty: 0, toteNumber: 'TOTE-03', orderNumber: 'ORD-99131', status: 'pending', }, ]) readonly totalUnitsToPick = computed(() => this.pickItems().reduce((acc, i) => acc + i.requiredQty, 0)) readonly totalUnitsPicked = computed(() => this.pickItems().reduce((acc, i) => acc + i.pickedQty, 0)) readonly progressPercentage = computed(() => Math.round((this.totalUnitsPicked() / this.totalUnitsToPick()) * 100)) readonly activeItem = computed( () => this.pickItems().find((i) => i.status === 'picking') || this.pickItems().find((i) => i.status === 'pending'), ) get hostClass(): string { return cn('block w-full space-y-6', this.className) } statusVariant(status: PickItem['status']): BadgeVariant { return status === 'picked' ? 'default' : status === 'picking' ? 'secondary' : status === 'shortage' ? 'destructive' : 'outline' } scanItem(): void { const activeItem = this.activeItem() if (!activeItem) return this.pickItems.update((prev) => { let movedToNext = false return prev .map((item) => { if (item.id === activeItem.id && item.pickedQty < item.requiredQty) { const nextQty = item.pickedQty + 1 const isDone = nextQty === item.requiredQty if (isDone) movedToNext = true return { ...item, pickedQty: nextQty, status: (isDone ? 'picked' : 'picking') as PickItem['status'] } } return item }) .map((item, _idx, arr) => { if (movedToNext && item.status === 'pending') { // make the first pending active const firstPending = arr.find((x) => x.status === 'pending') if (item.id === firstPending?.id) return { ...item, status: 'picking' as const } } return item }) }) this.activeBarcode.set('') } flagShortage(id: string): void { this.pickItems.update((prev) => { let foundNext = false return prev.map((item) => { if (item.id === id) { foundNext = true return { ...item, status: 'shortage' as const } } if (foundNext && item.status === 'pending') { foundNext = false return { ...item, status: 'picking' as const } } return item }) }) } }src/app/components/blocks/wave-picking-console/index.ts0.1 kB
export { UiWavePickingConsoleComponent, type PickItem, type WavePickingConsoleProps, } from './wave-picking-console.component'
Raw manifest:https://uipkge.dev/r/angular/wave-picking-console.json