Skip to contentVibraUI

Past orders

The whole book, newest first — searchable by account, filterable by status and region, sortable and paged; reads pastOrders().

Preview

Install

npx shadcn@latest add @vibra/widget-ecommerce-orders-overview-past-orders

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

Source

app/ecommerce/orders/overview/components/past-orders.tsx
"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>
  )
}
app/ecommerce/orders/overview/data.ts
/**
 * 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