Skip to contentVibraUI

Orders holding the product

The newest orders that hold the product, whatever became of them, each with its customer, quantity and line total; reads productRecord(id).

Preview

Install

npx shadcn@latest add @vibra/widget-ecommerce-product-product-orders

Needs the @vibra registry in your components.json — set it up once.

Source

app/ecommerce/products/[id]/components/product-orders.tsx
import Link from "next/link"

import { ORDER_STATUS_MAP, orderHref } from "@/lib/dashboards/ecommerce/vocabulary"
import { formatCurrency, formatDate, formatNumber } from "@/lib/format"
import { SimpleTable } from "@/components/ui/simple-table"
import { StatusBadge } from "@/components/ui/status-badge"
import { UserCell } from "@/components/ui/user-cell"
import { Widget } from "@/components/ui/widget"

import { type HoldingOrder } from "../data"

/** How many of the newest orders the card lists before it counts the rest. */
const SHOWN = 8

/**
 * The orders that hold the product, newest first, whatever became of them:
 * the customer with their face, the quantity on the line and what the line
 * charged at the time. Each order number opens its own page.
 */
export function ProductOrders({ orders }: { orders: HoldingOrder[] }) {
  const shown = orders.slice(0, SHOWN)
  const count = `${formatNumber(orders.length)} order${orders.length === 1 ? "" : "s"}`

  return (
    <Widget
      data-widget="widget-ecommerce-product-product-orders"
      title="Orders"
      description={`${count} hold this product`}
      footer={orders.length > SHOWN ? `The ${SHOWN} newest of ${count}` : undefined}
      contentClassName="p-0"
      className="h-full"
    >
      <SimpleTable
        size="sm"
        rows={shown}
        rowKey="id"
        emptyMessage="No order has held this product yet."
        className="rounded-none border-0"
        columns={[
          {
            key: "number",
            header: "Order",
            cell: (order) => (
              <Link
                href={orderHref(order.id)}
                className="rounded-sm font-mono text-xs underline-offset-4 focus-ring hover:underline"
              >
                {order.number}
              </Link>
            ),
          },
          {
            key: "customer",
            header: "Customer",
            cell: (order) => (
              <UserCell
                size="sm"
                name={order.customer.name}
                description={order.customer.company}
                src={order.customer.avatarUrl}
              />
            ),
          },
          {
            key: "placedAt",
            header: "Placed",
            cell: (order) => (
              <time dateTime={order.placedAt.toISOString()} className="whitespace-nowrap text-muted-foreground">
                {formatDate(order.placedAt, "medium", { timeZone: "UTC" })}
              </time>
            ),
          },
          {
            key: "status",
            header: "Status",
            cell: (order) => <StatusBadge status={order.status} map={ORDER_STATUS_MAP} />,
          },
          { key: "qty", header: "Qty", align: "right" },
          {
            key: "lineTotal",
            header: "Line total",
            align: "right",
            cell: (order) => formatCurrency(order.lineTotal),
          },
        ]}
      />
    </Widget>
  )
}
app/ecommerce/products/[id]/data.ts
/**
 * What one product's page reads. The product is a `db.products` row; what it
 * sold is read off the lines of `db.orders`, because the catalogue row keeps
 * no sales of its own, by the dashboard's own rule for a sale
 * (`salesBetween`): paid and fulfilled orders, so a cancelled cart and a
 * refund are not sales, while every order that holds the product is listed
 * whatever became of it. "Now" is `REFERENCE_DATE`; nothing here reads a clock.
 */
import { salesBetween } from "@/lib/dashboards/ecommerce/vocabulary"
import { formatDate, getInitials } from "@/lib/format"
import { db, REFERENCE_DATE, type Member, type Order, type Product } from "@/lib/sample-data"

const DAY_MS = 86_400_000
const WEEK_MS = 7 * DAY_MS

/** How many weeks the chart and the headline sales figures cover. */
export const WEEKS = 12
const SPAN_MS = WEEKS * WEEK_MS

/**
 * The product the page falls back to when it is rendered with no route param —
 * which is what the docs preview does: the active product that sold the most
 * units in the last twelve weeks, because a product page is worth looking at
 * when its chart has something on it. Read per request, like every row here.
 */
export function fallbackId(): string {
  const now = REFERENCE_DATE.getTime()
  const sales = salesBetween(db.orders.all(), now - SPAN_MS, now)
  const products = db.products.all()
  const active = products.filter((product) => product.status === "active")
  let best = active[0] ?? products[0]
  let most = -1
  for (const product of active) {
    const units = sales.get(product.id)?.units ?? 0
    if (units > most) {
      best = product
      most = units
    }
  }
  return best.id
}

