Menu

Change language

GitHub repo

Wave Picking Console

blockdashboard

Batch wave picking console with multi-order tote assignment, optimized pick route, and barcode verification.

Installation

$npx uipkge-ng@latest add wave-picking-console
Manifest:https://uipkge.dev/r/angular/wave-picking-console.jsonInstalls to:src/app/components/blocks/wave-picking-console/

Variants

Loading interactive previews…

Schema

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

PickItem
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'
}

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 &amp; 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

Raw manifest:https://uipkge.dev/r/angular/wave-picking-console.json