Menu

Change language

GitHub repo

Warehouse Inventory Grid

blocklogistics

WMS warehouse inventory and bin locator grid: facility selector, 4 summary metric cards for SKUs, capacity, low-stock triggers, and inbound shipments, searchable inventory table with multi-rack bin badges, on-hand/allocated/ATP counts, barcode scanning support, and slide-over bin relocation and receiving drawer.

Installation

$npx uipkge-ng@latest add warehouse-inventory-grid
Manifest:https://uipkge.dev/r/angular/warehouse-inventory-grid.jsonInstalls to:src/app/components/blocks/warehouse-inventory-grid/

Variants

Loading interactive previews…

Schema

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

WarehouseItem
interface WarehouseItem {
  id: string
  sku: string
  barcode: string
  name: string
  category: string
  palletId: string
  zone: string
  binLocation: string
  aisle: string
  rack: string
  shelf: string
  onHand: number
  allocated: number
  available: number
  reorderThreshold: number
  unitVolume: string
  status: StockStatus
  lastCycleCount: string
}

Files installed (2)

  • src/app/components/blocks/warehouse-inventory-grid/warehouse-inventory-grid.component.ts42.4 kB
    // WMS warehouse inventory and bin locator grid: facility selector, 4 summary metric
    // cards, searchable inventory table with multi-rack bin badges, on-hand/allocated/ATP
    // counts, barcode scanning support, and a slide-over bin relocation/receiving drawer.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      AlertTriangle,
      ArrowDownToLine,
      ArrowRightLeft,
      ArrowUpRight,
      Barcode,
      Boxes,
      Building2,
      Check,
      CheckCircle2,
      ClipboardCheck,
      LucideAngularModule,
      MoreHorizontal,
      Package,
      PackagePlus,
      Printer,
      QrCode,
      Search,
      Tag,
      Truck,
      Warehouse,
      X,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiSheetCloseComponent,
      UiSheetComponent,
      UiSheetContentComponent,
      UiSheetDescriptionComponent,
      UiSheetFooterComponent,
      UiSheetHeaderComponent,
      UiSheetTitleComponent,
    } from '@/ui/sheet/sheet.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type StockStatus = 'in_stock' | 'low_stock'
    
    export interface WarehouseItem {
      id: string
      sku: string
      barcode: string
      name: string
      category: string
      palletId: string
      zone: string
      binLocation: string
      aisle: string
      rack: string
      shelf: string
      onHand: number
      allocated: number
      available: number
      reorderThreshold: number
      unitVolume: string
      status: StockStatus
      lastCycleCount: string
    }
    
    const warehouseItems: WarehouseItem[] = [
      {
        id: 'wh-1',
        sku: '#SKU-84920',
        barcode: 'UPC-018249',
        name: 'Aero Runner 10.5 Matte Black',
        category: 'Footwear & Apparel',
        palletId: 'PLT-84920-04',
        zone: 'Zone A-West (High-Velocity)',
        binLocation: 'Aisle 04 · Rack B · Shelf 02',
        aisle: 'Aisle 04',
        rack: 'Rack B',
        shelf: 'Shelf 02',
        onHand: 450,
        allocated: 85,
        available: 365,
        reorderThreshold: 100,
        unitVolume: '0.04 m³',
        status: 'in_stock',
        lastCycleCount: '2 days ago',
      },
      {
        id: 'wh-2',
        sku: '#SKU-84921',
        barcode: 'UPC-029481',
        name: 'Technical Shell Jacket L',
        category: 'Outerwear',
        palletId: 'PLT-84921-02',
        zone: 'Zone A-North (Apparel Pick)',
        binLocation: 'Aisle 02 · Rack D · Shelf 01',
        aisle: 'Aisle 02',
        rack: 'Rack D',
        shelf: 'Shelf 01',
        onHand: 38,
        allocated: 30,
        available: 8,
        reorderThreshold: 50,
        unitVolume: '0.02 m³',
        status: 'low_stock',
        lastCycleCount: 'Yesterday',
      },
      {
        id: 'wh-3',
        sku: '#SKU-77210',
        barcode: 'UPC-093812',
        name: 'Heavy Duty Pallet Straps (50m)',
        category: 'Packaging & Materials',
        palletId: 'PLT-77210-09',
        zone: 'Zone B-Bulk (Industrial)',
        binLocation: 'Aisle 07 · Rack A · Shelf 04',
        aisle: 'Aisle 07',
        rack: 'Rack A',
        shelf: 'Shelf 04',
        onHand: 820,
        allocated: 140,
        available: 680,
        reorderThreshold: 200,
        unitVolume: '0.08 m³',
        status: 'in_stock',
        lastCycleCount: '5 days ago',
      },
      {
        id: 'wh-4',
        sku: '#SKU-99341',
        barcode: 'UPC-048201',
        name: 'Thermal Packaging Liner 24x24',
        category: 'Cold Chain Supplies',
        palletId: 'PLT-99341-01',
        zone: 'Zone C-Cold (Temp Controlled)',
        binLocation: 'Aisle 01 · Rack C · Shelf 03',
        aisle: 'Aisle 01',
        rack: 'Rack C',
        shelf: 'Shelf 03',
        onHand: 24,
        allocated: 18,
        available: 6,
        reorderThreshold: 60,
        unitVolume: '0.05 m³',
        status: 'low_stock',
        lastCycleCount: 'Today',
      },
      {
        id: 'wh-5',
        sku: '#SKU-65239',
        barcode: 'UPC-067194',
        name: 'Industrial Barcode Scanner BT',
        category: 'WMS Equipment',
        palletId: 'PLT-65239-03',
        zone: 'Zone D-Secure (High-Value)',
        binLocation: 'Aisle 09 · Rack A · Shelf 01',
        aisle: 'Aisle 09',
        rack: 'Rack A',
        shelf: 'Shelf 01',
        onHand: 115,
        allocated: 15,
        available: 100,
        reorderThreshold: 30,
        unitVolume: '0.01 m³',
        status: 'in_stock',
        lastCycleCount: '3 days ago',
      },
      {
        id: 'wh-6',
        sku: '#SKU-54018',
        barcode: 'UPC-081924',
        name: 'Anti-Static Bubble Wrap Roll',
        category: 'Packaging & Materials',
        palletId: 'PLT-54018-05',
        zone: 'Zone B-Bulk (Packaging Bay)',
        binLocation: 'Aisle 06 · Rack E · Shelf 02',
        aisle: 'Aisle 06',
        rack: 'Rack E',
        shelf: 'Shelf 02',
        onHand: 190,
        allocated: 45,
        available: 145,
        reorderThreshold: 75,
        unitVolume: '0.12 m³',
        status: 'in_stock',
        lastCycleCount: '1 week ago',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-warehouse-inventory-grid, [ui-warehouse-inventory-grid]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
        UiInputComponent,
        UiProgressComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSeparatorComponent,
        UiSheetCloseComponent,
        UiSheetComponent,
        UiSheetContentComponent,
        UiSheetDescriptionComponent,
        UiSheetFooterComponent,
        UiSheetHeaderComponent,
        UiSheetTitleComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="warehouse-inventory-grid" [class]="rootClass">
          <!-- Header Section -->
          <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <div class="space-y-1">
              <div class="flex flex-wrap items-center gap-2.5">
                <div class="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-lg">
                  <lucide-icon [img]="Warehouse" class="size-4.5 shrink-0" />
                </div>
                <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
                  Warehouse Inventory & Bin Locations
                </h1>
                <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1.5">
                  <span class="bg-success size-1.5 shrink-0 animate-pulse rounded-full"></span>
                  WMS Live
                </span>
              </div>
              <p class="text-muted-foreground text-xs sm:text-sm">
                Real-time multi-bin locator, safety stock reorder thresholds, and pallet relocation manifests.
              </p>
            </div>
    
            <div class="flex flex-wrap items-center gap-2.5">
              <!-- Warehouse Facility Select -->
              <ui-select [value]="facility()" (valueChange)="facility.set($event)">
                <button ui-select-trigger class="w-full sm:w-[320px]">
                  <lucide-icon [img]="Building2" class="text-muted-foreground size-4 shrink-0" />
                  <ui-select-value placeholder="Select warehouse facility" class="min-w-0 truncate" />
                </button>
                <ui-select-content>
                  <ui-select-item value="bay-area">Bay Area Fulfillment Center #04 · Aisle A-D</ui-select-item>
                  <ui-select-item value="seattle">Seattle Logistics Hub #02 · High-Bay B</ui-select-item>
                  <ui-select-item value="dallas">Dallas Central Depot #01 · Mezzanine C</ui-select-item>
                  <ui-select-item value="newark">Newark Cold Chain #03 · Zone Free-01</ui-select-item>
                </ui-select-content>
              </ui-select>
    
              <button ui-button variant="outline" class="gap-1.5" (click)="openTransferDrawer()">
                <lucide-icon [img]="ArrowRightLeft" class="size-4 shrink-0" />
                Relocate Bin
              </button>
    
              <button ui-button class="gap-1.5 shadow-xs" (click)="openReceiveDrawer()">
                <lucide-icon [img]="PackagePlus" class="size-4 shrink-0" />
                Receive Inventory
              </button>
            </div>
          </div>
    
          <!-- Notification / Action Feedback Banner -->
          @if (actionFeedback()) {
            <div
              class="border-primary/20 bg-primary/5 text-foreground flex items-center justify-between rounded-lg border px-4 py-3 text-sm shadow-xs transition-colors"
            >
              <div class="flex items-center gap-2.5">
                <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" />
                <span class="text-xs font-medium sm:text-sm">{{ actionFeedback() }}</span>
              </div>
              <button
                ui-button
                variant="ghost"
                size="xs"
                class="h-6 w-6 shrink-0 p-0"
                aria-label="Dismiss notification"
                (click)="actionFeedback.set(null)"
              >
                <lucide-icon [img]="X" class="size-3.5 shrink-0" />
              </button>
            </div>
          }
    
          <!-- 4 WMS Inventory Metric Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <!-- Metric 1: Total SKUs -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Total SKUs in Stock
                </h3>
                <div class="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-md">
                  <lucide-icon [img]="Package" class="size-4 shrink-0" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">
                  1,842 <span class="text-muted-foreground text-sm font-normal">SKUs</span>
                </div>
                <p class="text-muted-foreground flex items-center gap-1.5 pt-1 text-xs">
                  <span class="text-success flex items-center font-medium">
                    <lucide-icon [img]="ArrowUpRight" class="mr-0.5 inline size-3 shrink-0" />+48 SKUs
                  </span>
                  <span>· 99.4% count accuracy</span>
                </p>
              </div>
            </div>
    
            <!-- Metric 2: Warehouse Capacity -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Warehouse Capacity
                </h3>
                <div class="bg-info/10 text-info flex size-8 shrink-0 items-center justify-center rounded-md">
                  <lucide-icon [img]="Boxes" class="size-4 shrink-0" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                  <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">84.5%</div>
                  <span class="text-muted-foreground text-xs tabular-nums">12,400 / 14,800 pallets</span>
                </div>
                <div ui-progress [value]="84.5" class="h-1.5"></div>
                <p class="text-muted-foreground text-xs">2,400 available slots in High-Bay & Bulk</p>
              </div>
            </div>
    
            <!-- Metric 3: Low Stock Alerts -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Low Stock Alerts
                </h3>
                <div class="bg-warning/10 text-warning flex size-8 shrink-0 items-center justify-center rounded-md">
                  <lucide-icon [img]="AlertTriangle" class="size-4 shrink-0" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="flex items-center gap-2">
                  <div class="text-warning text-2xl font-bold tracking-tight tabular-nums">12</div>
                  <span ui-badge variant="warning" class="text-xs font-normal"> Action required </span>
                </div>
                <p class="text-muted-foreground pt-1 text-xs">Below safety stock threshold · 4 POs in transit</p>
              </div>
            </div>
    
            <!-- Metric 4: Inbound Shipments Today -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Inbound Shipments Today
                </h3>
                <div class="bg-success/10 text-success flex size-8 shrink-0 items-center justify-center rounded-md">
                  <lucide-icon [img]="Truck" class="size-4 shrink-0" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">
                  4 <span class="text-muted-foreground text-sm font-normal">POs arriving</span>
                </div>
                <p class="text-muted-foreground pt-1 text-xs">2,450 units expected by 16:00 · Bay 02 & 05</p>
              </div>
            </div>
          </div>
    
          <!-- Inventory Filter & Search Toolbar -->
          <div ui-card class="shadow-xs">
            <div ui-card-header class="border-border border-b pb-3">
              <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <div class="flex flex-1 flex-wrap items-center gap-2">
                  <div class="relative w-full min-w-0 md:max-w-sm">
                    <lucide-icon
                      [img]="Search"
                      class="text-muted-foreground absolute top-1/2 left-3 size-4 shrink-0 -translate-y-1/2"
                    />
                    <ui-input
                      [value]="searchQuery()"
                      (valueChange)="searchQuery.set($event)"
                      placeholder="Search SKU or product"
                      class="h-9 pl-9 text-xs sm:text-sm"
                    />
                  </div>
    
                  <!-- Category Filter -->
                  <ui-select [value]="categoryFilter()" (valueChange)="categoryFilter.set($event)">
                    <button ui-select-trigger class="h-9 w-[160px] text-xs">
                      <ui-select-value placeholder="All Categories" />
                    </button>
                    <ui-select-content>
                      <ui-select-item value="all">All Categories</ui-select-item>
                      <ui-select-item value="Footwear & Apparel">Footwear & Apparel</ui-select-item>
                      <ui-select-item value="Outerwear">Outerwear</ui-select-item>
                      <ui-select-item value="Packaging & Materials">Packaging & Materials</ui-select-item>
                      <ui-select-item value="Cold Chain Supplies">Cold Chain Supplies</ui-select-item>
                      <ui-select-item value="WMS Equipment">WMS Equipment</ui-select-item>
                    </ui-select-content>
                  </ui-select>
    
                  <!-- Status Filter -->
                  <ui-select [value]="statusFilter()" (valueChange)="statusFilter.set($event)">
                    <button ui-select-trigger class="h-9 w-[140px] text-xs">
                      <ui-select-value placeholder="All Stock Status" />
                    </button>
                    <ui-select-content>
                      <ui-select-item value="all">All Statuses</ui-select-item>
                      <ui-select-item value="in_stock">In Stock</ui-select-item>
                      <ui-select-item value="low_stock">Low Stock (Reorder)</ui-select-item>
                    </ui-select-content>
                  </ui-select>
                </div>
    
                <div class="text-muted-foreground flex items-center gap-2 text-xs">
                  <span
                    >Showing <strong class="text-foreground tabular-nums">{{ filteredItems().length }}</strong> of
                    {{ itemCount }} inventory items</span
                  >
                </div>
              </div>
            </div>
    
            <div ui-card-content class="p-0">
              <!-- Inventory Table -->
              <div class="overflow-x-auto">
                <table ui-table>
                  <thead ui-table-header>
                    <tr ui-table-row class="hover:bg-transparent">
                      <th ui-table-head class="w-[180px] text-xs font-semibold">SKU & Barcode</th>
                      <th ui-table-head class="min-w-[220px] text-xs font-semibold">Item & Category</th>
                      <th ui-table-head class="min-w-[210px] text-xs font-semibold">Bin / Rack Location</th>
                      <th ui-table-head class="text-right text-xs font-semibold">On Hand</th>
                      <th ui-table-head class="text-right text-xs font-semibold">Allocated</th>
                      <th ui-table-head class="text-right text-xs font-semibold">Available (ATP)</th>
                      <th ui-table-head class="min-w-[160px] text-xs font-semibold">Reorder & Status</th>
                      <th ui-table-head class="w-[60px] text-right text-xs font-semibold">Actions</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (item of filteredItems(); track item.id) {
                      <tr ui-table-row class="hover:bg-muted/40 transition-colors">
                        <!-- SKU & Barcode -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-0.5">
                            <div class="text-foreground font-mono text-xs font-semibold">
                              {{ item.sku }}
                            </div>
                            <div class="text-muted-foreground flex items-center gap-1 font-mono text-xs">
                              <lucide-icon [img]="Barcode" class="text-muted-foreground/70 size-3 shrink-0" />
                              <span>{{ item.barcode }}</span>
                            </div>
                          </div>
                        </td>
    
                        <!-- Item Name & Category -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-0.5">
                            <div class="text-foreground text-sm font-medium">
                              {{ item.name }}
                            </div>
                            <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                              <span>{{ item.category }}</span>
                              <span>·</span>
                              <span class="font-mono text-xs">{{ item.palletId }}</span>
                            </div>
                          </div>
                        </td>
    
                        <!-- Bin / Rack Location Badge -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-1">
                            <span ui-badge variant="outline" class="bg-muted/30 gap-1.5 font-mono text-xs font-medium">
                              <lucide-icon [img]="Tag" class="text-primary size-3 shrink-0" />
                              {{ item.binLocation }}
                            </span>
                            <div class="text-muted-foreground text-xs">
                              {{ item.zone }}
                            </div>
                          </div>
                        </td>
    
                        <!-- On Hand -->
                        <td ui-table-cell class="py-3 text-right">
                          <div class="text-foreground text-sm font-semibold tabular-nums">
                            {{ item.onHand.toLocaleString() }}
                            <span class="text-muted-foreground text-xs font-normal">units</span>
                          </div>
                          <div class="text-muted-foreground text-xs tabular-nums">
                            {{ item.unitVolume }}
                          </div>
                        </td>
    
                        <!-- Allocated -->
                        <td ui-table-cell class="py-3 text-right">
                          <div class="text-muted-foreground text-sm font-medium tabular-nums">
                            {{ item.allocated.toLocaleString() }}
                            <span class="text-muted-foreground text-xs font-normal">units</span>
                          </div>
                          <div class="text-muted-foreground text-xs tabular-nums">{{ reservedPct(item) }}% reserved</div>
                        </td>
    
                        <!-- Available (ATP) -->
                        <td ui-table-cell class="py-3 text-right">
                          <div class="text-foreground text-sm font-bold tabular-nums">
                            {{ item.available.toLocaleString() }}
                            <span class="text-muted-foreground text-xs font-normal">units</span>
                          </div>
                          <div class="text-success text-xs font-medium">Ready to pick</div>
                        </td>
    
                        <!-- Reorder Threshold & Status -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-1">
                            @if (item.status === 'in_stock') {
                              <span ui-badge variant="success" class="gap-1 text-xs whitespace-normal">
                                <lucide-icon [img]="CheckCircle2" class="size-3 shrink-0" />
                                In Stock
                              </span>
                            } @else {
                              <span ui-badge variant="warning" class="gap-1 text-xs">
                                <lucide-icon [img]="AlertTriangle" class="size-3 shrink-0" />
                                Low Stock - Reorder
                              </span>
                            }
                            <div class="text-muted-foreground text-xs tabular-nums">
                              Min safety: {{ item.reorderThreshold }} units
                            </div>
                          </div>
                        </td>
    
                        <!-- Actions Menu -->
                        <td ui-table-cell class="py-3 text-right">
                          <ui-dropdown-menu>
                            <button
                              ui-button
                              ui-dropdown-menu-trigger
                              variant="ghost"
                              size="icon-sm"
                              class="size-8 shrink-0"
                            >
                              <lucide-icon [img]="MoreHorizontal" class="size-4 shrink-0" />
                              <span class="sr-only">Open menu</span>
                            </button>
                            <ui-dropdown-menu-content align="end" class="w-48">
                              <ui-dropdown-menu-label class="text-muted-foreground text-xs"
                                >Bin Actions</ui-dropdown-menu-label
                              >
                              <ui-dropdown-menu-item class="cursor-pointer gap-2" (select)="openTransferDrawer(item)">
                                <lucide-icon [img]="ArrowRightLeft" class="text-muted-foreground size-4 shrink-0" />
                                Transfer / Relocate Bin
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item class="cursor-pointer gap-2" (select)="handlePrintBarcode(item)">
                                <lucide-icon [img]="Printer" class="text-muted-foreground size-4 shrink-0" />
                                Print Barcode Label
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item class="cursor-pointer gap-2" (select)="handleCycleCount(item)">
                                <lucide-icon [img]="ClipboardCheck" class="text-muted-foreground size-4 shrink-0" />
                                Log Cycle Count
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-separator />
                              <ui-dropdown-menu-item
                                class="text-warning cursor-pointer gap-2"
                                (select)="openReceiveDrawer()"
                              >
                                <lucide-icon [img]="PackagePlus" class="size-4 shrink-0" />
                                Receive Restock Batch
                              </ui-dropdown-menu-item>
                            </ui-dropdown-menu-content>
                          </ui-dropdown-menu>
                        </td>
                      </tr>
                    }
    
                    @if (filteredItems().length === 0) {
                      <tr ui-table-row>
                        <td ui-table-cell [attr.colspan]="8" class="text-muted-foreground h-32 text-center text-sm">
                          No warehouse inventory items found matching your criteria.
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
    
          <!-- Quick Transfer / Relocate Bin Drawer (Sheet) -->
          <div ui-sheet [open]="drawerOpen()" (openChange)="drawerOpen.set($event)">
            <div ui-sheet-content side="right" class="w-full space-y-6 overflow-y-auto p-6 sm:max-w-md">
              <!-- Drawer Header -->
              <div ui-sheet-header class="space-y-1 text-left">
                <div class="flex items-center gap-2">
                  <div class="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-md">
                    @if (drawerMode() === 'transfer') {
                      <lucide-icon [img]="ArrowRightLeft" class="size-4 shrink-0" />
                    } @else {
                      <lucide-icon [img]="PackagePlus" class="size-4 shrink-0" />
                    }
                  </div>
                  <h2 ui-sheet-title class="text-lg font-bold">
                    {{ drawerMode() === 'transfer' ? 'Relocate Bin / Pallet Transfer' : 'Receive Inventory Inbound' }}
                  </h2>
                </div>
                <p ui-sheet-description class="text-muted-foreground text-xs">
                  {{
                    drawerMode() === 'transfer'
                      ? 'Relocate inventory pallets between rack coordinates and generate WMS transfer manifests.'
                      : 'Process inbound freight shipments, inspect UPC barcodes, and assign initial staging bins.'
                  }}
                </p>
              </div>
    
              <!-- Mode: Transfer Bin -->
              @if (drawerMode() === 'transfer') {
                @if (selectedItem(); as item) {
                  <!-- Item Card Summary -->
                  <div class="border-border bg-muted/20 space-y-3 rounded-lg border p-4">
                    <div class="flex items-start justify-between gap-2">
                      <div>
                        <div class="text-primary font-mono text-xs font-semibold">{{ item.sku }}</div>
                        <div class="text-foreground text-sm font-medium">{{ item.name }}</div>
                        <div class="text-muted-foreground text-xs">{{ item.category }}</div>
                      </div>
                      <span ui-badge variant="outline" class="font-mono text-xs">{{ item.palletId }}</span>
                    </div>
    
                    <ui-separator />
    
                    <div class="grid grid-cols-1 gap-2 text-xs sm:grid-cols-2">
                      <div>
                        <span class="text-muted-foreground">Current Bin:</span>
                        <p class="text-foreground mt-0.5 font-mono text-xs font-medium">{{ item.binLocation }}</p>
                      </div>
                      <div>
                        <span class="text-muted-foreground">Available to Move:</span>
                        <p class="text-success mt-0.5 font-bold tabular-nums">{{ item.available }} units</p>
                      </div>
                    </div>
                  </div>
    
                  <!-- Transfer Form Fields -->
                  <div class="space-y-4">
                    <div class="space-y-1.5">
                      <label class="text-foreground text-xs font-medium">Destination Rack / Bin Location</label>
                      <ui-select [value]="transferDestination()" (valueChange)="transferDestination.set($event)">
                        <button ui-select-trigger class="w-full text-xs">
                          <ui-select-value placeholder="Select target bin location" />
                        </button>
                        <ui-select-content>
                          <ui-select-item value="bin-dest-1"
                            >Aisle 01 · Rack A · Shelf 03 (Zone A-North · Standard Rack)</ui-select-item
                          >
                          <ui-select-item value="bin-dest-2"
                            >Aisle 03 · Rack B · Shelf 01 (Zone A-West · Fast-Pick)</ui-select-item
                          >
                          <ui-select-item value="bin-dest-3"
                            >Aisle 08 · Rack C · Shelf 04 (Zone B-Bulk · High-Bay)</ui-select-item
                          >
                          <ui-select-item value="bin-dest-4"
                            >Aisle 05 · Rack D · Shelf 02 (Zone C-Cold · Cold Bay)</ui-select-item
                          >
                        </ui-select-content>
                      </ui-select>
                    </div>
    
                    <div class="space-y-1.5">
                      <div class="flex items-center justify-between">
                        <label class="text-foreground text-xs font-medium">Transfer Quantity (Units)</label>
                        <span class="text-muted-foreground text-xs tabular-nums">Max: {{ item.available }} units</span>
                      </div>
                      <ui-input
                        [value]="transferQty()"
                        (valueChange)="setTransferQty($event)"
                        type="number"
                        [min]="1"
                        [max]="item.available"
                        class="text-xs tabular-nums sm:text-sm"
                      />
                      <div class="flex gap-1.5 pt-1">
                        <button
                          type="button"
                          ui-button
                          variant="outline"
                          size="xs"
                          class="flex-1 text-xs"
                          (click)="setQuickQty(25)"
                        >
                          25
                        </button>
                        <button
                          type="button"
                          ui-button
                          variant="outline"
                          size="xs"
                          class="flex-1 text-xs"
                          (click)="setQuickQty(50)"
                        >
                          50
                        </button>
                        <button
                          type="button"
                          ui-button
                          variant="outline"
                          size="xs"
                          class="flex-1 text-xs"
                          (click)="setQuickQty(100)"
                        >
                          100
                        </button>
                        <button
                          type="button"
                          ui-button
                          variant="outline"
                          size="xs"
                          class="flex-1 text-xs"
                          (click)="setQuickQty(item.available)"
                        >
                          All ({{ item.available }})
                        </button>
                      </div>
                    </div>
    
                    <div class="space-y-1.5">
                      <label class="text-foreground text-xs font-medium">Pallet ID / Batch Lot</label>
                      <ui-input
                        [value]="transferPalletId()"
                        (valueChange)="transferPalletId.set($event)"
                        class="font-mono text-xs"
                      />
                    </div>
    
                    <div class="space-y-1.5">
                      <label class="text-foreground text-xs font-medium">Transfer Reason</label>
                      <ui-select [value]="transferReason()" (valueChange)="transferReason.set($event)">
                        <button ui-select-trigger class="w-full text-xs">
                          <ui-select-value placeholder="Select transfer reason" />
                        </button>
                        <ui-select-content>
                          <ui-select-item value="reslotting">Zone Re-slotting (Velocity Optimization)</ui-select-item>
                          <ui-select-item value="consolidation">Consolidation & Space Reclamation</ui-select-item>
                          <ui-select-item value="staging">Fast-Pick Outbound Wave Staging</ui-select-item>
                          <ui-select-item value="rack-repair">Damaged Rack Maintenance Relocation</ui-select-item>
                          <ui-select-item value="quarantine">Quarantine & Quality Audit Transfer</ui-select-item>
                        </ui-select-content>
                      </ui-select>
                    </div>
    
                    <!-- Verified Scanner / Operator Badge -->
                    <div class="border-border bg-muted/40 space-y-1 rounded-md border p-3 text-xs">
                      <div class="text-muted-foreground flex items-center justify-between">
                        <span class="flex items-center gap-1.5">
                          <lucide-icon [img]="QrCode" class="text-primary size-3.5 shrink-0" />
                          WMS Scanner Terminal
                        </span>
                        <span class="text-foreground font-mono font-medium">TC57-OP44</span>
                      </div>
                      <p class="text-muted-foreground">Operator: Marcus Vance (Badge #OP-921)</p>
                    </div>
                  </div>
                }
              } @else {
                <!-- Mode: Receive Inbound PO -->
                <div class="space-y-4">
                  <div class="space-y-1.5">
                    <label class="text-foreground text-xs font-medium">Inbound Purchase Order</label>
                    <ui-select [value]="receivePo()" (valueChange)="receivePo.set($event)">
                      <button ui-select-trigger class="w-full text-xs">
                        <ui-select-value placeholder="Select purchase order" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="po-8910">PO-2026-8910 · Acme Footwear Corp (500 units)</ui-select-item>
                        <ui-select-item value="po-8914">PO-2026-8914 · Pacific Packaging Ltd (1,200 units)</ui-select-item>
                        <ui-select-item value="po-8919">PO-2026-8919 · Thermal Supply Chain Co (750 units)</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
    
                  <div class="space-y-1.5">
                    <label class="text-foreground text-xs font-medium">Target SKU / Barcode</label>
                    <ui-input
                      [value]="receiveSku()"
                      (valueChange)="receiveSku.set($event)"
                      placeholder="#SKU-84920 or UPC code"
                      class="font-mono text-xs"
                    />
                  </div>
    
                  <div class="space-y-1.5">
                    <label class="text-foreground text-xs font-medium">Received Quantity (Units)</label>
                    <ui-input
                      [value]="receiveQty()"
                      (valueChange)="setReceiveQty($event)"
                      type="number"
                      class="text-xs tabular-nums"
                    />
                  </div>
    
                  <div class="space-y-1.5">
                    <label class="text-foreground text-xs font-medium">Assigned Inbound Staging Bay</label>
                    <ui-select [value]="receiveBin()" (valueChange)="receiveBin.set($event)">
                      <button ui-select-trigger class="w-full text-xs">
                        <ui-select-value placeholder="Select staging bin" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="bay-02">Staging Bay Inbound-02 (Aisle 00 · Cross-Dock)</ui-select-item>
                        <ui-select-item value="bay-05">Staging Bay Inbound-05 (Cold Holding)</ui-select-item>
                        <ui-select-item value="bay-08">Bulk Floor Staging Area B</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
    
                  <div class="text-foreground border-success/20 bg-success/5 space-y-1 rounded-md border p-3 text-xs">
                    <div class="text-success flex items-center gap-1.5 font-medium">
                      <lucide-icon [img]="Check" class="size-3.5 shrink-0" />
                      Inbound Freight Scan Ready
                    </div>
                    <p class="text-muted-foreground">
                      Pallet tags will be generated and printed automatically upon receipt confirmation.
                    </p>
                  </div>
                </div>
              }
    
              <!-- Drawer Footer -->
              <div
                ui-sheet-footer
                class="border-border flex flex-col-reverse gap-2 border-t pt-4 sm:flex-row sm:justify-end"
              >
                <div ui-sheet-close>
                  <button ui-button variant="outline" class="w-full sm:w-auto">Cancel</button>
                </div>
                @if (drawerMode() === 'transfer') {
                  <button ui-button class="w-full gap-1.5 sm:w-auto" (click)="handleConfirmTransfer()">
                    <lucide-icon [img]="ArrowRightLeft" class="size-4 shrink-0" />
                    Confirm Bin Transfer
                  </button>
                } @else {
                  <button ui-button class="w-full gap-1.5 sm:w-auto" (click)="handleConfirmReceive()">
                    <lucide-icon [img]="ArrowDownToLine" class="size-4 shrink-0" />
                    Confirm Receipt
                  </button>
                }
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiWarehouseInventoryGridComponent {
      protected readonly AlertTriangle = AlertTriangle
      protected readonly ArrowDownToLine = ArrowDownToLine
      protected readonly ArrowRightLeft = ArrowRightLeft
      protected readonly ArrowUpRight = ArrowUpRight
      protected readonly Barcode = Barcode
      protected readonly Boxes = Boxes
      protected readonly Building2 = Building2
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly ClipboardCheck = ClipboardCheck
      protected readonly MoreHorizontal = MoreHorizontal
      protected readonly Package = Package
      protected readonly PackagePlus = PackagePlus
      protected readonly Printer = Printer
      protected readonly QrCode = QrCode
      protected readonly Search = Search
      protected readonly Tag = Tag
      protected readonly Truck = Truck
      protected readonly Warehouse = Warehouse
      protected readonly X = X
    
      @Input('class') className?: string
    
      readonly facility = signal('bay-area')
      readonly searchQuery = signal('')
      readonly categoryFilter = signal('all')
      readonly statusFilter = signal('all')
      readonly zoneFilter = signal('all')
    
      readonly drawerOpen = signal(false)
      readonly drawerMode = signal<'transfer' | 'receive'>('transfer')
      readonly selectedItem = signal<WarehouseItem | null>(null)
    
      readonly transferDestination = signal('bin-dest-1')
      readonly transferQty = signal(50)
      readonly transferPalletId = signal('PLT-84920-04')
      readonly transferReason = signal('reslotting')
    
      readonly receivePo = signal('po-8910')
      readonly receiveSku = signal('SKU-84920')
      readonly receiveQty = signal(250)
      readonly receiveBin = signal('bay-02')
    
      readonly actionFeedback = signal<string | null>(null)
      private feedbackTimer: ReturnType<typeof setTimeout> | undefined
    
      readonly itemCount = warehouseItems.length
    
      readonly filteredItems = computed(() => {
        const q = this.searchQuery().trim().toLowerCase()
        const category = this.categoryFilter()
        const status = this.statusFilter()
        const zone = this.zoneFilter()
        return warehouseItems.filter((item) => {
          const matchesSearch =
            !q ||
            item.name.toLowerCase().includes(q) ||
            item.sku.toLowerCase().includes(q) ||
            item.barcode.toLowerCase().includes(q) ||
            item.binLocation.toLowerCase().includes(q) ||
            item.category.toLowerCase().includes(q)
          const matchesCategory = category === 'all' || item.category === category
          const matchesStatus = status === 'all' || item.status === status
          const matchesZone = zone === 'all' || item.zone.toLowerCase().includes(zone.toLowerCase())
          return matchesSearch && matchesCategory && matchesStatus && matchesZone
        })
      })
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      reservedPct(item: WarehouseItem): number {
        return Math.round((item.allocated / item.onHand) * 100)
      }
    
      setTransferQty(v: string): void {
        const n = Number(v)
        this.transferQty.set(Number.isFinite(n) ? n : 0)
      }
    
      setReceiveQty(v: string): void {
        const n = Number(v)
        this.receiveQty.set(Number.isFinite(n) ? n : 0)
      }
    
      setQuickQty(qty: number): void {
        const item = this.selectedItem()
        this.transferQty.set(item ? Math.min(qty, item.available) : qty)
      }
    
      openTransferDrawer(item?: WarehouseItem): void {
        const target = item ?? warehouseItems[0]!
        this.selectedItem.set(target)
        this.transferPalletId.set(target.palletId)
        this.transferQty.set(Math.min(50, target.available))
        this.drawerMode.set('transfer')
        this.drawerOpen.set(true)
      }
    
      openReceiveDrawer(): void {
        this.drawerMode.set('receive')
        this.drawerOpen.set(true)
      }
    
      private flashFeedback(message: string, ms: number): void {
        this.actionFeedback.set(message)
        clearTimeout(this.feedbackTimer)
        this.feedbackTimer = setTimeout(() => this.actionFeedback.set(null), ms)
      }
    
      handleConfirmTransfer(): void {
        const item = this.selectedItem()
        if (!item) return
        this.flashFeedback(`Successfully relocated ${this.transferQty()} units of ${item.sku} to destination bin.`, 4000)
        this.drawerOpen.set(false)
      }
    
      handleConfirmReceive(): void {
        this.flashFeedback(
          `Received ${this.receiveQty()} units for ${this.receivePo().toUpperCase()} staged into Bay 02.`,
          4000,
        )
        this.drawerOpen.set(false)
      }
    
      handlePrintBarcode(item: WarehouseItem): void {
        this.flashFeedback(`Barcode label queued for ${item.sku} (${item.barcode}) on Zebra Printer #2.`, 3500)
      }
    
      handleCycleCount(item: WarehouseItem): void {
        this.flashFeedback(`Cycle count logged and verified for bin ${item.binLocation}.`, 3500)
      }
    }
    
  • src/app/components/blocks/warehouse-inventory-grid/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/warehouse-inventory-grid.json