Purchase Order Receiving
blockdashboardInbound dock receiving workbench with PO matching, QA discrepancy logging, lot registration, and GRN generation.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/purchase-order-receiving.json$npx shadcn@latest add https://uipkge.dev/r/react/purchase-order-receiving.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/purchase-order-receiving.json$bunx shadcn@latest add https://uipkge.dev/r/react/purchase-order-receiving.jsonNamed registry:
npx shadcn@latest add @uipkge-react/purchase-order-receivingInstalls to:components/blocks/purchase-order-receiving/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
poNumber | string | — | optional |
vendorName | string | — | optional |
deliveryDocket | string | — | optional |
className | string | — | optional |
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)
components/blocks/purchase-order-receiving/PurchaseOrderReceiving.tsx13 kB
'use client' import * as React from 'react' import { AlertTriangle, Clock, PackageCheck, Printer, ShieldCheck, Warehouse } from 'lucide-react' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' 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 function PurchaseOrderReceiving({ poNumber = 'PO-2026-4409', vendorName = 'Apex Precision Engineering Ltd', deliveryDocket = 'BOL-APX-98214', className, }: PurchaseOrderReceivingProps) { const [isCompleted, setIsCompleted] = React.useState(false) const [receivingDock, setReceivingDock] = React.useState('dock-03') const [inspectionStatus, setInspectionStatus] = React.useState<'pending' | 'passed' | 'discrepancy'>('pending') const [lines, setLines] = React.useState<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', }, ]) const totalOrdered = lines.reduce((acc, item) => acc + item.orderedQty, 0) const totalAccepted = lines.reduce((acc, item) => acc + item.receivedQty, 0) const totalRejected = lines.reduce((acc, item) => acc + item.rejectedQty, 0) function updateLine(id: string, updates: Partial<POLineItem>) { setLines((prev) => prev.map((line) => (line.id === id ? { ...line, ...updates } : line))) } function finalizeReceipt() { if (totalRejected > 0 || totalAccepted < totalOrdered) { setInspectionStatus('discrepancy') } else { setInspectionStatus('passed') } setIsCompleted(true) } function resetReceipt() { setIsCompleted(false) setInspectionStatus('pending') } return ( <div data-slot="purchase-order-receiving" className={`w-full space-y-6 ${className ?? ''}`}> {/* Top PO Header Info */} <Card> <CardHeader className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="space-y-1"> <div className="flex items-center gap-2"> <span className="text-foreground font-mono text-sm font-semibold">{poNumber}</span> <Badge variant="outline" className="font-mono text-xs"> {deliveryDocket} </Badge> <Badge variant={ inspectionStatus === 'passed' ? 'default' : inspectionStatus === 'discrepancy' ? 'destructive' : 'secondary' } className="capitalize" > {isCompleted ? `GRN ${inspectionStatus}` : 'Receiving in Progress'} </Badge> </div> <CardTitle className="text-xl">Inbound Goods Receipt & QA Inspection</CardTitle> <CardDescription> Supplier: <strong className="text-foreground font-medium">{vendorName}</strong> · Inbound Dock Verification </CardDescription> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="sm" className="gap-1.5"> <Printer className="size-4" /> Print Putaway Labels </Button> {!isCompleted ? ( <Button variant="default" size="sm" className="gap-1.5" onClick={finalizeReceipt}> <PackageCheck className="size-4" /> Generate GRN </Button> ) : ( <Button variant="secondary" size="sm" className="gap-1.5" onClick={resetReceipt}> Edit Receipt </Button> )} </div> </CardHeader> <CardContent className="border-border grid grid-cols-1 gap-4 border-t pt-4 sm:grid-cols-3"> {/* Receiving Dock */} <div className="border-border space-y-1.5 rounded-lg border p-3"> <div className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium"> <Warehouse className="size-3.5" /> <span>RECEIVING BAY</span> </div> <Select value={receivingDock} onValueChange={setReceivingDock} disabled={isCompleted}> <SelectTrigger className="w-full"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="dock-01">Inbound Dock Bay 01 (Heavy Freight)</SelectItem> <SelectItem value="dock-02">Inbound Dock Bay 02 (Cross-Dock)</SelectItem> <SelectItem value="dock-03">Inbound Dock Bay 03 (Standard Freight)</SelectItem> <SelectItem value="dock-04">Inbound Cold Storage Quarantine Bay</SelectItem> </SelectContent> </Select> </div> {/* Discrepancy Summary */} <div className="border-border space-y-1 rounded-lg border p-3"> <div className="text-muted-foreground text-xs font-medium">RECEIPT SUMMARY</div> <div className="flex items-baseline justify-between pt-1"> <div className="font-mono text-xl font-semibold"> {totalAccepted} / {totalOrdered} </div> {totalRejected > 0 ? ( <Badge variant="destructive" className="font-mono text-xs"> {totalRejected} Rejected </Badge> ) : ( <Badge variant="outline" className="text-muted-foreground font-mono text-xs"> 100% Match </Badge> )} </div> </div> {/* QA Verification Badge */} <div className="border-border space-y-1 rounded-lg border p-3"> <div className="text-muted-foreground text-xs font-medium">QA AUDIT DISPOSITION</div> <div className="flex items-center gap-2 pt-1"> {inspectionStatus === 'passed' && <ShieldCheck className="text-foreground size-5" />} {inspectionStatus === 'discrepancy' && <AlertTriangle className="text-destructive size-5" />} {inspectionStatus === 'pending' && <Clock className="text-muted-foreground size-5" />} <span className="text-sm font-medium"> {inspectionStatus === 'passed' ? 'All lines cleared for putaway' : inspectionStatus === 'discrepancy' ? 'Discrepancy logged for vendor credit' : 'Awaiting line verification'} </span> </div> </div> </CardContent> </Card> {/* PO Line Item Matching Table */} <Card> <CardHeader className="pb-3"> <CardTitle className="text-base font-semibold">Purchase Order Line Verification</CardTitle> <CardDescription> Verify quantities received against vendor packing list and assign warehouse staging zones. </CardDescription> </CardHeader> <CardContent className="p-0"> <Table> <TableHeader> <TableRow> <TableHead className="w-[280px]">Item / Description</TableHead> <TableHead className="text-right">Ordered</TableHead> <TableHead className="w-[110px] text-right">Received</TableHead> <TableHead className="w-[110px] text-right">Rejected</TableHead> <TableHead>Rejection Reason</TableHead> <TableHead>Lot Number</TableHead> <TableHead>Staging Target</TableHead> </TableRow> </TableHeader> <TableBody> {lines.map((line) => ( <TableRow key={line.id}> {/* Item Details */} <TableCell> <div className="font-medium">{line.title}</div> <div className="text-muted-foreground font-mono text-xs">{line.sku}</div> </TableCell> {/* Ordered Qty */} <TableCell className="text-muted-foreground text-right font-mono text-sm"> {line.orderedQty} </TableCell> {/* Received Qty */} <TableCell className="text-right"> <Input type="number" min={0} value={line.receivedQty} onChange={(e) => updateLine(line.id, { receivedQty: Number(e.target.value) })} className="h-8 text-right font-mono" disabled={isCompleted} /> </TableCell> {/* Rejected Qty */} <TableCell className="text-right"> <Input type="number" min={0} value={line.rejectedQty} onChange={(e) => updateLine(line.id, { rejectedQty: Number(e.target.value) })} className="h-8 text-right font-mono" disabled={isCompleted} /> </TableCell> {/* Reason */} <TableCell> <Select value={line.rejectionReason} onValueChange={(val: POLineItem['rejectionReason']) => updateLine(line.id, { rejectionReason: val }) } disabled={isCompleted || line.rejectedQty === 0} > <SelectTrigger className="h-8 w-[160px] text-xs"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="none">None (Accepted)</SelectItem> <SelectItem value="damaged_packaging">Damaged Packaging</SelectItem> <SelectItem value="expired_lot">Short Shelf Life</SelectItem> <SelectItem value="wrong_specification">Incorrect Spec</SelectItem> <SelectItem value="temperature_excursion">Temp Excursion</SelectItem> </SelectContent> </Select> </TableCell> {/* Lot Number Input */} <TableCell> <Input value={line.lotNumber} onChange={(e) => updateLine(line.id, { lotNumber: e.target.value })} placeholder="Lot #" className="h-8 w-28 font-mono text-xs" disabled={isCompleted} /> </TableCell> {/* Staging Target */} <TableCell> <span className="bg-muted text-foreground rounded px-2 py-1 font-mono text-xs font-medium"> {line.stagingLocation} </span> </TableCell> </TableRow> ))} </TableBody> </Table> </CardContent> <CardFooter className="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 className="text-foreground font-medium"> Accepted: <span className="font-mono font-semibold">{totalAccepted}</span> units · Rejected:{' '} <span className="text-destructive font-mono font-semibold">{totalRejected}</span> units </div> </CardFooter> </Card> </div> ) }components/blocks/purchase-order-receiving/index.ts0.2 kB
export { PurchaseOrderReceiving } from './PurchaseOrderReceiving' export type { POLineItem, PurchaseOrderReceivingProps } from './PurchaseOrderReceiving'
Raw manifest:https://uipkge.dev/r/react/purchase-order-receiving.json