Sales headline
Bookings, open pipeline and win rate with their changes, and the quarter's quota as a goal track beside them; reads salesStats().
Preview
import { BanknoteIcon, FunnelIcon, TrophyIcon } from "lucide-react"
import { formatCurrency, formatPercent } from "@/lib/format"
import { Card, CardContent } from "@/components/ui/card"
import { GoalTracker } from "@/components/ui/goal-tracker"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"
import { type SalesStat } from "../data"
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
// The tracker prints its label, both numbers and the percentage on one row, so
// the money on it is abbreviated: the full figure would truncate the label.
const compactMoney = (value: number) =>
formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 0 })
// One icon per headline, keyed by the stat rather than its position, so the row
// can be reordered without the icons following the wrong numbers.
const ICONS: Record<string, React.ReactNode> = {
bookings: <BanknoteIcon />,
pipeline: <FunnelIcon />,
"win-rate": <TrophyIcon />,
}
export type SalesStatsProps = {
stats: SalesStat[]
quota: { label: string; current: number; goal: number; deadline: string }
}
/** The three headline numbers, and the quarter's target beside them. */
export function SalesStats({ stats, quota }: SalesStatsProps) {
return (
<StatCardGroup data-widget="widget-ecommerce-sales-sales-stats" columns={4}>
{stats.map((stat) => (
<StatCard
key={stat.key}
label={stat.label}
value={
stat.format === "currency"
? money(stat.value)
: formatPercent(stat.value, { maximumFractionDigits: 1 })
}
delta={stat.delta}
description={stat.description}
icon={ICONS[stat.key]}
/>
))}
{/* Not a StatCard: the tracker already prints the number, the target and
the share of it reached, so a value above it would say it twice. */}
<Card className="justify-center">
<CardContent>
<GoalTracker
label={quota.label}
current={quota.current}
goal={quota.goal}
format={compactMoney}
deadline={quota.deadline}
milestones={[
{ value: quota.goal / 2, label: "50%" },
{ value: (quota.goal * 3) / 4, label: "75%" },
]}
/>
</CardContent>
</Card>
</StatCardGroup>
)
}Install
$
npx shadcn@latest add @vibra/widget-ecommerce-sales-sales-statsNeeds the @vibra registry in your components.json — set it up once.
Source
import { BanknoteIcon, FunnelIcon, TrophyIcon } from "lucide-react"
import { formatCurrency, formatPercent } from "@/lib/format"
import { Card, CardContent } from "@/components/ui/card"
import { GoalTracker } from "@/components/ui/goal-tracker"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"
import { type SalesStat } from "../data"
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
// The tracker prints its label, both numbers and the percentage on one row, so
// the money on it is abbreviated: the full figure would truncate the label.
const compactMoney = (value: number) =>
formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 0 })
// One icon per headline, keyed by the stat rather than its position, so the row
// can be reordered without the icons following the wrong numbers.
const ICONS: Record<string, React.ReactNode> = {
bookings: <BanknoteIcon />,
pipeline: <FunnelIcon />,
"win-rate": <TrophyIcon />,
}
export type SalesStatsProps = {
stats: SalesStat[]
quota: { label: string; current: number; goal: number; deadline: string }
}
/** The three headline numbers, and the quarter's target beside them. */
export function SalesStats({ stats, quota }: SalesStatsProps) {
return (
<StatCardGroup data-widget="widget-ecommerce-sales-sales-stats" columns={4}>
{stats.map((stat) => (
<StatCard
key={stat.key}
label={stat.label}
value={
stat.format === "currency"
? money(stat.value)
: formatPercent(stat.value, { maximumFractionDigits: 1 })
}
delta={stat.delta}
description={stat.description}
icon={ICONS[stat.key]}
/>
))}
{/* Not a StatCard: the tracker already prints the number, the target and
the share of it reached, so a value above it would say it twice. */}
<Card className="justify-center">
<CardContent>
<GoalTracker
label={quota.label}
current={quota.current}
goal={quota.goal}
format={compactMoney}
deadline={quota.deadline}
milestones={[
{ value: quota.goal / 2, label: "50%" },
{ value: (quota.goal * 3) / 4, label: "75%" },
]}
/>
</CardContent>
</Card>
</StatCardGroup>
)
}/**
* What /sales reads. The deals are `db.orders`, the accounts behind them are
* `db.customers`, and the reps are `db.members` — an order is credited to the
* member who owns the account that placed it. Two things the rows do not record
* are rules over the rows rather than invented numbers: the stage an open deal
* sits in, hashed from its id so a deal always reads the same, and the
* quarter's quota, set from the quarter before it. "Now" is REFERENCE_DATE.
*/
import { getInitials } from "@/lib/format"
import { db, REFERENCE_DATE, type Member, type Order } from "@/lib/sample-data"
// Every read is per call, never held at module scope: a deal closed on the
// till or a refund since the server started is on the board the next time.
const orders = (): Order[] => db.orders.all()
const accountsById = () => new Map(db.customers.all().map((customer) => [customer.id, customer]))
const repsById = () => new Map(db.members.all().map((member) => [member.id, member]))
const dollars = (cents: number): number => cents / 100
const isWon = (order: Order): boolean => order.status === "paid" || order.status === "fulfilled"
const isLost = (order: Order): boolean =>
order.status === "cancelled" || order.status === "refunded"
const isOpen = (order: Order): boolean => order.status === "pending"
/** 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))
}
/**
* The complete months the order book covers. Orders run back 180 days from
* REFERENCE_DATE, which lands inside March, so March is a part month and the
* window opens in April: charting it would show a dip that is missing rows
* rather than a slower month.
*/
export const MONTHS = 5
const WINDOW_START = monthStart(MONTHS)
const WINDOW_END = monthStart(0)
const placedIn = (order: Order, from: Date, to: Date): boolean =>
order.placedAt >= from && order.placedAt < to
/** Closed-won value, in dollars, for the deals placed inside a window. */
function bookings(from: Date, to: Date): number {
return dollars(
orders().filter((order) => isWon(order) && placedIn(order, from, to)).reduce(
(total, order) => total + order.totalCents,
0
)
)
}
export type MonthPoint = { month: string; booked: number; fulfilled: number }
/**
* Booked and fulfilled value for every complete month on the book. A deal is
* booked in the month it was placed and fulfilled in the month it shipped, so
* the two lines separate exactly as far as the warehouse runs behind sales.
*/
export function revenueByMonth(): MonthPoint[] {
return Array.from({ length: MONTHS }, (_, index) => {
const from = monthStart(MONTHS - index)
const to = monthStart(MONTHS - index - 1)
const fulfilled = orders().filter(
(order) => order.fulfilledAt && order.fulfilledAt >= from && order.fulfilledAt < to
).reduce((total, order) => total + order.totalCents, 0)
return {
month: from.toISOString().slice(0, 7),
booked: Math.round(bookings(from, to)),
fulfilled: Math.round(dollars(fulfilled)),
}
})
}
/** The share of closed deals that were won, inside a window. */
function winRate(from: Date, to: Date): number {
const closed = orders().filter((order) => !isOpen(order) && placedIn(order, from, to))
const won = closed.filter(isWon).length
return closed.length > 0 ? won / closed.length : 0
}
const LAST_MONTH_START = monthStart(1)
const PRIOR_MONTH_START = monthStart(2)
const open = (): Order[] => orders().filter(isOpen)
/** How many deals are still open, for the copy that counts them. */
export function openDeals(): number {
return open().length
}
export type SalesStat = {
key: string
label: string
value: number
/** A ratio, or undefined where there is nothing to compare the number with. */
delta?: number
description: string
format: "currency" | "percent"
}
/**
* The change from `before` to `now`, as a ratio — or nothing at all when there
* is no base to measure against. A month that booked nothing, or closed no
* deals at all, makes every number after it infinitely better than it, which
* is not a reading a stat card can show.
*/
function change(now: number, before: number): number | undefined {
return before > 0 ? now / before - 1 : undefined
}
/** The three headline numbers beside the quota card. */
export function salesStats(): SalesStat[] {
const pipeline = open()
const booked = bookings(LAST_MONTH_START, WINDOW_END)
const before = bookings(PRIOR_MONTH_START, LAST_MONTH_START)
const rate = winRate(LAST_MONTH_START, WINDOW_END)
const wasRate = winRate(PRIOR_MONTH_START, LAST_MONTH_START)
return [
{
key: "bookings",
label: "Bookings",
value: booked,
delta: change(booked, before),
description: "vs the month before",
format: "currency",
},
{
key: "pipeline",
label: "Open pipeline",
value: dollars(pipeline.reduce((total, order) => total + order.totalCents, 0)),
description: `${pipeline.length} deals still to close`,
format: "currency",
},
{
key: "win-rate",
label: "Win rate",
value: rate,
delta: change(rate, wasRate),
description: "of the deals closed last month",
format: "percent",
},
]
}
// Quarters, from REFERENCE_DATE rather than a clock: the quarter it falls in,
// and the one before it that sets the target.
const QUARTER_MONTH = Math.floor(REFERENCE_DATE.getUTCMonth() / 3) * 3
const QUARTER_START = new Date(Date.UTC(REFERENCE_DATE.getUTCFullYear(), QUARTER_MONTH, 1))
const PRIOR_QUARTER_START = new Date(
Date.UTC(REFERENCE_DATE.getUTCFullYear(), QUARTER_MONTH - 3, 1)
)
// Day 0 of the next month is the last day of this one.
const QUARTER_END = new Date(Date.UTC(REFERENCE_DATE.getUTCFullYear(), QUARTER_MONTH + 3, 0))
const QUARTER_LABEL = `Q${Math.floor(QUARTER_MONTH / 3) + 1}`
const QUARTER_END_LABEL = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeZone: "UTC",
}).format(QUARTER_END)
/**
* The quarter's number: what has closed since it opened, against a target set
* at 15% over the quarter before it and rounded to the nearest $25k, which is
* how a target gets written down.
*/
export function quota() {
// Through now, inclusive: a deal closed at REFERENCE_DATE is this quarter's.
const current = Math.round(bookings(QUARTER_START, new Date(REFERENCE_DATE.getTime() + 1)))
const goal = Math.round((bookings(PRIOR_QUARTER_START, QUARTER_START) * 1.15) / 25_000) * 25_000
return {
label: `${QUARTER_LABEL} quota`,
current,
goal,
deadline: `${QUARTER_LABEL} closes ${QUARTER_END_LABEL}`,
}
}
// The four stages an open deal moves through. Which one it sits in is not a
// column on the order, so it is hashed from the id: stable across runs, and
// weighted so most of the pipeline is still early rather than spread evenly.
const STAGES = ["Qualified", "Discovery", "Proposal", "Negotiation"] as const
const STAGE_SHARE = [1, 0.62, 0.34, 0.13]
export type Stage = (typeof STAGES)[number] | "Closed won" | "Closed lost"
/** A number in [0, 1) from a row id — the same id always gives the same number. */
function hashUnit(id: string): number {
let hash = 2_166_136_261
for (let index = 0; index < id.length; index++) {
hash ^= id.charCodeAt(index)
hash = Math.imul(hash, 16_777_619)
}
return ((hash >>> 0) % 10_000) / 10_000
}
function stageIndex(order: Order): number {
const ticket = hashUnit(order.id)
let index = 0
while (index + 1 < STAGE_SHARE.length && ticket < STAGE_SHARE[index + 1]) index++
return index
}
/** Where a deal stands: its stage while it is open, its outcome once it is not. */
export function stageOf(order: Order): Stage {
if (isWon(order)) return "Closed won"
if (isLost(order)) return "Closed lost"
return STAGES[stageIndex(order)]
}
/** The open pipeline by value, counting every deal that has reached each stage. */
export function pipelineByStage(): { label: string; value: number }[] {
const pipeline = open()
return STAGES.map((label, index) => ({
label,
value: Math.round(
dollars(
pipeline.filter((order) => stageIndex(order) >= index).reduce(
(total, order) => total + order.totalCents,
0
)
)
),
}))
}
/** Closed-won value per rep over the window, largest first. */
export function repLeaderboard(): { label: string; value: number }[] {
const totals = new Map<string, number>()
const accounts = accountsById()
const reps = repsById()
for (const order of orders()) {
if (!isWon(order) || !placedIn(order, WINDOW_START, WINDOW_END)) continue
const owner = accounts.get(order.customerId)?.owner
if (!owner) continue
totals.set(owner, (totals.get(owner) ?? 0) + order.totalCents)
}
return [...totals]
.map(([id, cents]) => ({ label: reps.get(id)?.name ?? "Unassigned", value: Math.round(dollars(cents)) }))
.sort((a, b) => b.value - a.value)
.slice(0, 6)
}
export type Deal = {
id: string
number: string
account: string
contact: string
rep: string
stage: Stage
value: number
placedAt: Date
country: string
}
/** The deals on the board, newest first — the order book read as a sales list. */
export function deals(): Deal[] {
const accounts = accountsById()
const reps = repsById()
return orders().map((order) => {
const account = accounts.get(order.customerId)
return {
id: order.id,
number: order.number,
// A sale the till rang up for nobody in particular has no account.
account: account?.company ?? (order.customerId || "Walk-in"),
contact: account?.name ?? "Unknown",
rep: (account && reps.get(account.owner)?.name) ?? "Unassigned",
stage: stageOf(order),
value: dollars(order.totalCents),
placedAt: order.placedAt,
country: order.country,
}
})
.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 }
}
const MONTH_LABEL = new Intl.DateTimeFormat("en-US", { month: "long", timeZone: "UTC" })
/** Which month the headline numbers closed on. */
export function lastUpdated(): string {
return `Closed through ${MONTH_LABEL.format(LAST_MONTH_START)} ${LAST_MONTH_START.getUTCFullYear()}`
}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 Sales dashboard page