Skip to contentVibraUI
Part of the E-commerce dashboardinstalls at /ecommerce/orders/overview

Orders dashboard

An order operations page: four KPIs read against their own targets on bullet tracks, the week's volume by weekday, booked value by region, where the whole book stands as one bar, and every past order in a filterable table.

Open the live page

A server component inside AppShell: it aggregates db.orders once, joins db.customers for the account behind a row, and hands plain rows to the client islands that draw them. Every KPI is measured against one rule — the month before it plus 5%, rounded the way a target gets written down — and each card carries three readings that never repeat each other: the headline, a bullet track against the target, and a MetricDelta against the month before. Two things the rows do not carry are rules over them: the sales region a country belongs to, and that target. This block's route is nested, so nav.ts gives Orders a disclosure with a leaf for each of its routes and the leaf carries aria-current. Composes AppShell, PageHeader, StatCardGroup, StatCard, BulletChart, MetricDelta, DashboardGrid, ChartCard, BarChart, DonutChart, Widget, PercentageBar, DataTable, StatusBadge, CurrencyCell, NumberCell, DateCell and TruncateCell.

Preview

Install

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

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

Source

app/ecommerce/orders/overview/page.tsx
import { REFERENCE_DATE } from "@/lib/sample-data"
import { AppShell } from "@/components/ui/app-shell"
import { DashboardGrid, DashboardGridItem } from "@/components/ui/dashboard-grid"
import { PageHeader } from "@/components/ui/page-header"

import { signOut } from "./actions"
import { FulfilmentStatus } from "./components/fulfilment-status"
import { OrderKpis } from "./components/order-kpis"
import { OrdersByWeekday } from "./components/orders-by-weekday"
import { PastOrders } from "./components/past-orders"
import { RevenueByRegion } from "./components/revenue-by-region"
import {
  bookedTotal,
  CLOSED_MONTH,
  currentUser,
  fulfilmentSplit,
  kpis,
  lastUpdated,
  ordersByWeekday,
  pastOrders,
  PRIOR_MONTH,
  revenueByRegion,
  shellNotifications,
} from "./data"
import { NAV, ROUTES } from "@/lib/dashboards/ecommerce/nav"

/**
 * The orders overview. A server component: it aggregates `db.orders` once and
 * hands plain rows to the client islands that draw them, so the data layer
 * never has to cross into the browser bundle.
 */
