Stock Transfer Manager
blockdashboardInter-facility inventory transfer console with bin-level routing, barcode lookup, and condition classification.
Installation
$pnpm dlx uipkge-ng@latest add stock-transfer-manager$npx uipkge-ng@latest add stock-transfer-manager$yarn dlx uipkge-ng@latest add stock-transfer-manager$bunx uipkge-ng@latest add stock-transfer-managerManifest:
https://uipkge.dev/r/angular/stock-transfer-manager.jsonInstalls to:src/app/components/blocks/stock-transfer-manager/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.
TransferIteminterface TransferItem {
id: string
sku: string
name: string
barcode: string
lotNumber: string
sourceBin: string
targetBin: string
availableQty: number
transferQty: number
unit: string
condition: TransferItemCondition
}Files installed (2)
src/app/components/blocks/stock-transfer-manager/stock-transfer-manager.component.ts17 kB
import { Component, Input, OnInit, computed, signal, ChangeDetectionStrategy } from '@angular/core' import { ArrowRight, Barcode, CheckCircle2, LucideAngularModule, Plus, QrCode, Trash2, Truck, Warehouse, } 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 { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type TransferItemCondition = 'sellable' | 'quarantine' | 'damaged' export type StockTransferStatus = 'draft' | 'staged' | 'in_transit' | 'received' export interface TransferItem { id: string sku: string name: string barcode: string lotNumber: string sourceBin: string targetBin: string availableQty: number transferQty: number unit: string condition: TransferItemCondition } export interface StockTransferProps { transferId?: string initialStatus?: StockTransferStatus className?: string } /** * Angular port of the React StockTransferManager block: a transfer header card (id, status * badge, dispatch / receipt / reset action, origin / destination / shipping selects) above a * manifest table with bin routing, editable transfer quantities and item conditions. The * scan box adds one unit to the line whose barcode or SKU matches. Edit the sample * manifest below. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-stock-transfer-manager, [ui-stock-transfer-manager]', standalone: true, host: { '[attr.data-slot]': '"stock-transfer-manager"', '[class]': 'hostClass', }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <!-- Header Summary 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-muted-foreground font-mono text-sm font-medium">{{ transferId }}</span> <span ui-badge [variant]="statusVariant()" class="capitalize">{{ statusLabel() }}</span> </div> <h3 ui-card-title class="text-xl">Inter-Warehouse Stock Transfer</h3> <p ui-card-description> Transfer goods between fulfillment hubs with bin-level tracking and scan verification. </p> </div> <div class="flex items-center gap-2"> @if (status() === 'staged') { <button ui-button variant="default" class="gap-2" (click)="status.set('in_transit')"> <lucide-icon [img]="Truck" class="size-4" /> Dispatch Transfer </button> } @if (status() === 'in_transit') { <button ui-button variant="default" class="gap-2" (click)="status.set('received')"> <lucide-icon [img]="CheckCircle2" class="size-4" /> Confirm Receipt </button> } @if (status() === 'received') { <button ui-button variant="outline" class="gap-2" (click)="status.set('staged')">Reset Transfer</button> } </div> </div> <div ui-card-content class="border-border grid grid-cols-1 gap-4 border-t pt-4 sm:grid-cols-3"> <!-- Source Hub --> <div class="border-border space-y-2 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Warehouse" class="size-3.5" /> <span>ORIGIN FACILITY</span> </div> <ui-select [value]="sourceWarehouse()" (valueChange)="sourceWarehouse.set($event)" [disabled]="routingLocked()" > <button ui-select-trigger class="w-full"> <ui-select-value placeholder="Select origin warehouse" /> </button> <ui-select-content> <ui-select-item value="wh-east-01">East Coast Fulfillment (NJ-01)</ui-select-item> <ui-select-item value="wh-west-02">West Coast Distribution (CA-04)</ui-select-item> <ui-select-item value="wh-midwest-03">Midwest Logistics Hub (IL-02)</ui-select-item> </ui-select-content> </ui-select> </div> <!-- Destination Hub --> <div class="border-border space-y-2 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="ArrowRight" class="size-3.5" /> <span>DESTINATION FACILITY</span> </div> <ui-select [value]="targetWarehouse()" (valueChange)="targetWarehouse.set($event)" [disabled]="routingLocked()" > <button ui-select-trigger class="w-full"> <ui-select-value placeholder="Select target warehouse" /> </button> <ui-select-content> <ui-select-item value="wh-central-02">Central Distribution Center (TX-01)</ui-select-item> <ui-select-item value="wh-north-01">Northern Sorting Facility (WA-02)</ui-select-item> <ui-select-item value="wh-south-03">Southeast Regional Hub (GA-03)</ui-select-item> </ui-select-content> </ui-select> </div> <!-- Carrier / Transport --> <div class="border-border space-y-2 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Truck" class="size-3.5" /> <span>SHIPPING METHOD</span> </div> <ui-select [value]="transportType()" (valueChange)="transportType.set($event)" [disabled]="routingLocked()"> <button ui-select-trigger class="w-full"> <ui-select-value placeholder="Select transit mode" /> </button> <ui-select-content> <ui-select-item value="pallet_freight">Dedicated LTL Freight (Palletized)</ui-select-item> <ui-select-item value="express_courier">Priority Courier (Same Day Air)</ui-select-item> <ui-select-item value="internal_fleet">Internal Fleet Transfer (Route 09)</ui-select-item> </ui-select-content> </ui-select> </div> </div> </div> <!-- Line Items Table Card --> <div ui-card> <div ui-card-header class="flex flex-col gap-3 pb-3 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-base font-semibold">Manifest Line Items</h3> <p ui-card-description> {{ totalLineItems() }} unique SKUs · {{ totalUnits() }} total units staged for transfer </p> </div> <!-- Quick Scan / SKU lookup bar --> <div class="flex items-center gap-2"> <div class="relative w-64"> <lucide-icon [img]="Barcode" class="text-muted-foreground pointer-events-none absolute top-2.5 left-2.5 size-4" /> <ui-input [value]="barcodeQuery()" (valueChange)="barcodeQuery.set($event)" placeholder="Scan barcode or SKU..." class="pl-8" (keydown.enter)="handleScan()" /> </div> <button ui-button variant="secondary" size="icon" title="Scan Barcode" aria-label="Scan barcode" (click)="handleScan()" > <lucide-icon [img]="QrCode" class="size-4" /> </button> <button ui-button variant="outline" size="sm" class="gap-1" (click)="addItem()"> <lucide-icon [img]="Plus" class="size-3.5" /> Add Item </button> </div> </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-[280px]">Product / SKU</th> <th ui-table-head>Lot / Batch</th> <th ui-table-head>Source Bin</th> <th ui-table-head>Target Bin</th> <th ui-table-head class="text-right">Available</th> <th ui-table-head class="w-[120px] text-right">Transfer Qty</th> <th ui-table-head>Condition</th> <th ui-table-head class="w-[50px]"></th> </tr> </thead> <tbody ui-table-body> @for (item of items(); track item.id) { <tr ui-table-row> <!-- SKU + Name --> <td ui-table-cell> <div class="font-medium">{{ item.name }}</div> <div class="text-muted-foreground flex items-center gap-2 font-mono text-xs"> <span>{{ item.sku }}</span> <span>·</span> <span>{{ item.barcode }}</span> </div> </td> <!-- Lot Number --> <td ui-table-cell> <span class="font-mono text-xs">{{ item.lotNumber }}</span> </td> <!-- Source Bin --> <td ui-table-cell> <span class="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono text-xs font-medium">{{ item.sourceBin }}</span> </td> <!-- Target Bin --> <td ui-table-cell> <span class="border-border text-foreground rounded border px-1.5 py-0.5 font-mono text-xs font-medium" >{{ item.targetBin }}</span > </td> <!-- Available Qty --> <td ui-table-cell class="text-muted-foreground text-right font-mono text-xs"> {{ item.availableQty }} {{ item.unit }} </td> <!-- Transfer Qty Input --> <td ui-table-cell class="text-right"> <ui-input type="number" [min]="1" [max]="item.availableQty" [value]="item.transferQty" (valueChange)="updateItemQty(item.id, $event)" class="h-8 text-right font-mono" [disabled]="status() === 'received'" /> </td> <!-- Condition --> <td ui-table-cell> <ui-select [value]="item.condition" (valueChange)="updateItemCondition(item.id, $event)" [disabled]="status() === 'received'" > <button ui-select-trigger class="h-8 w-[110px] text-xs capitalize"> <ui-select-value /> </button> <ui-select-content> <ui-select-item value="sellable">Sellable</ui-select-item> <ui-select-item value="quarantine">Quarantine</ui-select-item> <ui-select-item value="damaged">Damaged</ui-select-item> </ui-select-content> </ui-select> </td> <!-- Remove Action --> <td ui-table-cell> <button ui-button variant="ghost" size="icon" class="text-muted-foreground hover:text-destructive size-8" [disabled]="status() === 'received'" [attr.aria-label]="'Remove ' + item.sku" (click)="removeItem(item.id)" > <lucide-icon [img]="Trash2" class="size-4" /> </button> </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>All inventory movements are recorded in immutable ledger log #LOG-9402.</div> <div class="text-foreground font-medium"> Total: <span class="font-mono">{{ totalUnits() }}</span> items across <span class="font-mono">{{ totalLineItems() }}</span> positions </div> </div> </div> `, }) export class UiStockTransferManagerComponent implements OnInit { @Input() transferId = 'TRF-2026-8841' @Input() initialStatus: StockTransferStatus = 'staged' @Input('class') className?: string protected readonly ArrowRight = ArrowRight protected readonly Barcode = Barcode protected readonly CheckCircle2 = CheckCircle2 protected readonly Plus = Plus protected readonly QrCode = QrCode protected readonly Trash2 = Trash2 protected readonly Truck = Truck protected readonly Warehouse = Warehouse readonly status = signal<StockTransferStatus>('staged') readonly sourceWarehouse = signal('wh-east-01') readonly targetWarehouse = signal('wh-central-02') readonly transportType = signal('pallet_freight') readonly barcodeQuery = signal('') readonly items = signal<TransferItem[]>([ { id: 'itm-1', sku: 'SKU-LOGI-884', name: 'Industrial Barcode Scanner IP65', barcode: '079357318921', lotNumber: 'LOT-2026-A1', sourceBin: 'A-04-R2-B12', targetBin: 'C-01-R1-B03', availableQty: 140, transferQty: 25, unit: 'pcs', condition: 'sellable', }, { id: 'itm-2', sku: 'SKU-PWR-331', name: 'Lithium Iron Battery Pack 48V', barcode: '079357318945', lotNumber: 'LOT-2025-X9', sourceBin: 'B-12-R4-B01', targetBin: 'D-08-R2-B09', availableQty: 48, transferQty: 12, unit: 'units', condition: 'sellable', }, { id: 'itm-3', sku: 'SKU-SENS-102', name: 'Optical Proximity Sensor M18', barcode: '079357318988', lotNumber: 'LOT-2026-C4', sourceBin: 'A-02-R1-B06', targetBin: 'A-09-R3-B02', availableQty: 320, transferQty: 80, unit: 'pcs', condition: 'sellable', }, ]) readonly totalUnits = computed(() => this.items().reduce((sum, item) => sum + Number(item.transferQty || 0), 0)) readonly totalLineItems = computed(() => this.items().length) /** Origin, destination and shipping method can only change before dispatch. */ readonly routingLocked = computed(() => this.status() !== 'draft' && this.status() !== 'staged') /** React seeds its status state from `initialStatus` once, on mount. */ ngOnInit(): void { this.status.set(this.initialStatus) } get hostClass(): string { return cn('block w-full space-y-6', this.className) } protected statusVariant(): BadgeVariant { const status = this.status() return status === 'received' ? 'default' : status === 'in_transit' ? 'secondary' : 'outline' } protected statusLabel(): string { return this.status().replace('_', ' ') } addItem(): void { this.items.update((prev) => [ ...prev, { id: `itm-${Date.now()}`, sku: 'SKU-GEN-001', name: 'Standard Packing Material Kit', barcode: '079357399999', lotNumber: 'LOT-UNASSIGNED', sourceBin: 'A-01-R1-B01', targetBin: 'B-01-R1-B01', availableQty: 100, transferQty: 10, unit: 'kits', condition: 'sellable', }, ]) } removeItem(id: string): void { this.items.update((prev) => prev.filter((i) => i.id !== id)) } updateItemQty(id: string, qty: string | number): void { const transferQty = Number(qty) this.items.update((prev) => prev.map((i) => (i.id === id ? { ...i, transferQty } : i))) } updateItemCondition(id: string, condition: string): void { this.items.update((prev) => prev.map((i) => (i.id === id ? { ...i, condition: condition as TransferItemCondition } : i)), ) } handleScan(): void { const query = this.barcodeQuery() if (!query) return this.items.update((prev) => prev.map((i) => (i.barcode === query || i.sku === query ? { ...i, transferQty: i.transferQty + 1 } : i)), ) this.barcodeQuery.set('') } }src/app/components/blocks/stock-transfer-manager/index.ts0.2 kB
export { UiStockTransferManagerComponent } from './stock-transfer-manager.component' export type { TransferItem, TransferItemCondition, StockTransferProps, StockTransferStatus, } from './stock-transfer-manager.component'
Raw manifest:https://uipkge.dev/r/angular/stock-transfer-manager.json