Past orders
The whole book, newest first — searchable by account, filterable by status and region, sortable and paged; reads pastOrders().
Preview
"use client"
import * as React from "react"
import { ArrowRightIcon, PrinterIcon, RotateCcwIcon } from "lucide-react"
import {
DataTable,
DataTableColumnHeader,
DataTableRowActions,
type DataTableColumnDef,
type DataTableFacet,
} from "@/components/ui/data-table"
import { StatusBadge, type StatusVariant } from "@/components/ui/status-badge"
import { CurrencyCell, DateCell, NumberCell, TruncateCell } from "@/components/ui/table-cells"
import { Widget } from "@/components/ui/widget"
import { type OrderRow } from "../data"
// StatusBadge has no default for "fulfilled" or "refunded"; the other three
// resolve on their own and are named here so the whole vocabulary reads in
// one place.
const STATUS_MAP: Record<string, StatusVariant> = {
fulfilled: "success",
paid: "success",
pending: "warning",
refunded: "danger",
cancelled: "neutral",
}
// Every way an order is paid, the till's cash and vouchers too: its sales reach
// this table.
const PAYMENT_LABELS: Record<OrderRow["payment"], string> = {
card: "Card",
bank: "Bank",
wallet: "Wallet",
cash: "Cash",
voucher: "Voucher",
}
const FACETS: DataTableFacet<OrderRow>[] = [
{
columnId: "status",
title: "Status",
options: Object.keys(STATUS_MAP).map((status) => ({
label: status.charAt(0).toUpperCase() + status.slice(1),
value: status,
})),
},
{
columnId: "region",
title: "Region",
options: ["Americas", "EMEA", "APAC"].map((region) => ({ label: region, value: region })),
},
]
export function PastOrders({ rows }: { rows: OrderRow[] }) {
// The title names the table too, so a screen reader announces it by name.
const titleId = React.useId()
const columns = React.useMemo<DataTableColumnDef<OrderRow>[]>(
() => [
{
accessorKey: "number",
header: ({ column }) => <DataTableColumnHeader column={column} title="Order" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.number}</span>,
meta: { label: "Order" },
},
{
accessorKey: "account",
header: ({ column }) => <DataTableColumnHeader column={column} title="Account" />,
cell: ({ row }) => <TruncateCell maxWidth={200}>{row.original.account}</TruncateCell>,
meta: { label: "Account" },
},
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => <StatusBadge status={row.original.status} map={STATUS_MAP} />,
meta: { label: "Status" },
},
{
accessorKey: "payment",
header: ({ column }) => <DataTableColumnHeader column={column} title="Payment" />,
cell: ({ row }) => (
<span className="text-muted-foreground">{PAYMENT_LABELS[row.original.payment]}</span>
),
meta: { label: "Payment" },
},
{
accessorKey: "region",
header: ({ column }) => <DataTableColumnHeader column={column} title="Region" />,
cell: ({ row }) => <span className="text-muted-foreground">{row.original.region}</span>,
meta: { label: "Region" },
},
{
accessorKey: "items",
header: ({ column }) => <DataTableColumnHeader column={column} title="Items" />,
cell: ({ row }) => <NumberCell value={row.original.items} />,
meta: { align: "right", label: "Items" },
},
{
accessorKey: "total",
header: ({ column }) => <DataTableColumnHeader column={column} title="Total" />,
cell: ({ row }) => <CurrencyCell value={row.original.total} />,
meta: { align: "right", label: "Total" },
},
{
accessorKey: "placedAt",
header: ({ column }) => <DataTableColumnHeader column={column} title="Placed" />,
cell: ({ row }) => (
<DateCell date={row.original.placedAt} className="text-muted-foreground" />
),
meta: { align: "right", label: "Placed" },
},
{
id: "actions",
size: 44,
enableSorting: false,
enableHiding: false,
cell: ({ row }) => (
<DataTableRowActions
label={`Open menu for ${row.original.number}`}
actions={[
{ label: "Open order", icon: <ArrowRightIcon />, onSelect: () => {} },
{ label: "Print packing slip", icon: <PrinterIcon />, onSelect: () => {} },
{
label: "Start a return",
icon: <RotateCcwIcon />,
destructive: true,
separatorBefore: true,
onSelect: () => {},
},
]}
/>
),
},
],
[]
)
return (
<Widget titleId={titleId} data-widget="widget-ecommerce-orders-overview-past-orders" title="Past orders" description="The whole book, newest first">
<DataTable
aria-labelledby={titleId}
size="sm"
columns={columns}
data={rows}
pageSize={8}
searchKey="account"
searchPlaceholder="Search accounts…"
facets={FACETS}
enableRowSelection={false}
getRowId={(order) => order.id}
initialSorting={[{ id: "placedAt", desc: true }]}
/>
</Widget>
)
}Install
$
npx shadcn@latest add @vibra/widget-ecommerce-orders-overview-past-ordersNeeds the @vibra registry in your components.json — set it up once.
Source
"use client"
import * as React from "react"
import { ArrowRightIcon, PrinterIcon, RotateCcwIcon } from "lucide-react"
import {
DataTable,
DataTableColumnHeader,
DataTableRowActions,
type DataTableColumnDef,
type DataTableFacet,
} from "@/components/ui/data-table"
import { StatusBadge, type StatusVariant } from "@/components/ui/status-badge"
import { CurrencyCell, DateCell, NumberCell, TruncateCell } from "@/components/ui/table-cells"
import { Widget } from "@/components/ui/widget"
import { type OrderRow } from "../data"
// StatusBadge has no default for "fulfilled" or "refunded"; the other three
// resolve on their own and are named here so the whole vocabulary reads in
// one place.
const STATUS_MAP: Record<string, StatusVariant> = {
fulfilled: "success",
paid: "success",
pending: "warning",
refunded: "danger",
cancelled: "neutral",
}
// Every way an order is paid, the till's cash and vouchers too: its sales reach
// this table.
const PAYMENT_LABELS: Record<OrderRow["payment"], string> = {
card: "Card",
bank: "Bank",
wallet: "Wallet",
cash: "Cash",
voucher: "Voucher",
}
const FACETS: DataTableFacet<OrderRow>[] = [
{
columnId: "status",
title: "Status",
options: Object.keys(STATUS_MAP).map((status) => ({
label: status.charAt(0).toUpperCase() + status.slice(1),
value: status,
})),
},
{
columnId: "region",
title: "Region",
options: ["Americas", "EMEA", "APAC"].map((region) => ({ label: region, value: region })),
},
]
export function PastOrders({ rows }: { rows: OrderRow[] }) {
// The title names the table too, so a screen reader announces it by name.
const titleId = React.useId()
const columns = React.useMemo<DataTableColumnDef<OrderRow>[]>(
() => [
{
accessorKey: "number",
header: ({ column }) => <DataTableColumnHeader column={column} title="Order" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.number}</span>,
meta: { label: "Order" },
},
{
accessorKey: "account",
header: ({ column }) => <DataTableColumnHeader column={column} title="Account" />,
cell: ({ row }) => <TruncateCell maxWidth={200}>{row.original.account}</TruncateCell>,
meta: { label: "Account" },
},
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => <StatusBadge status={row.original.status} map={STATUS_MAP} />,
meta: { label: "Status" },
},
{
accessorKey: "payment",
header: ({ column }) => <DataTableColumnHeader column={column} title="Payment" />,
cell: ({ row }) => (
<span className="text-muted-foreground">{PAYMENT_LABELS[row.original.payment]}</span>
),
meta: { label: "Payment" },
},
{
accessorKey: "region",
header: ({ column }) => <DataTableColumnHeader column={column} title="Region" />,
cell: ({ row }) => <span className="text-muted-foreground">{row.original.region}</span>,
meta: { label: "Region" },
},
{
accessorKey: "items",
header: ({ column }) => <DataTableColumnHeader column={column} title="Items" />,
cell: ({ row }) => <NumberCell value={row.original.items} />,
meta: { align: "right", label: "Items" },
},
{
accessorKey: "total",
header: ({ column }) => <DataTableColumnHeader column={column} title="Total" />,
cell: ({ row }) => <CurrencyCell value={row.original.total} />,
meta: { align: "right", label: "Total" },
},
{
accessorKey: "placedAt",
header: ({ column }) => <DataTableColumnHeader column={column} title="Placed" />,
cell: ({ row }) => (
<DateCell date={row.original.placedAt} className="text-muted-foreground" />
),
meta: { align: "right", label: "Placed" },
},
{
id: "actions",
size: 44,
enableSorting: false,
enableHiding: false,
cell: ({ row }) => (
<DataTableRowActions
label={`Open menu for ${row.original.number}`}
actions={[
{ label: "Open order", icon: <ArrowRightIcon />, onSelect: () => {} },
{ label: "Print packing slip", icon: <PrinterIcon />, onSelect: () => {} },
{
label: "Start a return",
icon: <RotateCcwIcon />,
destructive: true,
separatorBefore: true,
onSelect: () => {},
},
]}
/>
),
},
],
[]
)
return (
<Widget titleId={titleId} data-widget="widget-ecommerce-orders-overview-past-orders" title="Past orders" description="The whole book, newest first">
<DataTable
aria-labelledby={titleId}
size="sm"
columns={columns}
data={rows}
pageSize={8}
searchKey="account"
searchPlaceholder="Search accounts…"
facets={FACETS}
enableRowSelection={false}
getRowId={(order) => order.id}
initialSorting={[{ id: "placedAt", desc: true }]}
/>
</Widget>
)
}/**
* What /orders/overview reads. Everything on the page is an aggregate over
* `db.orders`, joined to `db.customers` for the account behind a row. Two
* things the rows do not carry are rules over them: the sales region a country
* belongs to, and the month's target — every KPI is measured against the month
* before it plus 5%, rounded the way a target gets written down. "Now" is
* REFERENCE_DATE, so the closed month is always the same one.
*/
import { getInitials } from "@/lib/format"
import { db, REFERENCE_DATE, type Member, type Order } from "@/lib/sample-data"
// Read per call, never held: a sale rung up or an order refunded since the
// server started is in the book the next time the page asks.
const orders = (): Order[] => db.orders.all()
const dollars = (cents: number): number => cents / 100
/** Midnight UTC on the first of the month `k` months before the current one. */
function monthStart(k: number): Date {
return new Date(Date.UTC(REFERENCE_DATE.getUTCFullYear(), REFERENCE_DATE.getUTCMonth() - k, 1))
}
const MONTH_END = monthStart(0)
const MONTH_START = monthStart(1)
const PRIOR_START = monthStart(2)
const MONTH_NAME = new Intl.DateTimeFormat("en-US", { month: "long", timeZone: "UTC" })
/** The last complete month, which is the month every KPI is read for. */
export const CLOSED_MONTH = MONTH_NAME.format(MONTH_START)
/** The month the targets were set from. */
export const PRIOR_MONTH = MONTH_NAME.format(PRIOR_START)
// A cancelled order was never a sale, so it counts towards nothing but the
// status split at the bottom of the page.
const billable = (order: Order): boolean => order.status !== "cancelled"
function placedIn(from: Date, to: Date): Order[] {
return orders().filter((order) => order.placedAt >= from && order.placedAt < to)
}
const revenue = (rows: Order[]): number =>
dollars(rows.reduce((total, order) => total + order.totalCents, 0))
const fulfilled = (rows: Order[]): number =>
rows.length > 0 ? rows.filter((order) => order.status === "fulfilled").length / rows.length : 0
export type Kpi = {
key: string
label: string
value: number
target: number
/** Ascending band edges for the bullet track: behind, on track, ahead. */
ranges: number[]
/** Change against the month before, as a ratio. */
delta: number
format: "number" | "currency" | "percent"
}
/**
* The target every KPI is read against: the month before it, 5% higher, rounded
* to `step`. One rule for all four, so nothing on the row is hand-set.
*/
function targetFrom(previous: number, step: number): number {
return Math.round((previous * 1.05) / step) * step
}
// Read against the target rather than the axis: behind at 80%, on track at
// 100%, ahead past it. The bands are shares of the target, so they mean the
// same thing on a count, a dollar amount and a rate.
const bands = (target: number): number[] => [target * 0.8, target, target * 1.25]
/** The four headline numbers for the closed month, each against its target. */
export function kpis(): Kpi[] {
const month = placedIn(MONTH_START, MONTH_END).filter(billable)
const prior = placedIn(PRIOR_START, MONTH_START).filter(billable)
const count = month.length
const priorCount = prior.length
const money = revenue(month)
const priorMoney = revenue(prior)
const aov = count > 0 ? money / count : 0
const priorAov = priorCount > 0 ? priorMoney / priorCount : 0
const rate = fulfilled(month)
const priorRate = fulfilled(prior)
const rows: [string, string, number, number, number, Kpi["format"]][] = [
["orders", "Orders", count, priorCount, 5, "number"],
["revenue", "Revenue", money, priorMoney, 1_000, "currency"],
["aov", "Average order", aov, priorAov, 5, "currency"],
["fulfilment", "Fulfilled", rate, priorRate, 0.01, "percent"],
]
return rows.map(([key, label, value, previous, step, format]) => {
const target = targetFrom(previous, step)
return {
key,
label,
value,
target,
ranges: bands(target),
delta: previous > 0 ? value / previous - 1 : 0,
format,
}
})
}
// Weekday names in the order a week is read, not the order getUTCDay returns.
const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
/** How the whole order book falls across the days of the week. */
export function ordersByWeekday(): { day: string; orders: number }[] {
const counts = new Array<number>(WEEKDAYS.length).fill(0)
for (const order of orders()) {
if (!billable(order)) continue
// getUTCDay is Sunday-first; the chart is Monday-first.
counts[(order.placedAt.getUTCDay() + 6) % 7] += 1
}
return WEEKDAYS.map((day, index) => ({ day, orders: counts[index] }))
}
// Which sales region a country belongs to. The order carries a country, not a
// region, so the grouping is a rule over the row.
const REGIONS: Record<string, string> = {
"United States": "Americas",
Canada: "Americas",
Brazil: "Americas",
"United Kingdom": "EMEA",
Germany: "EMEA",
France: "EMEA",
Netherlands: "EMEA",
Sweden: "EMEA",
Spain: "EMEA",
Portugal: "EMEA",
Ireland: "EMEA",
Australia: "APAC",
Japan: "APAC",
India: "APAC",
}
const REGION_ORDER = ["Americas", "EMEA", "APAC"]
/** The sales region an order shipped to. */
export function regionOf(country: string): string {
return REGIONS[country] ?? "Rest of world"
}
/** Booked value per region across the whole book, largest first. */
export function revenueByRegion(): { name: string; value: number }[] {
const totals = new Map<string, number>()
for (const order of orders()) {
if (!billable(order)) continue
const region = regionOf(order.country)
totals.set(region, (totals.get(region) ?? 0) + order.totalCents)
}
return REGION_ORDER.filter((region) => totals.has(region))
.map((region) => ({ name: region, value: Math.round(dollars(totals.get(region) ?? 0)) }))
.sort((a, b) => b.value - a.value)
}
/** Everything the book is worth, for the middle of the donut. */
export function bookedTotal(): number {
return revenueByRegion().reduce((total, region) => total + region.value, 0)
}
// The five states an order can be in, in the order they read as progress.
const STATUSES: Order["status"][] = ["fulfilled", "paid", "pending", "refunded", "cancelled"]
/** How the whole book splits across the five order states. */
export function fulfilmentSplit(): { label: string; value: number; status: Order["status"] }[] {
const book = orders()
return STATUSES.map((status) => ({
status,
label: status.charAt(0).toUpperCase() + status.slice(1),
value: book.filter((order) => order.status === status).length,
}))
}
export type OrderRow = {
id: string
number: string
account: string
status: Order["status"]
payment: Order["paymentMethod"]
region: string
country: string
items: number
total: number
placedAt: Date
}
/** The order book as a table, newest first. */
export function pastOrders(): OrderRow[] {
const accounts = new Map(db.customers.all().map((customer) => [customer.id, customer]))
return orders().map((order) => ({
id: order.id,
number: order.number,
// A sale the till rang up for nobody in particular has no account.
account: accounts.get(order.customerId)?.company ?? (order.customerId || "Walk-in"),
status: order.status,
payment: order.paymentMethod,
region: regionOf(order.country),
country: order.country,
items: order.items.reduce((count, item) => count + item.qty, 0),
total: dollars(order.totalCents),
placedAt: order.placedAt,
}))
.sort((a, b) => b.placedAt.getTime() - a.placedAt.getTime())
.slice(0, 60)
}
/** 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 }))
}
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 }
}
/** Which month the KPIs closed on, and what set their targets. */
export function lastUpdated(): string {
return `${CLOSED_MONTH} ${MONTH_START.getUTCFullYear()} · targets set from ${PRIOR_MONTH}`
}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 Orders dashboard page