Wholesale B2b Quick Order
blockcommerceB2B wholesale bulk order matrix with volume tier pricing, case pack multipliers, and CSV SKU paste input.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/wholesale-b2b-quick-order.json$npx shadcn@latest add https://uipkge.dev/r/react/wholesale-b2b-quick-order.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/wholesale-b2b-quick-order.json$bunx shadcn@latest add https://uipkge.dev/r/react/wholesale-b2b-quick-order.jsonNamed registry:
npx shadcn@latest add @uipkge-react/wholesale-b2b-quick-orderInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
poNumber | string | — | optional |
accountName | string | — | optional |
accountTier | string | — | optional |
tierDiscountPercent | number | — | optional |
paymentTerms | string | — | optional |
freeFreightThreshold | number | — | optional |
initialProducts | WholesaleProduct[] | — | optional |
className | string | — | optional |
onSubmitOrder | (payload: { | — | optional |
items | WholesaleProduct[] | — | required |
subtotal | number | — | required |
totalUnits | number | — | required |
totalCases | number | — | required |
savings | number | — | required |
onSaveTemplate | (items: WholesaleProduct[]) => void | — | optional |
onExportCsv | (items: WholesaleProduct[]) => void | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
WholesaleProductinterface WholesaleProduct {
id: string
sku: string
name: string
variant: string
casePack: number
msrpUnitPrice: number
wholesaleUnitPrice: number
cases: number
availableCases: number
image: string
category: string
}Files installed (1)
components/blocks/WholesaleB2bQuickOrder.tsx35.5 kB
'use client' import * as React from 'react' import { useState, useMemo, useCallback } from 'react' import { AlertCircle, Bookmark, Building2, CheckCircle2, Download, FileSpreadsheet, Minus, Package, Plus, RefreshCw, Send, Layers, Trash2, TrendingDown, Truck, Upload, X, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Textarea } from '@/components/ui/textarea' export interface WholesaleProduct { id: string sku: string name: string variant: string casePack: number msrpUnitPrice: number wholesaleUnitPrice: number cases: number availableCases: number image: string category: string } export interface WholesaleB2bQuickOrderProps { poNumber?: string accountName?: string accountTier?: string tierDiscountPercent?: number paymentTerms?: string freeFreightThreshold?: number initialProducts?: WholesaleProduct[] className?: string onSubmitOrder?: (payload: { poNumber: string items: WholesaleProduct[] subtotal: number totalUnits: number totalCases: number savings: number }) => void onSaveTemplate?: (items: WholesaleProduct[]) => void onExportCsv?: (items: WholesaleProduct[]) => void } export const DEFAULT_WHOLESALE_PRODUCTS: WholesaleProduct[] = [ { id: 'prod-1', sku: 'SKU-84920', name: 'Pro Studio Headphones', variant: 'Matte Black · Pro Series', casePack: 12, msrpUnitPrice: 299.0, wholesaleUnitPrice: 194.35, cases: 4, availableCases: 450, image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&auto=format&fit=crop&q=80', category: 'Audio & Acoustics', }, { id: 'prod-2', sku: 'SKU-49102', name: 'Aero Minimalist Runner', variant: 'Arctic White · US 10-12 Assorted', casePack: 10, msrpUnitPrice: 140.0, wholesaleUnitPrice: 91.0, cases: 3, availableCases: 180, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=300&auto=format&fit=crop&q=80', category: 'Footwear', }, { id: 'prod-3', sku: 'SKU-77215', name: 'Technical Shell Parka', variant: 'Mineral Gray · Waterproof 3L', casePack: 8, msrpUnitPrice: 220.0, wholesaleUnitPrice: 143.0, cases: 2, availableCases: 95, image: 'https://images.unsplash.com/photo-1591047139829-d91aecb6caea?w=300&auto=format&fit=crop&q=80', category: 'Outerwear', }, { id: 'prod-4', sku: 'SKU-10934', name: 'Braided USB-C Cable', variant: 'Space Gray · 2m 240W EPR', casePack: 24, msrpUnitPrice: 25.0, wholesaleUnitPrice: 16.25, cases: 2, availableCases: 620, image: 'https://images.unsplash.com/photo-1583863788434-e58a36330cf0?w=300&auto=format&fit=crop&q=80', category: 'Hardware & Cables', }, { id: 'prod-5', sku: 'SKU-63821', name: 'Leather Cardholder', variant: 'Saddle Brown · Full-Grain Veg-Tan', casePack: 20, msrpUnitPrice: 50.0, wholesaleUnitPrice: 32.5, cases: 1, availableCases: 24, image: 'https://images.unsplash.com/photo-1627123424574-724758594e93?w=300&auto=format&fit=crop&q=80', category: 'Leather Goods', }, ] const formatCurrency = (val: number) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(val) } const formatNumber = (val: number) => { return new Intl.NumberFormat('en-US').format(val) } export function WholesaleB2bQuickOrder({ poNumber = 'PO-2026-8841B', accountName = 'Northwind Retailers', accountTier = 'Tier 3 Wholesale Partner', tierDiscountPercent = 35, paymentTerms = 'Net 30 Invoicing', freeFreightThreshold = 10000, initialProducts, className, onSubmitOrder, onSaveTemplate, onExportCsv, }: WholesaleB2bQuickOrderProps) { const [products, setProducts] = useState<WholesaleProduct[]>(() => initialProducts ? JSON.parse(JSON.stringify(initialProducts)) : JSON.parse(JSON.stringify(DEFAULT_WHOLESALE_PRODUCTS)), ) const [poRef] = useState(poNumber) const [showCsvBox, setShowCsvBox] = useState(false) const [csvPasteText, setCsvPasteText] = useState('') const [pasteFeedback, setPasteFeedback] = useState<{ type: 'success' | 'error'; message: string } | null>(null) const [templateSaved, setTemplateSaved] = useState(false) const [orderSubmitted, setOrderSubmitted] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) // Computations const totalCases = useMemo(() => products.reduce((acc, p) => acc + (Number(p.cases) || 0), 0), [products]) const totalUnits = useMemo( () => products.reduce((acc, p) => acc + (Number(p.cases) || 0) * p.casePack, 0), [products], ) const wholesaleSubtotal = useMemo( () => products.reduce((acc, p) => acc + (Number(p.cases) || 0) * p.casePack * p.wholesaleUnitPrice, 0), [products], ) const msrpTotal = useMemo( () => products.reduce((acc, p) => acc + (Number(p.cases) || 0) * p.casePack * p.msrpUnitPrice, 0), [products], ) const totalSavings = useMemo(() => Math.max(0, msrpTotal - wholesaleSubtotal), [msrpTotal, wholesaleSubtotal]) const savingsPercent = useMemo(() => { if (msrpTotal <= 0) return tierDiscountPercent return Math.round((totalSavings / msrpTotal) * 100) }, [msrpTotal, totalSavings, tierDiscountPercent]) const isFreeFreight = wholesaleSubtotal >= freeFreightThreshold const freightAmount = isFreeFreight ? 0 : 250 const grandTotal = wholesaleSubtotal + freightAmount const nextTierTarget = 50000 const nextTierRemaining = Math.max(0, nextTierTarget - wholesaleSubtotal) const tierProgressPercent = Math.min(100, Math.round((wholesaleSubtotal / nextTierTarget) * 100)) // Actions const updateCases = useCallback((id: string, delta: number) => { setProducts((prev) => prev.map((p) => { if (p.id === id) { const current = Number(p.cases) || 0 const next = Math.max(0, Math.min(p.availableCases, current + delta)) return { ...p, cases: next } } return p }), ) }, []) const setCases = useCallback((id: string, val: string | number) => { setProducts((prev) => prev.map((p) => { if (p.id === id) { const parsed = parseInt(String(val), 10) const next = isNaN(parsed) || parsed < 0 ? 0 : Math.min(p.availableCases, parsed) return { ...p, cases: next } } return p }), ) }, []) const clearLine = useCallback((id: string) => { setProducts((prev) => prev.map((p) => (p.id === id ? { ...p, cases: 0 } : p))) }, []) const resetAllCases = useCallback(() => { setProducts((prev) => prev.map((p) => ({ ...p, cases: 0 }))) setPasteFeedback(null) }, []) const restoreDefaultCases = useCallback(() => { setProducts(JSON.parse(JSON.stringify(DEFAULT_WHOLESALE_PRODUCTS))) setPasteFeedback(null) }, []) const applyCsvPaste = useCallback(() => { setPasteFeedback(null) if (!csvPasteText.trim()) { setPasteFeedback({ type: 'error', message: 'Please paste SKU and case quantity rows to parse.', }) return } const lines = csvPasteText.split(/\r?\n/) let updatedCount = 0 const unknownSkus: string[] = [] setProducts((prev) => { const cloned = prev.map((p) => ({ ...p })) for (const rawLine of lines) { const line = rawLine.trim() if (!line || line.startsWith('#') || line.toLowerCase().startsWith('sku')) continue const parts = line.split(/[,;\t]+/).map((s) => s.trim()) if (parts.length >= 2) { const skuQuery = parts[0].toUpperCase() const qty = parseInt(parts[1], 10) if (!isNaN(qty)) { const product = cloned.find( (p) => p.sku.toUpperCase() === skuQuery || p.sku.toUpperCase().includes(skuQuery), ) if (product) { product.cases = Math.max(0, Math.min(product.availableCases, qty)) updatedCount++ } else { unknownSkus.push(parts[0]) } } } } return cloned }) if (updatedCount > 0) { setPasteFeedback({ type: 'success', message: `Successfully updated ${updatedCount} SKU line items in bulk matrix.${ unknownSkus.length > 0 ? ` Note: ${unknownSkus.length} SKU(s) not found in catalog (${unknownSkus.slice(0, 3).join(', ')}).` : '' }`, }) } else { setPasteFeedback({ type: 'error', message: 'No matching catalog SKUs found. Verify format: SKU-84920, 10', }) } }, [csvPasteText]) const loadSampleCsv = useCallback(() => { const sample = `SKU-84920, 8\nSKU-49102, 5\nSKU-77215, 4\nSKU-10934, 12\nSKU-63821, 6` setCsvPasteText(sample) // Parse sample directly const lines = sample.split(/\r?\n/) setProducts((prev) => { const cloned = prev.map((p) => ({ ...p })) let count = 0 for (const rawLine of lines) { const parts = rawLine.split(/[,;\t]+/).map((s) => s.trim()) if (parts.length >= 2) { const skuQuery = parts[0].toUpperCase() const qty = parseInt(parts[1], 10) const product = cloned.find((p) => p.sku.toUpperCase() === skuQuery) if (product && !isNaN(qty)) { product.cases = Math.max(0, Math.min(product.availableCases, qty)) count++ } } } return cloned }) setPasteFeedback({ type: 'success', message: 'Sample manifest loaded and applied to matrix (5 SKUs updated).', }) }, []) const handleSaveTemplate = useCallback(() => { setTemplateSaved(true) onSaveTemplate?.(products) setTimeout(() => { setTemplateSaved(false) }, 4000) }, [products, onSaveTemplate]) const handleSubmitOrder = useCallback(() => { if (totalCases === 0) return setIsSubmitting(true) setTimeout(() => { setIsSubmitting(false) setOrderSubmitted(true) onSubmitOrder?.({ poNumber: poRef, items: products.filter((p) => p.cases > 0), subtotal: wholesaleSubtotal, totalUnits, totalCases, savings: totalSavings, }) }, 600) }, [totalCases, poRef, products, wholesaleSubtotal, totalUnits, totalSavings, onSubmitOrder]) const handleExportCsv = useCallback(() => { onExportCsv?.(products) const header = 'SKU,Product Name,Variant,Case Pack Multiplier,Cases Ordered,Total Units,Wholesale Unit Price,Line Total\n' const rows = products .filter((p) => p.cases > 0) .map( (p) => `"${p.sku}","${p.name}","${p.variant}",${p.casePack},${p.cases},${p.cases * p.casePack},${p.wholesaleUnitPrice},${(p.cases * p.casePack * p.wholesaleUnitPrice).toFixed(2)}`, ) .join('\n') const csvContent = 'data:text/csv;charset=utf-8,' + encodeURIComponent(header + rows) const downloadLink = document.createElement('a') downloadLink.setAttribute('href', csvContent) downloadLink.setAttribute('download', `${poRef}-order-matrix.csv`) document.body.appendChild(downloadLink) downloadLink.click() document.body.removeChild(downloadLink) }, [products, onExportCsv, poRef]) return ( <div data-slot="wholesale-b2b-quick-order" className={cn('bg-background text-foreground w-full space-y-6', className)} > {/* Header & Account Info */} <Card className="border-border bg-card shadow-xs"> <CardHeader className="pb-4"> <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div className="space-y-1.5"> <div className="flex flex-wrap items-center gap-2"> <div className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium"> <Building2 className="size-3.5" /> <span>{accountName}</span> <span>·</span> <span>{accountTier}</span> </div> <Badge variant="outline" className="border-success/30 bg-success/10 text-success font-medium"> <Layers className="mr-1 size-3" /> {tierDiscountPercent}% Off MSRP Wholesale Tier </Badge> </div> <CardTitle className="text-xl font-bold tracking-tight sm:text-2xl"> Wholesale & B2B Bulk Order Matrix </CardTitle> <CardDescription className="text-muted-foreground text-xs sm:text-sm"> Build your purchase order by entering case quantities below or pasting bulk SKU manifests with automated case multipliers. </CardDescription> </div> {/* Header Action Buttons */} <div className="flex flex-wrap items-center gap-2"> <Button aria-label="Close CSV paste" variant="outline" size="sm" className="gap-1.5 text-xs font-medium" onClick={() => setShowCsvBox(!showCsvBox)} > <FileSpreadsheet className="size-3.5" /> <span>{showCsvBox ? 'Hide CSV Paste' : 'Upload CSV Order'}</span> </Button> <Button variant="outline" size="sm" className="gap-1.5 text-xs font-medium" onClick={handleSaveTemplate}> <Bookmark className="size-3.5" /> <span>Save Order Template</span> </Button> </div> </div> </CardHeader> <CardContent className="pt-0"> {/* Account Terms & Partner Meta Strip */} <div className="bg-muted/40 border-border grid grid-cols-2 gap-3 rounded-lg border p-3 sm:grid-cols-4 sm:gap-4"> <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">PO Reference</div> <div className="font-mono text-sm font-semibold">{poRef}</div> </div> <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">Payment Terms</div> <div className="text-foreground flex items-center gap-1 text-sm font-semibold"> <CheckCircle2 className="text-success size-3.5" /> <span>{paymentTerms}</span> </div> </div> <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">Freight Status</div> <div className={cn('text-sm font-semibold tabular-nums', isFreeFreight ? 'text-success' : 'text-foreground')} > {isFreeFreight ? 'Free Ground Freight' : '$250 Standard Freight'} </div> </div> <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">Volume Tier Status</div> <div className="text-foreground text-sm font-semibold"> Tier 3 <span className="text-muted-foreground font-normal">({tierDiscountPercent}% Margin)</span> </div> </div> </div> {/* Volume Tier Margin Incentive Banner */} <div className="border-border bg-card/60 mt-3 rounded-lg border p-3"> <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-center gap-2"> <TrendingDown className="text-success size-4 shrink-0" /> <div className="text-xs"> <span className="text-foreground font-semibold">Tier 4 Milestone ($50,000):</span> <span className="text-muted-foreground ml-1"> {nextTierRemaining > 0 ? `Add ${formatCurrency(nextTierRemaining)} more to unlock 42% Tier 4 Enterprise Margin.` : 'Tier 4 Enterprise Margin (42%) Unlocked!'} </span> </div> </div> <div className="text-muted-foreground flex items-center gap-2 text-xs"> <span>{tierProgressPercent}% to Tier 4</span> <div className="bg-muted border-border h-2 w-24 overflow-hidden rounded-full border"> <div className="bg-success h-full transition-[width] duration-300" style={{ width: `${tierProgressPercent}%` }} /> </div> </div> </div> </div> </CardContent> </Card> {/* Template Saved Alert */} {templateSaved && ( <div className="border-success/30 bg-success/10 text-success text-success flex items-center justify-between rounded-lg border p-3 text-xs sm:text-sm"> <div className="flex items-center gap-2"> <Bookmark className="text-success size-4 shrink-0" /> <span>Order matrix saved as your primary template. You can re-populate this draft at any time.</span> </div> <Button variant="ghost" size="xs" className="text-success hover:text-success" aria-label="Dismiss notification" onClick={() => setTemplateSaved(false)} > <X className="size-3.5" /> </Button> </div> )} {/* Order Submitted Success Alert */} {orderSubmitted && ( <div className="border-success/30 bg-success/10 text-success dark:text-foreground rounded-xl border p-4 sm:p-5"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-start gap-3"> <CheckCircle2 className="text-success mt-0.5 size-5 shrink-0" /> <div className="space-y-1"> <h3 className="text-sm font-semibold sm:text-base">Purchase Order {poRef} Submitted Successfully!</h3> <p className="text-muted-foreground text-xs sm:text-sm"> Your bulk order of{' '} <strong className="text-foreground"> {totalCases} cases ({totalUnits} units) </strong>{' '} totaling <strong className="text-foreground">{formatCurrency(grandTotal)}</strong> has been queued for warehouse dispatch under {paymentTerms}. </p> </div> </div> <div className="flex items-center gap-2"> <Button aria-label="Download attachment" variant="outline" size="sm" className="text-xs" onClick={handleExportCsv} > <Download className="mr-1.5 size-3.5" /> Download PO PDF </Button> <Button size="sm" className="text-xs" onClick={() => setOrderSubmitted(false)}> Start New Order </Button> </div> </div> </div> )} {/* Quick SKU & Quantity CSV Paste Box */} {showCsvBox && ( <Card className="border-border bg-card shadow-xs transition-colors duration-200"> <CardHeader className="pb-3"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <FileSpreadsheet className="text-primary size-4" /> <CardTitle className="text-sm font-semibold sm:text-base"> Quick Paste SKU & Quantity Manifest </CardTitle> </div> <Button aria-label="Close CSV paste" variant="ghost" size="xs" className="text-muted-foreground hover:text-foreground" onClick={() => setShowCsvBox(false)} > <X className="size-4" /> </Button> </div> <CardDescription className="text-muted-foreground text-xs"> Paste CSV rows with SKU identifier and case quantity (one item per line, e.g.{' '} <code className="bg-muted rounded px-1 font-mono">SKU-84920, 10</code>) to auto-populate matrix. </CardDescription> </CardHeader> <CardContent className="space-y-3"> <Textarea value={csvPasteText} onValueChange={(val) => setCsvPasteText(val)} placeholder={'SKU-84920, 8\nSKU-49102, 5\nSKU-77215, 4\nSKU-10934, 12\nSKU-63821, 6'} rows={4} className="font-mono text-xs" /> <div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center gap-2"> <Button size="sm" className="text-xs font-medium" onClick={applyCsvPaste}> <Upload className="mr-1.5 size-3.5" /> Apply SKU List to Matrix </Button> <Button variant="outline" size="sm" className="text-xs font-medium" onClick={loadSampleCsv}> Load Sample Data </Button> {csvPasteText && ( <Button variant="ghost" size="sm" className="text-muted-foreground text-xs" onClick={() => setCsvPasteText('')} > Clear Input </Button> )} </div> <div className="text-muted-foreground text-xs">Supports comma, tab, and semicolon delimited lines</div> </div> {/* Parse Feedback Alert */} {pasteFeedback && ( <div className={cn( 'flex items-center gap-2 rounded-lg border p-2.5 text-xs', pasteFeedback.type === 'success' ? 'border-success/30 bg-success/10 text-success' : 'border-destructive/30 bg-destructive/10 text-destructive', )} > {pasteFeedback.type === 'success' ? ( <CheckCircle2 className="text-success size-4 shrink-0" /> ) : ( <AlertCircle className="size-4 shrink-0" /> )} <span>{pasteFeedback.message}</span> </div> )} </CardContent> </Card> )} {/* Bulk SKU Matrix Table Card */} <Card className="border-border bg-card shadow-xs"> <CardHeader className="border-border border-b pb-3"> <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle className="text-base font-semibold">Bulk SKU Order Matrix</CardTitle> <CardDescription className="text-muted-foreground text-xs"> {products.length} wholesale catalog items available with active case multipliers. </CardDescription> </div> <div className="flex items-center gap-2"> <Button variant="outline" size="xs" className="text-muted-foreground text-xs" onClick={restoreDefaultCases} > <RefreshCw className="mr-1 size-3" /> Reset Defaults </Button> <Button variant="outline" size="xs" className="text-muted-foreground text-xs" onClick={resetAllCases}> <Trash2 className="mr-1 size-3" /> Zero All </Button> </div> </div> </CardHeader> <CardContent className="p-0"> <div className="overflow-x-auto"> <Table className="w-full max-w-[760px] min-w-full"> <TableHeader> <TableRow className="bg-muted/30"> <TableHead className="w-[300px] text-xs font-semibold">Product & SKU Identifier</TableHead> <TableHead className="text-xs font-semibold">Case Multiplier</TableHead> <TableHead className="text-xs font-semibold">Unit Price</TableHead> <TableHead className="text-xs font-semibold">Case Quantity</TableHead> <TableHead className="text-xs font-semibold">Availability</TableHead> <TableHead className="text-right text-xs font-semibold">Line Total</TableHead> <TableHead className="w-[50px]"> <span className="sr-only">Actions</span> </TableHead> </TableRow> </TableHeader> <TableBody> {products.map((product) => ( <TableRow key={product.id} className={cn('transition-colors', product.cases > 0 && 'bg-primary/5 dark:bg-primary/5')} > {/* Product Thumbnail & Details */} <TableCell className="py-3.5"> <div className="flex items-center gap-3"> <img src={product.image} alt={product.name} className="border-border bg-muted/40 size-12 shrink-0 rounded-lg border object-cover" loading="lazy" /> <div className="min-w-0 space-y-0.5"> <div className="text-foreground truncate text-sm font-medium">{product.name}</div> <div className="text-muted-foreground flex items-center gap-1.5 text-xs"> <span className="font-mono">{product.sku}</span> <span>·</span> <span className="truncate">{product.variant}</span> </div> </div> </div> </TableCell> {/* Case Pack Multiplier */} <TableCell className="py-3.5"> <div className="flex items-center gap-1.5"> <Package className="text-muted-foreground size-3.5" /> <span className="text-foreground font-mono text-xs font-medium tabular-nums"> {product.casePack} units / case </span> </div> </TableCell> {/* Pricing (MSRP vs Wholesale) */} <TableCell className="py-3.5"> <div className="space-y-0.5"> <div className="text-foreground text-sm font-semibold tabular-nums"> {formatCurrency(product.wholesaleUnitPrice)} <span className="text-muted-foreground text-xs font-normal"> wholesale</span> </div> <div className="text-muted-foreground flex items-center gap-1.5 text-xs"> <span className="tabular-nums line-through"> {formatCurrency(product.msrpUnitPrice)} MSRP </span> <span className="text-success font-medium"> - {Math.round( ((product.msrpUnitPrice - product.wholesaleUnitPrice) / product.msrpUnitPrice) * 100, )} % </span> </div> </div> </TableCell> {/* Quantity Stepper Input */} <TableCell className="py-3.5"> <div className="space-y-1"> <div className="flex items-center gap-1"> <Button aria-label="Decrease cases" variant="outline" size="xs" className="size-7 p-0" disabled={product.cases <= 0} onClick={() => updateCases(product.id, -1)} > <Minus className="size-3" /> </Button> <Input value={product.cases} type="number" min={0} max={product.availableCases} className="h-7 w-16 text-center font-mono text-xs tabular-nums" onChange={(e) => setCases(product.id, e.target.value)} /> <Button aria-label="Increase cases" variant="outline" size="xs" className="size-7 p-0" disabled={product.cases >= product.availableCases} onClick={() => updateCases(product.id, 1)} > <Plus className="size-3" /> </Button> </div> <div className="text-muted-foreground font-mono text-xs tabular-nums"> {product.cases} {product.cases === 1 ? 'case' : 'cases'} ={' '} <strong className="text-foreground">{product.cases * product.casePack}</strong> units </div> </div> </TableCell> {/* Stock Status */} <TableCell className="py-3.5"> {product.availableCases > 50 ? ( <Badge variant="outline" className="border-success/30 bg-success/10 text-success text-xs font-medium" > In Stock · {formatNumber(product.availableCases)} cases </Badge> ) : ( <Badge variant="outline" className="border-warning/30 bg-warning/10 text-warning text-xs font-medium" > Low Stock · {product.availableCases} cases </Badge> )} </TableCell> {/* Line Total */} <TableCell className="py-3.5 text-right"> <div className="space-y-0.5"> <div className="text-foreground font-mono text-sm font-bold tabular-nums"> {formatCurrency(product.cases * product.casePack * product.wholesaleUnitPrice)} </div> {product.cases > 0 ? ( <div className="text-success text-success font-mono text-xs tabular-nums"> Save{' '} {formatCurrency( product.cases * product.casePack * (product.msrpUnitPrice - product.wholesaleUnitPrice), )} </div> ) : ( <div className="text-muted-foreground text-xs">0 units</div> )} </div> </TableCell> {/* Row Clear Action */} <TableCell className="py-3.5 text-center"> {product.cases > 0 && ( <Button variant="ghost" size="xs" className="text-muted-foreground hover:text-destructive size-7 p-0" title="Clear row quantity" onClick={() => clearLine(product.id)} > <Trash2 className="size-3.5" /> </Button> )} </TableCell> </TableRow> ))} </TableBody> </Table> </div> </CardContent> </Card> {/* Sticky Order Summary Footer */} <div className="bg-card/95 border-border sticky bottom-4 z-20 rounded-xl border p-4 shadow-lg backdrop-blur-md transition-colors sm:p-5"> <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> {/* Summary Totals Metrics */} <div className="grid grid-cols-2 gap-3 sm:grid-cols-4 sm:gap-6"> {/* Total Units */} <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">Total Units</div> <div className="font-mono text-base font-bold tabular-nums sm:text-lg"> {formatNumber(totalUnits)} Units </div> <div className="text-muted-foreground font-mono text-xs tabular-nums">{totalCases} Cases ordered</div> </div> {/* Wholesale Subtotal */} <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">Wholesale Subtotal</div> <div className="font-mono text-base font-bold tabular-nums sm:text-lg"> {formatCurrency(wholesaleSubtotal)} </div> <div className="text-muted-foreground text-xs">MSRP {formatCurrency(msrpTotal)}</div> </div> {/* Tier Savings */} <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">Tier Volume Savings</div> <div className="text-success text-success font-mono text-base font-bold tabular-nums sm:text-lg"> -{formatCurrency(totalSavings)} </div> <div className="text-success text-xs font-medium">{savingsPercent}% Margin Savings</div> </div> {/* Freight & Terms */} <div className="space-y-0.5"> <div className="text-muted-foreground text-xs font-medium">Freight & Terms</div> <div className="text-foreground flex items-center gap-1.5 text-xs font-semibold"> <Truck className="text-primary size-3.5 shrink-0" /> <span>{isFreeFreight ? 'Free Freight (>$10k)' : '$250 Ground'}</span> </div> <div className="text-muted-foreground text-xs">{paymentTerms}</div> </div> </div> {/* Submit & Secondary Actions */} <div className="flex flex-wrap items-center gap-2.5 pt-2 lg:pt-0"> <Button aria-label="Download attachment" variant="outline" size="sm" className="text-xs font-medium" disabled={totalCases === 0} onClick={handleExportCsv} > <Download className="mr-1.5 size-3.5" /> Export CSV </Button> <Button size="default" className="gap-2 text-xs font-semibold sm:text-sm" disabled={totalCases === 0 || isSubmitting} onClick={handleSubmitOrder} > {!isSubmitting ? <Send className="size-4" /> : <RefreshCw className="size-4 animate-spin" />} <span>{isSubmitting ? 'Processing PO...' : 'Submit Wholesale Purchase Order'}</span> </Button> </div> </div> </div> </div> ) } export default WholesaleB2bQuickOrder
Raw manifest:https://uipkge.dev/r/react/wholesale-b2b-quick-order.json