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
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>
)
}Install
$
npx shadcn@latest add @vibra/widget-ecommerce-product-product-ordersNeeds the @vibra registry in your components.json — set it up once.
Source
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>
)
}/**
* 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