Revenue by month
Booked against fulfilled value over the complete months on the book — the gap between them is the warehouse; reads revenueByMonth().
Preview
"use client"
import { formatCurrency } from "@/lib/format"
import { AreaChart } from "@/components/ui/area-chart"
import { ChartCard } from "@/components/ui/chart-card"
import { type MonthPoint } from "../data"
// Fixed to UTC so the axis reads the same wherever the page is rendered.
const MONTH_LABEL = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" })
const SERIES = [
{ key: "booked", label: "Booked", color: "chart-1" as const },
{ key: "fulfilled", label: "Fulfilled", color: "chart-2" as const },
]
const money = (value: number) =>
formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 0 })
/** Booked against fulfilled value: the gap between them is the warehouse. */
export function RevenueByMonth({ rows }: { rows: MonthPoint[] }) {
return (
<ChartCard
data-widget="widget-ecommerce-sales-revenue-by-month"
title="Revenue by month"
description={`Booked and fulfilled value, the ${rows.length} complete months on the book`}
height={264}
className="h-full"
>
<AreaChart
data={rows}
index="month"
series={SERIES}
height={264}
showYAxis
valueFormatter={money}
indexFormatter={(value) => MONTH_LABEL.format(new Date(`${value}-01T00:00:00Z`))}
/>
</ChartCard>
)
}Install
$
npx shadcn@latest add @vibra/widget-ecommerce-sales-revenue-by-monthNeeds the @vibra registry in your components.json — set it up once.
Source
"use client"
import { formatCurrency } from "@/lib/format"
import { AreaChart } from "@/components/ui/area-chart"
import { ChartCard } from "@/components/ui/chart-card"
import { type MonthPoint } from "../data"
// Fixed to UTC so the axis reads the same wherever the page is rendered.
const MONTH_LABEL = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" })
const SERIES = [
{ key: "booked", label: "Booked", color: "chart-1" as const },
{ key: "fulfilled", label: "Fulfilled", color: "chart-2" as const },
]
const money = (value: number) =>
formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 0 })
/** Booked against fulfilled value: the gap between them is the warehouse. */
export function RevenueByMonth({ rows }: { rows: MonthPoint[] }) {
return (
<ChartCard
data-widget="widget-ecommerce-sales-revenue-by-month"
title="Revenue by month"
description={`Booked and fulfilled value, the ${rows.length} complete months on the book`}
height={264}
className="h-full"
>
<AreaChart
data={rows}
index="month"
series={SERIES}
height={264}
showYAxis
valueFormatter={money}
indexFormatter={(value) => MONTH_LABEL.format(new Date(`${value}-01T00:00:00Z`))}
/>
</ChartCard>
)
}/**
* 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