UIPackage
Menu

Framework

Change language

Boilerplate repo

Wholesale B2b Quick Order

blockcommerce

B2B wholesale bulk order matrix with volume tier pricing, case pack multipliers, and CSV SKU paste input.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/wholesale-b2b-quick-order.json
Named registry:npx shadcn-vue@latest add @uipkge/wholesale-b2b-quick-orderInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
poNumberstring'PO-2026-8841B'optional
accountNamestring'Northwind Retailers'optional
accountTierstring'Tier 3 Wholesale Partner'optional
tierDiscountPercentnumber35optional
paymentTermsstring'Net 30 Invoicing'optional
freeFreightThresholdnumber10000optional
initialProductsWholesaleProduct[]optional
classHTMLAttributes['class']optional

Files installed (1)

  • app/components/blocks/WholesaleB2bQuickOrder.vue31.4 kB
    <script lang="ts">
    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 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',
      },
    ]
    </script>
    
    <script setup lang="ts">
    import { computed, ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import {
      AlertCircle,
      Bookmark,
      Building2,
      CheckCircle2,
      Download,
      FileSpreadsheet,
      Minus,
      Package,
      Plus,
      RefreshCw,
      Send,
      Layers,
      Trash2,
      TrendingDown,
      Truck,
      Upload,
      X,
    } from 'lucide-vue-next'
    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'
    
    interface Props {
      poNumber?: string
      accountName?: string
      accountTier?: string
      tierDiscountPercent?: number
      paymentTerms?: string
      freeFreightThreshold?: number
      initialProducts?: WholesaleProduct[]
      class?: HTMLAttributes['class']
    }
    
    const props = withDefaults(defineProps<Props>(), {
      poNumber: 'PO-2026-8841B',
      accountName: 'Northwind Retailers',
      accountTier: 'Tier 3 Wholesale Partner',
      tierDiscountPercent: 35,
      paymentTerms: 'Net 30 Invoicing',
      freeFreightThreshold: 10000,
    })
    
    const emit = defineEmits<{
      submitOrder: [
        payload: {
          poNumber: string
          items: WholesaleProduct[]
          subtotal: number
          totalUnits: number
          totalCases: number
          savings: number
        },
      ]
      saveTemplate: [items: WholesaleProduct[]]
      exportCsv: [items: WholesaleProduct[]]
    }>()
    
    const products = ref<WholesaleProduct[]>(
      props.initialProducts
        ? JSON.parse(JSON.stringify(props.initialProducts))
        : JSON.parse(JSON.stringify(DEFAULT_WHOLESALE_PRODUCTS)),
    )
    
    const poRef = ref(props.poNumber)
    const showCsvBox = ref(false)
    const csvPasteText = ref('')
    const pasteFeedback = ref<{ type: 'success' | 'error'; message: string } | null>(null)
    const templateSaved = ref(false)
    const orderSubmitted = ref(false)
    const isSubmitting = ref(false)
    
    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)
    }
    
    // Computations
    const totalCases = computed(() => products.value.reduce((acc, p) => acc + (Number(p.cases) || 0), 0))
    const totalUnits = computed(() => products.value.reduce((acc, p) => acc + (Number(p.cases) || 0) * p.casePack, 0))
    const wholesaleSubtotal = computed(() =>
      products.value.reduce((acc, p) => acc + (Number(p.cases) || 0) * p.casePack * p.wholesaleUnitPrice, 0),
    )
    const msrpTotal = computed(() =>
      products.value.reduce((acc, p) => acc + (Number(p.cases) || 0) * p.casePack * p.msrpUnitPrice, 0),
    )
    const totalSavings = computed(() => Math.max(0, msrpTotal.value - wholesaleSubtotal.value))
    const savingsPercent = computed(() => {
      if (msrpTotal.value <= 0) return props.tierDiscountPercent
      return Math.round((totalSavings.value / msrpTotal.value) * 100)
    })
    
    const isFreeFreight = computed(() => wholesaleSubtotal.value >= props.freeFreightThreshold)
    const freightAmount = computed(() => (isFreeFreight.value ? 0 : 250))
    const grandTotal = computed(() => wholesaleSubtotal.value + freightAmount.value)
    
    const nextTierTarget = 50000
    const nextTierRemaining = computed(() => Math.max(0, nextTierTarget - wholesaleSubtotal.value))
    const tierProgressPercent = computed(() => Math.min(100, Math.round((wholesaleSubtotal.value / nextTierTarget) * 100)))
    
    // Actions
    const updateCases = (id: string, delta: number) => {
      const item = products.value.find((p) => p.id === id)
      if (item) {
        const current = Number(item.cases) || 0
        const next = Math.max(0, Math.min(item.availableCases, current + delta))
        item.cases = next
      }
    }
    
    const setCases = (id: string, val: string | number) => {
      const item = products.value.find((p) => p.id === id)
      if (item) {
        const parsed = parseInt(String(val), 10)
        if (isNaN(parsed) || parsed < 0) {
          item.cases = 0
        } else {
          item.cases = Math.min(item.availableCases, parsed)
        }
      }
    }
    
    const clearLine = (id: string) => {
      const item = products.value.find((p) => p.id === id)
      if (item) {
        item.cases = 0
      }
    }
    
    const resetAllCases = () => {
      products.value.forEach((p) => {
        p.cases = 0
      })
      pasteFeedback.value = null
    }
    
    const restoreDefaultCases = () => {
      products.value = JSON.parse(JSON.stringify(DEFAULT_WHOLESALE_PRODUCTS))
      pasteFeedback.value = null
    }
    
    const applyCsvPaste = () => {
      pasteFeedback.value = null
      if (!csvPasteText.value.trim()) {
        pasteFeedback.value = {
          type: 'error',
          message: 'Please paste SKU and case quantity rows to parse.',
        }
        return
      }
    
      const lines = csvPasteText.value.split(/\r?\n/)
      let updatedCount = 0
      const unknownSkus: string[] = []
    
      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 = products.value.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])
            }
          }
        }
      }
    
      if (updatedCount > 0) {
        pasteFeedback.value = {
          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 {
        pasteFeedback.value = {
          type: 'error',
          message: 'No matching catalog SKUs found. Verify format: SKU-84920, 10',
        }
      }
    }
    
    const loadSampleCsv = () => {
      csvPasteText.value = `SKU-84920, 8\nSKU-49102, 5\nSKU-77215, 4\nSKU-10934, 12\nSKU-63821, 6`
      applyCsvPaste()
    }
    
    const handleSaveTemplate = () => {
      templateSaved.value = true
      emit('saveTemplate', products.value)
      setTimeout(() => {
        templateSaved.value = false
      }, 4000)
    }
    
    const handleSubmitOrder = () => {
      if (totalCases.value === 0) return
      isSubmitting.value = true
      setTimeout(() => {
        isSubmitting.value = false
        orderSubmitted.value = true
        emit('submitOrder', {
          poNumber: poRef.value,
          items: products.value.filter((p) => p.cases > 0),
          subtotal: wholesaleSubtotal.value,
          totalUnits: totalUnits.value,
          totalCases: totalCases.value,
          savings: totalSavings.value,
        })
      }, 600)
    }
    
    const handleExportCsv = () => {
      emit('exportCsv', products.value)
      const header =
        'SKU,Product Name,Variant,Case Pack Multiplier,Cases Ordered,Total Units,Wholesale Unit Price,Line Total\n'
      const rows = products.value
        .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.value}-order-matrix.csv`)
      document.body.appendChild(downloadLink)
      downloadLink.click()
      document.body.removeChild(downloadLink)
    }
    </script>
    
    <template>
      <div data-slot="wholesale-b2b-quick-order" :class="cn('bg-background text-foreground w-full space-y-6', props.class)">
        <!-- Header & Account Info -->
        <Card class="border-border bg-card shadow-xs">
          <CardHeader class="pb-4">
            <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
              <div class="space-y-1.5">
                <div class="flex flex-wrap items-center gap-2">
                  <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                    <Building2 class="size-3.5" />
                    <span>{{ accountName }}</span>
                    <span>·</span>
                    <span>{{ accountTier }}</span>
                  </div>
                  <Badge variant="outline" class="border-success/30 bg-success/10 text-success font-medium">
                    <Layers class="mr-1 size-3" />
                    {{ tierDiscountPercent }}% Off MSRP Wholesale Tier
                  </Badge>
                </div>
                <CardTitle class="text-xl font-bold tracking-tight sm:text-2xl">
                  Wholesale & B2B Bulk Order Matrix
                </CardTitle>
                <CardDescription class="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 class="flex flex-wrap items-center gap-2">
                <Button
                  aria-label="Close CSV paste"
                  variant="outline"
                  size="sm"
                  class="gap-1.5 text-xs font-medium"
                  @click="showCsvBox = !showCsvBox"
                >
                  <FileSpreadsheet class="size-3.5" />
                  <span>{{ showCsvBox ? 'Hide CSV Paste' : 'Upload CSV Order' }}</span>
                </Button>
                <Button variant="outline" size="sm" class="gap-1.5 text-xs font-medium" @click="handleSaveTemplate">
                  <Bookmark class="size-3.5" />
                  <span>Save Order Template</span>
                </Button>
              </div>
            </div>
          </CardHeader>
    
          <CardContent class="pt-0">
            <!-- Account Terms & Partner Meta Strip -->
            <div class="bg-muted/40 border-border grid grid-cols-2 gap-3 rounded-lg border p-3 sm:grid-cols-4 sm:gap-4">
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">PO Reference</div>
                <div class="font-mono text-sm font-semibold">{{ poRef }}</div>
              </div>
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">Payment Terms</div>
                <div class="text-foreground flex items-center gap-1 text-sm font-semibold">
                  <CheckCircle2 class="text-success size-3.5" />
                  <span>{{ paymentTerms }}</span>
                </div>
              </div>
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">Freight Status</div>
                <div :class="cn('text-sm font-semibold tabular-nums', isFreeFreight ? 'text-success' : 'text-foreground')">
                  {{ isFreeFreight ? 'Free Ground Freight' : '$250 Standard Freight' }}
                </div>
              </div>
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">Volume Tier Status</div>
                <div class="text-foreground text-sm font-semibold">
                  Tier 3 <span class="text-muted-foreground font-normal">({{ tierDiscountPercent }}% Margin)</span>
                </div>
              </div>
            </div>
    
            <!-- Volume Tier Margin Incentive Banner -->
            <div class="border-border bg-card/60 mt-3 rounded-lg border p-3">
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                <div class="flex items-center gap-2">
                  <TrendingDown class="text-success size-4 shrink-0" />
                  <div class="text-xs">
                    <span class="text-foreground font-semibold">Tier 4 Milestone ($50,000):</span>
                    <span class="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 class="text-muted-foreground flex items-center gap-2 text-xs">
                  <span>{{ tierProgressPercent }}% to Tier 4</span>
                  <div class="bg-muted border-border h-2 w-24 overflow-hidden rounded-full border">
                    <div
                      class="bg-success h-full transition-[width] duration-300"
                      :style="{ width: `${tierProgressPercent}%` }"
                    />
                  </div>
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
    
        <!-- Template Saved Alert -->
        <div
          v-if="templateSaved"
          class="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 class="flex items-center gap-2">
            <Bookmark class="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
            aria-label="Dismiss notification"
            variant="ghost"
            size="xs"
            class="text-success hover:text-success"
            @click="templateSaved = false"
          >
            <X class="size-3.5" />
          </Button>
        </div>
    
        <!-- Order Submitted Success Alert -->
        <div
          v-if="orderSubmitted"
          class="border-success/30 bg-success/10 text-success dark:text-foreground rounded-xl border p-4 sm:p-5"
        >
          <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
            <div class="flex items-start gap-3">
              <CheckCircle2 class="text-success mt-0.5 size-5 shrink-0" />
              <div class="space-y-1">
                <h3 class="text-sm font-semibold sm:text-base">Purchase Order {{ poRef }} Submitted Successfully!</h3>
                <p class="text-muted-foreground text-xs sm:text-sm">
                  Your bulk order of
                  <strong class="text-foreground">{{ totalCases }} cases ({{ totalUnits }} units)</strong> totaling
                  <strong class="text-foreground">{{ formatCurrency(grandTotal) }}</strong> has been queued for warehouse
                  dispatch under {{ paymentTerms }}.
                </p>
              </div>
            </div>
            <div class="flex items-center gap-2">
              <Button aria-label="Download attachment" variant="outline" size="sm" class="text-xs" @click="handleExportCsv">
                <Download class="mr-1.5 size-3.5" />
                Download PO PDF
              </Button>
              <Button size="sm" class="text-xs" @click="orderSubmitted = false"> Start New Order </Button>
            </div>
          </div>
        </div>
    
        <!-- Quick SKU & Quantity CSV Paste Box -->
        <Card v-if="showCsvBox" class="border-border bg-card shadow-xs transition-colors duration-200">
          <CardHeader class="pb-3">
            <div class="flex items-center justify-between">
              <div class="flex items-center gap-2">
                <FileSpreadsheet class="text-primary size-4" />
                <CardTitle class="text-sm font-semibold sm:text-base"> Quick Paste SKU & Quantity Manifest </CardTitle>
              </div>
              <Button
                aria-label="Close CSV paste"
                variant="ghost"
                size="xs"
                class="text-muted-foreground hover:text-foreground"
                @click="showCsvBox = false"
              >
                <X class="size-4" />
              </Button>
            </div>
            <CardDescription class="text-muted-foreground text-xs">
              Paste CSV rows with SKU identifier and case quantity (one item per line, e.g.
              <code class="bg-muted rounded px-1 font-mono">SKU-84920, 10</code>) to auto-populate matrix.
            </CardDescription>
          </CardHeader>
    
          <CardContent class="space-y-3">
            <Textarea
              v-model="csvPasteText"
              placeholder="SKU-84920, 8&#10;SKU-49102, 5&#10;SKU-77215, 4&#10;SKU-10934, 12&#10;SKU-63821, 6"
              :rows="4"
              class="font-mono text-xs"
            />
    
            <div class="flex flex-wrap items-center justify-between gap-2">
              <div class="flex flex-wrap items-center gap-2">
                <Button size="sm" class="text-xs font-medium" @click="applyCsvPaste">
                  <Upload class="mr-1.5 size-3.5" />
                  Apply SKU List to Matrix
                </Button>
                <Button variant="outline" size="sm" class="text-xs font-medium" @click="loadSampleCsv">
                  Load Sample Data
                </Button>
                <Button
                  v-if="csvPasteText"
                  variant="ghost"
                  size="sm"
                  class="text-muted-foreground text-xs"
                  @click="csvPasteText = ''"
                >
                  Clear Input
                </Button>
              </div>
              <div class="text-muted-foreground text-xs">Supports comma, tab, and semicolon delimited lines</div>
            </div>
    
            <!-- Parse Feedback Alert -->
            <div
              v-if="pasteFeedback"
              :class="
                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',
                )
              "
            >
              <CheckCircle2 v-if="pasteFeedback.type === 'success'" class="text-success size-4 shrink-0" />
              <AlertCircle v-else class="size-4 shrink-0" />
              <span>{{ pasteFeedback.message }}</span>
            </div>
          </CardContent>
        </Card>
    
        <!-- Bulk SKU Matrix Table Card -->
        <Card class="border-border bg-card shadow-xs">
          <CardHeader class="border-border border-b pb-3">
            <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <CardTitle class="text-base font-semibold">Bulk SKU Order Matrix</CardTitle>
                <CardDescription class="text-muted-foreground text-xs">
                  {{ products.length }} wholesale catalog items available with active case multipliers.
                </CardDescription>
              </div>
              <div class="flex items-center gap-2">
                <Button variant="outline" size="xs" class="text-muted-foreground text-xs" @click="restoreDefaultCases">
                  <RefreshCw class="mr-1 size-3" />
                  Reset Defaults
                </Button>
                <Button variant="outline" size="xs" class="text-muted-foreground text-xs" @click="resetAllCases">
                  <Trash2 class="mr-1 size-3" />
                  Zero All
                </Button>
              </div>
            </div>
          </CardHeader>
    
          <CardContent class="p-0">
            <div class="overflow-x-auto">
              <Table class="w-full max-w-[760px] min-w-full">
                <TableHeader>
                  <TableRow class="bg-muted/30">
                    <TableHead class="w-[300px] text-xs font-semibold">Product & SKU Identifier</TableHead>
                    <TableHead class="text-xs font-semibold">Case Multiplier</TableHead>
                    <TableHead class="text-xs font-semibold">Unit Price</TableHead>
                    <TableHead class="text-xs font-semibold">Case Quantity</TableHead>
                    <TableHead class="text-xs font-semibold">Availability</TableHead>
                    <TableHead class="text-right text-xs font-semibold">Line Total</TableHead>
                    <TableHead class="w-[50px]">
                      <span class="sr-only">Actions</span>
                    </TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  <TableRow
                    v-for="product in products"
                    :key="product.id"
                    :class="cn('transition-colors', product.cases > 0 && 'bg-primary/5 dark:bg-primary/5')"
                  >
                    <!-- Product Thumbnail & Details -->
                    <TableCell class="py-3.5">
                      <div class="flex items-center gap-3">
                        <img
                          :src="product.image"
                          :alt="product.name"
                          class="border-border bg-muted/40 size-12 shrink-0 rounded-lg border object-cover"
                          loading="lazy"
                        />
                        <div class="min-w-0 space-y-0.5">
                          <div class="text-foreground truncate text-sm font-medium">
                            {{ product.name }}
                          </div>
                          <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                            <span class="font-mono">{{ product.sku }}</span>
                            <span>·</span>
                            <span class="truncate">{{ product.variant }}</span>
                          </div>
                        </div>
                      </div>
                    </TableCell>
    
                    <!-- Case Pack Multiplier -->
                    <TableCell class="py-3.5">
                      <div class="flex items-center gap-1.5">
                        <Package class="text-muted-foreground size-3.5" />
                        <span class="text-foreground font-mono text-xs font-medium tabular-nums">
                          {{ product.casePack }} units / case
                        </span>
                      </div>
                    </TableCell>
    
                    <!-- Pricing (MSRP vs Wholesale) -->
                    <TableCell class="py-3.5">
                      <div class="space-y-0.5">
                        <div class="text-foreground text-sm font-semibold tabular-nums">
                          {{ formatCurrency(product.wholesaleUnitPrice) }}
                          <span class="text-muted-foreground text-xs font-normal">wholesale</span>
                        </div>
                        <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                          <span class="tabular-nums line-through">{{ formatCurrency(product.msrpUnitPrice) }} MSRP</span>
                          <span class="text-success font-medium">
                            -{{
                              Math.round(
                                ((product.msrpUnitPrice - product.wholesaleUnitPrice) / product.msrpUnitPrice) * 100,
                              )
                            }}%
                          </span>
                        </div>
                      </div>
                    </TableCell>
    
                    <!-- Quantity Stepper Input -->
                    <TableCell class="py-3.5">
                      <div class="space-y-1">
                        <div class="flex items-center gap-1">
                          <Button
                            aria-label="Decrease cases"
                            variant="outline"
                            size="xs"
                            class="size-7 p-0"
                            :disabled="product.cases <= 0"
                            @click="updateCases(product.id, -1)"
                          >
                            <Minus class="size-3" />
                          </Button>
                          <Input
                            :model-value="product.cases"
                            type="number"
                            min="0"
                            :max="product.availableCases"
                            class="h-7 w-16 text-center font-mono text-xs tabular-nums"
                            @update:model-value="(val) => setCases(product.id, val)"
                          />
                          <Button
                            aria-label="Increase cases"
                            variant="outline"
                            size="xs"
                            class="size-7 p-0"
                            :disabled="product.cases >= product.availableCases"
                            @click="updateCases(product.id, 1)"
                          >
                            <Plus class="size-3" />
                          </Button>
                        </div>
                        <div class="text-muted-foreground font-mono text-xs tabular-nums">
                          {{ product.cases }} {{ product.cases === 1 ? 'case' : 'cases' }} =
                          <strong class="text-foreground">{{ product.cases * product.casePack }}</strong> units
                        </div>
                      </div>
                    </TableCell>
    
                    <!-- Stock Status -->
                    <TableCell class="py-3.5">
                      <Badge
                        v-if="product.availableCases > 50"
                        variant="outline"
                        class="border-success/30 bg-success/10 text-success text-xs font-medium"
                      >
                        In Stock · {{ formatNumber(product.availableCases) }} cases
                      </Badge>
                      <Badge
                        v-else
                        variant="outline"
                        class="border-warning/30 bg-warning/10 text-warning text-xs font-medium"
                      >
                        Low Stock · {{ product.availableCases }} cases
                      </Badge>
                    </TableCell>
    
                    <!-- Line Total -->
                    <TableCell class="py-3.5 text-right">
                      <div class="space-y-0.5">
                        <div class="text-foreground font-mono text-sm font-bold tabular-nums">
                          {{ formatCurrency(product.cases * product.casePack * product.wholesaleUnitPrice) }}
                        </div>
                        <div v-if="product.cases > 0" class="text-success text-success font-mono text-xs tabular-nums">
                          Save
                          {{
                            formatCurrency(
                              product.cases * product.casePack * (product.msrpUnitPrice - product.wholesaleUnitPrice),
                            )
                          }}
                        </div>
                        <div v-else class="text-muted-foreground text-xs">0 units</div>
                      </div>
                    </TableCell>
    
                    <!-- Row Clear Action -->
                    <TableCell class="py-3.5 text-center">
                      <Button
                        v-if="product.cases > 0"
                        variant="ghost"
                        size="xs"
                        class="text-muted-foreground hover:text-destructive size-7 p-0"
                        title="Clear row quantity"
                        @click="clearLine(product.id)"
                      >
                        <Trash2 class="size-3.5" />
                      </Button>
                    </TableCell>
                  </TableRow>
                </TableBody>
              </Table>
            </div>
          </CardContent>
        </Card>
    
        <!-- Sticky Order Summary Footer -->
        <div
          class="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 class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <!-- Summary Totals Metrics -->
            <div class="grid grid-cols-2 gap-3 sm:grid-cols-4 sm:gap-6">
              <!-- Total Units -->
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">Total Units</div>
                <div class="font-mono text-base font-bold tabular-nums sm:text-lg">
                  {{ formatNumber(totalUnits) }} Units
                </div>
                <div class="text-muted-foreground font-mono text-xs tabular-nums">{{ totalCases }} Cases ordered</div>
              </div>
    
              <!-- Wholesale Subtotal -->
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">Wholesale Subtotal</div>
                <div class="font-mono text-base font-bold tabular-nums sm:text-lg">
                  {{ formatCurrency(wholesaleSubtotal) }}
                </div>
                <div class="text-muted-foreground text-xs">MSRP {{ formatCurrency(msrpTotal) }}</div>
              </div>
    
              <!-- Tier Savings -->
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">Tier Volume Savings</div>
                <div class="text-success text-success font-mono text-base font-bold tabular-nums sm:text-lg">
                  -{{ formatCurrency(totalSavings) }}
                </div>
                <div class="text-success text-xs font-medium">{{ savingsPercent }}% Margin Savings</div>
              </div>
    
              <!-- Freight & Terms -->
              <div class="space-y-0.5">
                <div class="text-muted-foreground text-xs font-medium">Freight & Terms</div>
                <div class="text-foreground flex items-center gap-1.5 text-xs font-semibold">
                  <Truck class="text-primary size-3.5 shrink-0" />
                  <span>{{ isFreeFreight ? 'Free Freight (>$10k)' : '$250 Ground' }}</span>
                </div>
                <div class="text-muted-foreground text-xs">
                  {{ paymentTerms }}
                </div>
              </div>
            </div>
    
            <!-- Submit & Secondary Actions -->
            <div class="flex flex-wrap items-center gap-2.5 pt-2 lg:pt-0">
              <Button
                aria-label="Download attachment"
                variant="outline"
                size="sm"
                class="text-xs font-medium"
                :disabled="totalCases === 0"
                @click="handleExportCsv"
              >
                <Download class="mr-1.5 size-3.5" />
                Export CSV
              </Button>
    
              <Button
                size="default"
                class="gap-2 text-xs font-semibold sm:text-sm"
                :disabled="totalCases === 0 || isSubmitting"
                @click="handleSubmitOrder"
              >
                <Send v-if="!isSubmitting" class="size-4" />
                <RefreshCw v-else class="size-4 animate-spin" />
                <span>{{ isSubmitting ? 'Processing PO...' : 'Submit Wholesale Purchase Order' }}</span>
              </Button>
            </div>
          </div>
        </div>
      </div>
    </template>
    

Raw manifest:https://uipkge.dev/r/vue/wholesale-b2b-quick-order.json