/** Every product id, for `generateStaticParams`. */
export function productIds(): string[] {
  return db.products.all().map((product) => product.id)
}

/** One week of the chart: its first day, and the units sold in it. */
export type WeekPoint = { week: string; units: number }

/** One order that holds the product, with the line that holds it. */
export type HoldingOrder = {
  id: string
  number: string
  status: Order["status"]
  placedAt: Date
  customer: { name: string; company: string; avatarUrl?: string }
  qty: number
  /** What the line charged, in whole dollars — the price at the time, not today's. */
  lineTotal: number
}

/** The product and everything the page says about it, gathered in one read. */
export type ProductRecord = {
  product: Product
  /** The twelve weeks, oldest first, the last ending at "now". */
  weeks: WeekPoint[]
  sales: { units: number; revenue: number }
  /** The same figures over the twelve weeks before, for the pills. */
  previous: { units: number; revenue: number }
  orders: HoldingOrder[]
}

/** Midnight UTC on the day `ms` falls in. */
const dayOf = (ms: number) => {
  const date = new Date(ms)
  return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
}

type WeekSales = { from: number; units: number; cents: number }

/**
 * One product's sales week by week, over the twelve weeks ending `offset`
 * spans before "now", oldest first. The chart draws these weeks and the
 * headline figures are their sums, so the bars always add up to the tile.
 */
function weeklySales(productId: string, offset: number): WeekSales[] {
  const end = REFERENCE_DATE.getTime() - offset * SPAN_MS
  const orders = db.orders.all()
  return Array.from({ length: WEEKS }, (_, index) => {
    const from = end - (WEEKS - index) * WEEK_MS
    const sale = salesBetween(orders, from, from + WEEK_MS).get(productId)
    return { from, units: sale?.units ?? 0, cents: sale?.cents ?? 0 }
  })
}

/** A run of weeks summed into the headline's units and revenue. */
function summed(weeks: WeekSales[]): { units: number; revenue: number } {
  return {
    units: weeks.reduce((sum, week) => sum + week.units, 0),
    revenue: weeks.reduce((sum, week) => sum + week.cents, 0) / 100,
  }
}

/**
 * The whole record, or undefined when the id names no product. The orders are
 * every one that holds it, newest first, whatever became of them — the list is
 * the product's history, and a refund is part of that.
 */
export async function productRecord(id: string): Promise<ProductRecord | undefined> {
  const product = await db.products.get(id)
  if (!product) return undefined

  const customers = new Map(db.customers.all().map((customer) => [customer.id, customer]))
  const orders = db.orders
    .all()
    .filter((order) => order.items.some((item) => item.productId === id))
    .sort((a, b) => b.placedAt.getTime() - a.placedAt.getTime())
    .map((order): HoldingOrder => {
      const line = order.items.find((item) => item.productId === id)!
      const customer = customers.get(order.customerId)
      return {
        id: order.id,
        number: order.number,
        status: order.status,
        placedAt: order.placedAt,
        customer: {
          // A sale the till rang up for nobody in particular has no customer.
          name: customer?.name ?? (order.customerId ? "Unknown customer" : "Walk-in"),
          company: customer?.company ?? "—",
          avatarUrl: customer?.avatarUrl,
        },
        qty: line.qty,
        lineTotal: (line.qty * line.unitCents) / 100,
      }
    })

  const weeks = weeklySales(id, 0)
  return {
    product,
    weeks: weeks.map((week) => ({
      week: new Date(dayOf(week.from + DAY_MS)).toISOString().slice(0, 10),
      units: week.units,
    })),
    sales: summed(weeks),
    previous: summed(weeklySales(id, 1)),
    orders,
  }
}

/** The freshness line under the title, measured against REFERENCE_DATE. */
export function lastUpdated(): string {
  return `Synced ${formatDate(REFERENCE_DATE, "medium", { timeZone: "UTC" })}`
}

function ownerRow(): Member {
  return db.members.all().find((member) => member.role === "owner") ?? db.members.all()[0]
}

/** The person looking at the page: whoever owns this workspace. */
export function currentUser() {
  const owner = ownerRow()
  return { name: owner.name, email: owner.email, initials: getInitials(owner.name), avatarUrl: owner.avatarUrl }
}

/** The bell's contents: the newest notifications, unread first in the panel. */
export function shellNotifications() {
  return db.notifications
    .all()
    .sort((a, b) => b.at.getTime() - a.at.getTime())
    .slice(0, 6)
    .map(({ id, title, description, at, read, href }) => ({ id, title, description, at, read, href }))
}

Its page

On its page the card sits among the rest of the dashboard and shares its range and its data with them.

From the Product record page