Purchase Order Receiving
blockdashboardInbound dock receiving workbench with PO matching, QA discrepancy logging, lot registration, and GRN generation.
Installation
$pnpm dlx uipkge-ng@latest add purchase-order-receiving$npx uipkge-ng@latest add purchase-order-receiving$yarn dlx uipkge-ng@latest add purchase-order-receiving$bunx uipkge-ng@latest add purchase-order-receivingManifest:
https://uipkge.dev/r/angular/purchase-order-receiving.jsonInstalls to:src/app/components/blocks/purchase-order-receiving/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.
POLineIteminterface POLineItem {
id: string
sku: string
title: string
orderedQty: number
receivedQty: number
rejectedQty: number
rejectionReason: 'none' | 'damaged_packaging' | 'expired_lot' | 'wrong_specification' | 'temperature_excursion'
lotNumber: string
expiryDate: string
stagingLocation: string
}Files installed (2)
src/app/components/blocks/purchase-order-receiving/purchase-order-receiving.component.ts14.9 kB
import { Component, Input, computed, signal, ChangeDetectionStrategy } from '@angular/core' import { AlertTriangle, Clock, LucideAngularModule, PackageCheck, Printer, ShieldCheck, 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 interface POLineItem { id: string sku: string title: string orderedQty: number receivedQty: number rejectedQty: number rejectionReason: 'none' | 'damaged_packaging' | 'expired_lot' | 'wrong_specification' | 'temperature_excursion' lotNumber: string expiryDate: string stagingLocation: string } export interface PurchaseOrderReceivingProps { poNumber?: string vendorName?: string deliveryDocket?: string className?: string } export type InspectionStatus = 'pending' | 'passed' | 'discrepancy' /** * Angular port of the React PurchaseOrderReceiving block: an inbound dock workbench with the * PO header (docket, receipt status, print / Generate GRN actions), a receiving bay picker, * the accepted-vs-ordered summary and QA disposition, and a line table where each PO line's * received / rejected quantities, rejection reason and lot number are logged. Generating the * GRN locks the lines and marks the receipt passed or discrepancy. Edit the sample lines below. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-purchase-order-receiving, [ui-purchase-order-receiving]', standalone: true, host: { '[attr.data-slot]': '"purchase-order-receiving"', '[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: ` <!-- Top PO Header Info --> <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">{{ poNumber }}</span> <span ui-badge variant="outline" class="font-mono text-xs">{{ deliveryDocket }}</span> <span ui-badge [variant]="statusVariant()" class="capitalize">{{ isCompleted() ? 'GRN ' + inspectionStatus() : 'Receiving in Progress' }}</span> </div> <h3 ui-card-title class="text-xl">Inbound Goods Receipt & QA Inspection</h3> <p ui-card-description> Supplier: <strong class="text-foreground font-medium">{{ vendorName }}</strong> · Inbound Dock Verification </p> </div> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="gap-1.5"> <lucide-icon [img]="Printer" class="size-4" /> Print Putaway Labels </button> @if (!isCompleted()) { <button ui-button variant="default" size="sm" class="gap-1.5" (click)="finalizeReceipt()"> <lucide-icon [img]="PackageCheck" class="size-4" /> Generate GRN </button> } @else { <button ui-button variant="secondary" size="sm" class="gap-1.5" (click)="resetReceipt()"> Edit Receipt </button> } </div> </div> <div ui-card-content class="border-border grid grid-cols-1 gap-4 border-t pt-4 sm:grid-cols-3"> <!-- Receiving Dock --> <div class="border-border space-y-1.5 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>RECEIVING BAY</span> </div> <ui-select [value]="receivingDock()" (valueChange)="receivingDock.set($event)" [disabled]="isCompleted()"> <button ui-select-trigger class="w-full"> <ui-select-value /> </button> <ui-select-content> <ui-select-item value="dock-01">Inbound Dock Bay 01 (Heavy Freight)</ui-select-item> <ui-select-item value="dock-02">Inbound Dock Bay 02 (Cross-Dock)</ui-select-item> <ui-select-item value="dock-03">Inbound Dock Bay 03 (Standard Freight)</ui-select-item> <ui-select-item value="dock-04">Inbound Cold Storage Quarantine Bay</ui-select-item> </ui-select-content> </ui-select> </div> <!-- Discrepancy Summary --> <div class="border-border space-y-1 rounded-lg border p-3"> <div class="text-muted-foreground text-xs font-medium">RECEIPT SUMMARY</div> <div class="flex items-baseline justify-between pt-1"> <div class="font-mono text-xl font-semibold">{{ totalAccepted() }} / {{ totalOrdered() }}</div> @if (totalRejected() > 0) { <span ui-badge variant="destructive" class="font-mono text-xs">{{ totalRejected() }} Rejected</span> } @else { <span ui-badge variant="outline" class="text-muted-foreground font-mono text-xs">100% Match</span> } </div> </div> <!-- QA Verification Badge --> <div class="border-border space-y-1 rounded-lg border p-3"> <div class="text-muted-foreground text-xs font-medium">QA AUDIT DISPOSITION</div> <div class="flex items-center gap-2 pt-1"> @if (inspectionStatus() === 'passed') { <lucide-icon [img]="ShieldCheck" class="text-foreground size-5" /> } @if (inspectionStatus() === 'discrepancy') { <lucide-icon [img]="AlertTriangle" class="text-destructive size-5" /> } @if (inspectionStatus() === 'pending') { <lucide-icon [img]="Clock" class="text-muted-foreground size-5" /> } <span class="text-sm font-medium">{{ dispositionLabel() }}</span> </div> </div> </div> </div> <!-- PO Line Item Matching Table --> <div ui-card> <div ui-card-header class="pb-3"> <h3 ui-card-title class="text-base font-semibold">Purchase Order Line Verification</h3> <p ui-card-description> Verify quantities received against vendor packing list and assign warehouse staging zones. </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-[280px]">Item / Description</th> <th ui-table-head class="text-right">Ordered</th> <th ui-table-head class="w-[110px] text-right">Received</th> <th ui-table-head class="w-[110px] text-right">Rejected</th> <th ui-table-head>Rejection Reason</th> <th ui-table-head>Lot Number</th> <th ui-table-head>Staging Target</th> </tr> </thead> <tbody ui-table-body> @for (line of lines(); track line.id) { <tr ui-table-row> <!-- Item Details --> <td ui-table-cell> <div class="font-medium">{{ line.title }}</div> <div class="text-muted-foreground font-mono text-xs">{{ line.sku }}</div> </td> <!-- Ordered Qty --> <td ui-table-cell class="text-muted-foreground text-right font-mono text-sm"> {{ line.orderedQty }} </td> <!-- Received Qty --> <td ui-table-cell class="text-right"> <ui-input type="number" [min]="0" [value]="line.receivedQty" (valueChange)="updateLine(line.id, { receivedQty: toNumber($event) })" class="h-8 text-right font-mono" [disabled]="isCompleted()" /> </td> <!-- Rejected Qty --> <td ui-table-cell class="text-right"> <ui-input type="number" [min]="0" [value]="line.rejectedQty" (valueChange)="updateLine(line.id, { rejectedQty: toNumber($event) })" class="h-8 text-right font-mono" [disabled]="isCompleted()" /> </td> <!-- Reason --> <td ui-table-cell> <ui-select [value]="line.rejectionReason" (valueChange)="setRejectionReason(line.id, $event)" [disabled]="isCompleted() || line.rejectedQty === 0" > <button ui-select-trigger class="h-8 w-[160px] text-xs"> <ui-select-value /> </button> <ui-select-content> <ui-select-item value="none">None (Accepted)</ui-select-item> <ui-select-item value="damaged_packaging">Damaged Packaging</ui-select-item> <ui-select-item value="expired_lot">Short Shelf Life</ui-select-item> <ui-select-item value="wrong_specification">Incorrect Spec</ui-select-item> <ui-select-item value="temperature_excursion">Temp Excursion</ui-select-item> </ui-select-content> </ui-select> </td> <!-- Lot Number Input --> <td ui-table-cell> <ui-input [value]="line.lotNumber" (valueChange)="updateLine(line.id, { lotNumber: $event })" placeholder="Lot #" class="h-8 w-28 font-mono text-xs" [disabled]="isCompleted()" /> </td> <!-- Staging Target --> <td ui-table-cell> <span class="bg-muted text-foreground rounded px-2 py-1 font-mono text-xs font-medium">{{ line.stagingLocation }}</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>Official GRN generates supplier debit note automatically for any non-zero rejection.</div> <div class="text-foreground font-medium"> Accepted: <span class="font-mono font-semibold">{{ totalAccepted() }}</span> units · Rejected: <span class="text-destructive font-mono font-semibold">{{ totalRejected() }}</span> units </div> </div> </div> `, }) export class UiPurchaseOrderReceivingComponent { @Input() poNumber = 'PO-2026-4409' @Input() vendorName = 'Apex Precision Engineering Ltd' @Input() deliveryDocket = 'BOL-APX-98214' @Input('class') className?: string protected readonly AlertTriangle = AlertTriangle protected readonly Clock = Clock protected readonly PackageCheck = PackageCheck protected readonly Printer = Printer protected readonly ShieldCheck = ShieldCheck protected readonly Warehouse = Warehouse readonly isCompleted = signal(false) readonly receivingDock = signal('dock-03') readonly inspectionStatus = signal<InspectionStatus>('pending') readonly lines = signal<POLineItem[]>([ { id: 'po-1', sku: 'SKU-VALVE-99', title: 'High-Pressure Hydraulic Valve 3/8"', orderedQty: 100, receivedQty: 100, rejectedQty: 0, rejectionReason: 'none', lotNumber: 'LOT-HYD-998', expiryDate: '2029-12-31', stagingLocation: 'ZONE-A-RACK-04', }, { id: 'po-2', sku: 'SKU-SEAL-04', title: 'Fluorocarbon O-Ring Flange Kit (Pack of 50)', orderedQty: 250, receivedQty: 245, rejectedQty: 5, rejectionReason: 'damaged_packaging', lotNumber: 'LOT-O-7712', expiryDate: '2028-06-30', stagingLocation: 'ZONE-A-RACK-01', }, { id: 'po-3', sku: 'SKU-GAUGE-12', title: 'Digital Calibration Pressure Gauge 0-600 PSI', orderedQty: 40, receivedQty: 40, rejectedQty: 0, rejectionReason: 'none', lotNumber: 'LOT-CAL-002', expiryDate: '2031-01-15', stagingLocation: 'ZONE-B-RACK-09', }, ]) readonly totalOrdered = computed(() => this.lines().reduce((acc, item) => acc + item.orderedQty, 0)) readonly totalAccepted = computed(() => this.lines().reduce((acc, item) => acc + item.receivedQty, 0)) readonly totalRejected = computed(() => this.lines().reduce((acc, item) => acc + item.rejectedQty, 0)) readonly statusVariant = computed<BadgeVariant>(() => { const status = this.inspectionStatus() return status === 'passed' ? 'default' : status === 'discrepancy' ? 'destructive' : 'secondary' }) readonly dispositionLabel = computed(() => { const status = this.inspectionStatus() return status === 'passed' ? 'All lines cleared for putaway' : status === 'discrepancy' ? 'Discrepancy logged for vendor credit' : 'Awaiting line verification' }) get hostClass(): string { return cn('block w-full space-y-6', this.className) } updateLine(id: string, updates: Partial<POLineItem>): void { this.lines.update((prev) => prev.map((line) => (line.id === id ? { ...line, ...updates } : line))) } setRejectionReason(id: string, value: string): void { this.updateLine(id, { rejectionReason: value as POLineItem['rejectionReason'] }) } protected toNumber(value: string): number { return Number(value) } finalizeReceipt(): void { if (this.totalRejected() > 0 || this.totalAccepted() < this.totalOrdered()) { this.inspectionStatus.set('discrepancy') } else { this.inspectionStatus.set('passed') } this.isCompleted.set(true) } resetReceipt(): void { this.isCompleted.set(false) this.inspectionStatus.set('pending') } }src/app/components/blocks/purchase-order-receiving/index.ts0.2 kB
export { UiPurchaseOrderReceivingComponent } from './purchase-order-receiving.component' export type { InspectionStatus, POLineItem, PurchaseOrderReceivingProps } from './purchase-order-receiving.component'
Raw manifest:https://uipkge.dev/r/angular/purchase-order-receiving.json