Skip to contentVibraUI

Order KPIs

Orders, revenue, average order and fulfilment rate for the closed month, each on a bullet track against its target and moved against the month before; reads kpis().

Preview

Install

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

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

Source

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/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