export default function OrdersOverviewPage() {
  return (
    <AppShell
      nav={NAV}
      activeHref={ROUTES.ordersOverview}
      user={currentUser()}
      notifications={shellNotifications()}
      now={REFERENCE_DATE}
      onSignOut={signOut}
    >
      <PageHeader
        title="Orders"
        description="How the book closed last month, and where it stands now."
        meta={lastUpdated()}
      />

      <OrderKpis kpis={kpis()} month={CLOSED_MONTH} priorMonth={PRIOR_MONTH} />

      <DashboardGrid>
        <DashboardGridItem colSpan={{ base: 12, lg: 5 }}>
          <OrdersByWeekday rows={ordersByWeekday()} />
        </DashboardGridItem>
        <DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
          <RevenueByRegion slices={revenueByRegion()} total={bookedTotal()} />
        </DashboardGridItem>
        <DashboardGridItem colSpan={{ base: 12, lg: 3 }}>
          <FulfilmentStatus segments={fulfilmentSplit()} />
        </DashboardGridItem>

        <DashboardGridItem colSpan={12}>
          <PastOrders rows={pastOrders()} />
        </DashboardGridItem>
      </DashboardGrid>
    </AppShell>
  )
}
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}`
}
app/ecommerce/orders/overview/actions.ts
"use server"

import { mockAuthAdapter } from "@/lib/auth-adapter"
import { type Result } from "@/lib/sample-data"

/**
 * The one thing this page changes. A server action so the page can stay a
 * server component and still hand the shell something to call, and a `Result`
 * so the caller reads the same success-or-error shape every mutation returns.
 */
export async function signOut(): Promise<Result<{ signedOut: true }>> {
  await mockAuthAdapter.signOut()
  return { ok: true, data: { signedOut: true } }
}
app/ecommerce/orders/overview/components/fulfilment-status.tsx
"use client"

import { formatNumber, formatPercent } from "@/lib/format"
import { PercentageBar } from "@/components/ui/percentage-bar"
import { StatusBadge, type StatusVariant } from "@/components/ui/status-badge"
import { Widget } from "@/components/ui/widget"

// Status colour, not palette colour: a reader should be able to tell "shipped"
// from "money back" without consulting a legend key.
const PAINT: Record<string, string> = {
  fulfilled: "var(--success)",
  paid: "var(--info)",
  pending: "var(--warning)",
  refunded: "var(--danger)",
  cancelled: "var(--muted-foreground)",
}

const STATUS_MAP: Record<string, StatusVariant> = { refunded: "danger", fulfilled: "success" }

export type FulfilmentStatusProps = {
  segments: { label: string; value: number; status: string }[]
}

/** Where the whole book stands, as one bar and the five counts under it. */
export function FulfilmentStatus({ segments }: FulfilmentStatusProps) {
  const total = segments.reduce((sum, segment) => sum + segment.value, 0)
  const shipped = segments.find((segment) => segment.status === "fulfilled")?.value ?? 0

  return (
    <Widget
      data-widget="widget-ecommerce-orders-overview-fulfilment-status"
      title="Fulfilment"
      description="Every order on the book, by state"
      className="h-full"
      footer={
        <span className="flex items-center gap-2">
          <StatusBadge status="fulfilled" map={STATUS_MAP} />
          {`${formatPercent(shipped / total, { maximumFractionDigits: 1 })} of ${formatNumber(total, { maximumFractionDigits: 0 })} orders`}
        </span>
      }
    >
      <PercentageBar
        segments={segments.map((segment) => ({
          label: segment.label,
          value: segment.value,
          color: PAINT[segment.status],
        }))}
        format={(value, percent) =>
          `${formatNumber(value, { maximumFractionDigits: 0 })} · ${percent}%`
        }
      />
    </Widget>
  )
}
app/ecommerce/orders/overview/components/order-kpis.tsx
import { formatCurrency, formatNumber, formatPercent } from "@/lib/format"
import { BulletChart } from "@/components/ui/bullet-chart"
import { MetricDelta } from "@/components/ui/metric-delta"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"

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

// How each KPI prints. The bullet track and the headline share one formatter,
// so the value above the bar and the value on it never disagree.
const FORMATS: Record<Kpi["format"], (value: number) => string> = {
  number: (value) => formatNumber(value, { maximumFractionDigits: 0 }),
  currency: (value) => formatCurrency(value, "USD", { maximumFractionDigits: 0 }),
  percent: (value) => formatPercent(value, { maximumFractionDigits: 1 }),
}

export type OrderKpisProps = {
  kpis: Kpi[]
  month: string
  priorMonth: string
}

/**
 * The month's four numbers. The card carries the headline, the bullet track
 * says where it stands against the target, and the delta below it says which
 * way it moved — three readings, none of them repeating another.
 */
export function OrderKpis({ kpis, month, priorMonth }: OrderKpisProps) {
  return (
    <StatCardGroup data-widget="widget-ecommerce-orders-overview-order-kpis" columns={4}>
      {kpis.map((kpi) => {
        const format = FORMATS[kpi.format]

        return (
          <StatCard
            key={kpi.key}
            label={kpi.label}
            value={format(kpi.value)}
            description={`In ${month}`}
            footer={
              <div className="flex flex-col gap-2">
                <BulletChart
                  size="sm"
                  label="Against target"
                  // Four tracks on one row would otherwise share a name.
                  aria-label={`${kpi.label} against target`}
                  value={kpi.value}
                  target={kpi.target}
                  ranges={kpi.ranges}
                  valueFormatter={format}
                />
                <div className="flex items-baseline justify-between gap-3 text-xs text-muted-foreground">
                  <span>{`vs ${priorMonth}`}</span>
                  <MetricDelta value={kpi.delta} size="sm" />
                </div>
              </div>
            }
          />
        )
      })}
    </StatCardGroup>
  )
}
app/ecommerce/orders/overview/components/orders-by-weekday.tsx
"use client"

import { formatNumber } from "@/lib/format"
import { BarChart } from "@/components/ui/bar-chart"
import { ChartCard } from "@/components/ui/chart-card"

const SERIES = [{ key: "orders", label: "Orders", color: "chart-1" as const }]

const count = (value: number) => formatNumber(value, { maximumFractionDigits: 0 })

/** Where the week's volume actually sits, across the whole book. */
export function OrdersByWeekday({ rows }: { rows: { day: string; orders: number }[] }) {
  return (
    <ChartCard
      data-widget="widget-ecommerce-orders-overview-orders-by-weekday"
      title="Orders by weekday"
      description="Every order on the book, by the day it was placed"
      height={240}
      className="h-full"
    >
      <BarChart
        data={rows}
        index="day"
        series={SERIES}
        height={240}
        showYAxis
        valueFormatter={count}
      />
    </ChartCard>
  )
}
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/components/revenue-by-region.tsx
"use client"

import { formatCurrency } from "@/lib/format"
import { ChartCard } from "@/components/ui/chart-card"
import { DonutChart } from "@/components/ui/donut-chart"

const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
const compact = (value: number) =>
  formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 1 })

export type RevenueByRegionProps = {
  slices: { name: string; value: number }[]
  total: number
}

/** Booked value split three ways, with the whole book in the middle. */
export function RevenueByRegion({ slices, total }: RevenueByRegionProps) {
  return (
    <ChartCard
      data-widget="widget-ecommerce-orders-overview-revenue-by-region"
      title="Revenue by region"
      description="Booked value, by where the order shipped"
      height={240}
      className="h-full"
    >
      <DonutChart
        data={slices}
        height={240}
        centerLabel="Booked"
        centerValue={compact(total)}
        valueFormatter={money}
      />
    </ChartCard>
  )
}