/saas/revenueRevenue dashboard
A revenue page: MRR with its month and year deltas, the ratios beside it, twelve months by plan, the quarter's target, where the month's movement came from, the largest accounts, and how much of what customers pay for they use.
Nothing on this page changes after it loads, so only the parts that need a browser carry "use client" — the charts and the counting hero. The widgets around them, and the page itself, render on the server inside AppShell, with nav.ts describing the navigation as plain data. book.ts holds the model — how a subscription becomes revenue and what the revenue was on a given date — and data.ts the selectors over it, so the part a consumer swaps for a real billing system sits on its own. The book is db.subscriptions walked month by month: a subscription bills from the day it started until the day it was cancelled, so the twelve-month chart, the closing MRR, the movements, the ratios and the quarter tracker all come off the same rows, and the page closes each month rather than reading a part-finished one. Movements are New and Churn alone, because a subscription carries one rate rather than a history of them and an invented expansion would need an invented contraction to cancel it. The movement roll-up is the one part of the page a reader can ask a different question of: three windows, all computed on the server off the same book, so switching costs nothing and the four figures roll to their new readings through StatCard's `roll` rather than cutting to them. Composes AppShell, PageHeader, ExportMenu, BigNumber, MetricDelta, StatList, SectionHeader, SegmentedControl, StatCardGroup, StatCard, DashboardGrid, ChartCard, StackedBarChart, WaterfallChart, GoalTracker, PercentageBar, Widget, SimpleTable, CurrencyCell, DeltaCell and UsageMeter.
Preview
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 { CommittedUsage } from "./components/committed-usage"
import { MrrMovements } from "./components/mrr-movements"
import { QuarterGoal } from "./components/quarter-goal"
import { RevenueActions } from "./components/revenue-actions"
import { RevenueByPlan } from "./components/revenue-by-plan"
import { RevenueHero } from "./components/revenue-hero"
import { RevenueMix } from "./components/revenue-mix"
import { RevenueRollups } from "./components/revenue-rollups"
import { TopCustomers } from "./components/top-customers"
import {
bookAsOf,
currentUser,
mrrYearOpening,
mrrMovements,
planKeys,
quarterTarget,
revenueByPlan,
revenueHeadline,
revenueWindows,
shellNotifications,
} from "./data"
import { NAV, ROUTES } from "@/lib/dashboards/saas/nav"
/**
* The revenue page. Nothing here changes after it loads, so only the parts
* that need a browser — the charts and the counting hero — are client
* components; the page itself and the widgets around them render on the
* server, inside the shell, and hand those islands their figures as props, so
* the book they are read from never reaches the browser.
*/
export default function RevenuePage() {
return (
<AppShell
nav={NAV}
activeHref={ROUTES.revenue}
user={currentUser()}
notifications={shellNotifications()}
now={REFERENCE_DATE}
onSignOut={signOut}
>
<PageHeader
title="Revenue"
description={`Where the book stands at the close of ${bookAsOf()}.`}
meta="Recognised monthly, net of refunds"
actions={<RevenueActions />}
/>
<RevenueHero headline={revenueHeadline()} />
<RevenueRollups windows={revenueWindows()} />
<DashboardGrid>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<RevenueByPlan rows={revenueByPlan()} plans={planKeys()} target={quarterTarget()} asOf={bookAsOf()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<QuarterGoal />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<MrrMovements movements={mrrMovements()} opening={mrrYearOpening()} asOf={bookAsOf()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<RevenueMix />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<TopCustomers />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<CommittedUsage />
</DashboardGridItem>
</DashboardGrid>
</AppShell>
)
}Install
npx shadcn@latest add @vibra/saas-revenueNeeds the @vibra registry in your components.json — set it up once.
Source
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 { CommittedUsage } from "./components/committed-usage"
import { MrrMovements } from "./components/mrr-movements"
import { QuarterGoal } from "./components/quarter-goal"
import { RevenueActions } from "./components/revenue-actions"
import { RevenueByPlan } from "./components/revenue-by-plan"
import { RevenueHero } from "./components/revenue-hero"
import { RevenueMix } from "./components/revenue-mix"
import { RevenueRollups } from "./components/revenue-rollups"
import { TopCustomers } from "./components/top-customers"
import {
bookAsOf,
currentUser,
mrrYearOpening,
mrrMovements,
planKeys,
quarterTarget,
revenueByPlan,
revenueHeadline,
revenueWindows,
shellNotifications,
} from "./data"
import { NAV, ROUTES } from "@/lib/dashboards/saas/nav"
/**
* The revenue page. Nothing here changes after it loads, so only the parts
* that need a browser — the charts and the counting hero — are client
* components; the page itself and the widgets around them render on the
* server, inside the shell, and hand those islands their figures as props, so
* the book they are read from never reaches the browser.
*/
export default function RevenuePage() {
return (
<AppShell
nav={NAV}
activeHref={ROUTES.revenue}
user={currentUser()}
notifications={shellNotifications()}
now={REFERENCE_DATE}
onSignOut={signOut}
>
<PageHeader
title="Revenue"
description={`Where the book stands at the close of ${bookAsOf()}.`}
meta="Recognised monthly, net of refunds"
actions={<RevenueActions />}
/>
<RevenueHero headline={revenueHeadline()} />
<RevenueRollups windows={revenueWindows()} />
<DashboardGrid>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<RevenueByPlan rows={revenueByPlan()} plans={planKeys()} target={quarterTarget()} asOf={bookAsOf()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<QuarterGoal />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<MrrMovements movements={mrrMovements()} opening={mrrYearOpening()} asOf={bookAsOf()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<RevenueMix />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<TopCustomers />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<CommittedUsage />
</DashboardGridItem>
</DashboardGrid>
</AppShell>
)
}/**
* The book: how a subscription becomes revenue, and what the revenue was on any
* given date. `db.subscriptions` records one rate per account and the two dates
* it ran between, so a month's book is every subscription that had started by
* the close and had not been cancelled yet.
*
* `data.ts` holds the selectors the page reads; this is the model underneath
* them, kept apart because it is the part a consumer swaps when the rows come
* from a real billing system.
*/
import { db, REFERENCE_DATE, type Customer, type Subscription } from "@/lib/sample-data"
/** The plan tiers that carry revenue, largest first — Free never enters the book. */
export type PaidTier = Exclude<Customer["plan"], "free">
export const PAID_TIERS = ["enterprise", "team", "starter"] as const satisfies readonly PaidTier[]
/**
* Every subscription, read per call and never held at module scope: an account
* cancelled or started since the server started is in the next book.
*/
export function subscriptions(): Subscription[] {
return db.subscriptions.all()
}
export const PLAN_NAME_BY_ID = new Map(db.plans.all().map((plan) => [plan.id, plan.name]))
const PLAN_BY_TIER = new Map(db.plans.all().map((plan) => [plan.name.toLowerCase(), plan]))
/** The plan a tier names. Customer.plan and Plan.name are the same four tiers. */
export function planFor(tier: PaidTier) {
return PLAN_BY_TIER.get(tier)!
}
// What a plan charges a seat a month, read off the accounts paying it today:
// every live account on a tier bills the same rate, so one of them states it
// for the accounts that have since stopped paying.
export function seatPrices(): Map<string, number> {
const prices = new Map<string, number>()
for (const customer of db.customers.all()) {
if (customer.mrrCents > 0 && !prices.has(customer.plan)) {
prices.set(customer.plan, customer.mrrCents / customer.seats)
}
}
return prices
}
/** What a subscription bills a month, in cents. A trial bills nothing. */
export function billedCents(subscription: Subscription, prices = seatPrices()): number {
if (subscription.status === "trialing") return 0
const tier = (PLAN_NAME_BY_ID.get(subscription.planId) ?? "").toLowerCase()
return subscription.seats * (prices.get(tier) ?? 0)
}
/** Midnight UTC on the first of the month `k` months before the current one. */
export function monthStart(k: number): Date {
return new Date(Date.UTC(REFERENCE_DATE.getUTCFullYear(), REFERENCE_DATE.getUTCMonth() - k, 1))
}
/** The month that ends at `monthStart(k)`, as "2026-08". */
export function closedMonthKey(k: number): string {
return monthStart(k + 1).toISOString().slice(0, 7)
}
/** Whether a subscription was billing at `at`. */
export const live = (subscription: Subscription, at: Date): boolean =>
subscription.startedAt <= at && (!subscription.cancelledAt || subscription.cancelledAt > at)
/** The book at `at`, in dollars, for one plan or for all of them. */
export function bookAt(at: Date, planId?: string): number {
const prices = seatPrices()
return (
subscriptions()
.filter((row) => live(row, at) && (!planId || row.planId === planId))
.reduce((total, row) => total + billedCents(row, prices), 0) / 100
)
}
/** What started billing, and what stopped, between two instants — in dollars. */
export function movement(from: Date, to: Date): { won: number; lost: number } {
let won = 0
let lost = 0
const prices = seatPrices()
for (const subscription of subscriptions()) {
if (subscription.startedAt > from && subscription.startedAt <= to) won += billedCents(subscription, prices)
const { cancelledAt } = subscription
if (cancelledAt && cancelledAt > from && cancelledAt <= to) lost += billedCents(subscription, prices)
}
return { won: won / 100, lost: lost / 100 }
}/**
* What this page reads. The book is `db.subscriptions` walked month by month by
* `./book`, so the twelve-month chart, the closing MRR, the movements, the
* ratios and the quarter tracker all come off the same rows. The page closes
* each month rather than reading a part-finished one; "now" is `REFERENCE_DATE`.
*/
import { formatCurrency, formatPercent, getInitials } from "@/lib/format"
import { db, REFERENCE_DATE, seeded, type Member, type Subscription } from "@/lib/sample-data"
import {
billedCents,
bookAt,
closedMonthKey,
live,
monthStart,
movement,
PAID_TIERS,
planFor,
PLAN_NAME_BY_ID,
seatPrices,
subscriptions,
type PaidTier,
} from "./book"
const MONTHS = 12
/** One month of the book. The keys are the paid tiers, so a typo will not compile. */
export type MonthlyRevenue = { month: string } & Record<PaidTier, number>
/** Twelve closed months of MRR split by plan, oldest first. Each row is that month's book. */
export function revenueByPlan(): MonthlyRevenue[] {
return Array.from({ length: MONTHS }, (_, index) => {
const k = MONTHS - 1 - index
const at = monthStart(k)
return {
month: closedMonthKey(k),
enterprise: Math.round(bookAt(at, planFor("enterprise").id)),
team: Math.round(bookAt(at, planFor("team").id)),
starter: Math.round(bookAt(at, planFor("starter").id)),
}
})
}
/** A tier a revenue chart stacks: the key its rows carry, and the plan's name. */
export type PlanKey = { key: PaidTier; label: string }
/** The tiers a revenue chart stacks, largest first. */
export function planKeys(): PlanKey[] {
return PAID_TIERS.map((tier) => ({ key: tier, label: planFor(tier).name }))
}
const CLOSE_AT = monthStart(0)
const OPENING_AT = monthStart(1)
const YEAR_AGO_AT = monthStart(MONTHS)
/** Monthly recurring revenue at the close of the last complete month, read off the book as it stands. */
const closingMrr = () => Math.round(bookAt(CLOSE_AT))
/** The closing split by plan, in the shape PercentageBar takes. */
export function revenueMix(): { label: string; value: number }[] {
return PAID_TIERS.map((tier) => ({
label: planFor(tier).name,
value: Math.round(bookAt(CLOSE_AT, planFor(tier).id)),
}))
}
/** Enterprise's share of the book now, and twelve months ago. */
export function enterpriseShare(): { now: number; yearAgo: number } {
const enterprise = planFor("enterprise").id
return {
now: bookAt(CLOSE_AT, enterprise) / bookAt(CLOSE_AT),
yearAgo: bookAt(YEAR_AGO_AT, enterprise) / bookAt(YEAR_AGO_AT),
}
}
/**
* What the book won and lost between two month closes, over the same twelve
* months the chart above draws — a single month of this data is mostly trials
* that have not started paying. Two movements rather than the usual four: an
* account moving up or down a plan leaves no trace in the rows, and a made-up
* expansion would need a made-up contraction to cancel it.
*/
function movements(fromK: number, toK: number) {
const opening = bookAt(monthStart(fromK))
const { won, lost } = movement(monthStart(fromK), monthStart(toK))
return { won, lost, opening, net: bookAt(monthStart(toK)) - opening }
}
/** The book the twelve months of movement below opened against. */
export function mrrYearOpening(): number {
return Math.round(movements(MONTHS, 0).opening)
}
/** One step of the year's MRR walk, as the waterfall draws it. */
export type MrrMovement = { label: string; value: number; isTotal?: boolean }
/** How the closing book got to where it is over the twelve months behind it. */
export function mrrMovements(): MrrMovement[] {
const YEAR = movements(MONTHS, 0)
return [
{ label: "New", value: Math.round(YEAR.won) },
{ label: "Churn", value: -Math.round(YEAR.lost) },
{ label: "Net new", value: 0, isTotal: true },
]
}
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
const paying = (at: Date): number => {
const prices = seatPrices()
return subscriptions().filter((subscription) => live(subscription, at) && billedCents(subscription, prices) > 0)
.length
}
const retained = (span: ReturnType<typeof movements>): number => (span.opening - span.lost) / span.opening
/** One of the four ratios beside the headline number: printed, and moved against the year before. */
export type HeadlineStat = { label: string; value: string; delta: number; positiveIsGood?: boolean }
/** The four ratios beside the headline number, each against the year before it. */
export function headlineStats(): HeadlineStat[] {
const YEAR = movements(MONTHS, 0)
const YEAR_BEFORE = movements(MONTHS * 2, MONTHS)
const arpu = closingMrr() / paying(CLOSE_AT)
const wasArpu = bookAt(YEAR_AGO_AT) / paying(YEAR_AGO_AT)
const churn = YEAR.lost / YEAR.opening
const wasChurn = YEAR_BEFORE.lost / YEAR_BEFORE.opening
const kept = retained(YEAR)
// What an account is worth at a churn rate: the months it lasts, times what
// it pays for each of them.
const months = (rate: number) => (rate > 0 ? 12 / rate : 36)
const ltv = arpu * months(churn)
const wasLtv = wasArpu * months(wasChurn)
return [
{ label: "ARPU", value: money(arpu), delta: arpu / wasArpu - 1 },
{
label: "Gross churn",
// A fall is the win, so the delta is read the other way up.
value: formatPercent(churn, { maximumFractionDigits: 1 }),
delta: wasChurn > 0 ? churn / wasChurn - 1 : 0,
positiveIsGood: false,
},
{ label: "Lifetime value", value: money(ltv), delta: ltv / wasLtv - 1 },
{
label: "Gross retention",
value: formatPercent(kept, { maximumFractionDigits: 0 }),
delta: kept / retained(YEAR_BEFORE) - 1,
},
]
}
export type RevenueWindow = {
key: "1m" | "3m" | "12m"
/** How the window reads in a sentence, e.g. "last quarter". */
label: string
won: number
lost: number
net: number
/** Net new as a share of what the book opened the window at. */
growth: number
/** Each against the same length of book immediately before it. */
wonDelta: number
lostDelta: number
netDelta: number
growthDelta: number
}
// The three windows a revenue conversation actually compares. Month counts,
// not dates: the book is walked month by month, and a part-finished month is
// mostly trials that have not started paying.
const WINDOWS = [
{ key: "1m", label: "last month", months: 1 },
{ key: "3m", label: "last quarter", months: 3 },
{ key: "12m", label: "last year", months: MONTHS },
] as const
/**
* What the book won and lost over each window, and how that compares with the
* same length of book immediately before it — the same two movements the
* waterfall draws, read at three lengths so the page can be asked a different
* question without a round trip.
*/
export function revenueWindows(): RevenueWindow[] {
const ratio = (now: number, before: number) => (before > 0 ? now / before - 1 : 0)
return WINDOWS.map(({ key, label, months }) => {
const span = movements(months, 0)
const before = movements(months * 2, months)
const growth = span.opening > 0 ? span.net / span.opening : 0
const wasGrowth = before.opening > 0 ? before.net / before.opening : 0
return {
key,
label,
won: Math.round(span.won),
lost: Math.round(span.lost),
net: Math.round(span.net),
growth,
wonDelta: ratio(span.won, before.won),
lostDelta: ratio(span.lost, before.lost),
netDelta: ratio(span.net, before.net),
growthDelta: ratio(growth, wasGrowth),
}
})
}
const DAY_MS = 86_400_000
const MONTH_NAME = new Intl.DateTimeFormat("en-US", { month: "long", timeZone: "UTC" })
// The quarter runs to the end of its third month, not to the start of it.
const QUARTER_END = new Date(
Date.UTC(REFERENCE_DATE.getUTCFullYear(), Math.floor(REFERENCE_DATE.getUTCMonth() / 3) * 3 + 3, 1) - 1
)
/** Everything the headline card prints: the closing book, how it moved, who pays it, and the four ratios. */
export type RevenueHeadline = {
mrr: number
/** On the month before. */
delta: number
/** On the same close a year earlier. */
yoy: number
customers: number
stats: HeadlineStat[]
}
/** The headline card's figures, read here and handed to the card: the card is an island, and never reads the book. */
export function revenueHeadline(): RevenueHeadline {
const mrr = closingMrr()
return {
mrr,
delta: mrr / Math.round(bookAt(OPENING_AT)) - 1,
yoy: mrr / bookAt(YEAR_AGO_AT) - 1,
customers: db.customers.all().length,
stats: headlineStats(),
}
}
const QUARTER_LABEL = `Q${Math.floor(REFERENCE_DATE.getUTCMonth() / 3) + 1}`
// Months into the quarter so far, so the opening is the quarter's own start.
const quarterOpening = () => bookAt(monthStart(REFERENCE_DATE.getUTCMonth() % 3))
/** Whole days left in the quarter, counting the rest of today. */
export const QUARTER_DAYS_LEFT = Math.floor((QUARTER_END.getTime() - REFERENCE_DATE.getTime()) / DAY_MS)
/** Net new MRR booked this quarter, against the number the board was given. */
export function quarterGoal() {
const opening = quarterOpening()
const current = Math.round(closingMrr() - opening)
const months = REFERENCE_DATE.getUTCMonth() % 3
return {
label: `${QUARTER_LABEL} net new MRR`,
current,
// A round number a little beyond the run rate: a target, not a forecast.
goal: Math.ceil((current * 1.55) / 1_000) * 1_000,
milestones: Array.from({ length: Math.max(0, months - 1) }, (_, index) => ({
value: Math.round(bookAt(monthStart(months - 1 - index)) - opening),
label: MONTH_NAME.format(monthStart(months - index)).slice(0, 3),
})),
deadline: `Quarter closes ${QUARTER_END.getUTCDate()} ${MONTH_NAME.format(QUARTER_END)}`,
}
}
/**
* The book the quarter has to close on: what it opened at plus the number the
* board was given. The goal itself is net new, so a chart of the whole book
* plots this rather than the difference — the same target, read on the scale
* the chart is already on.
*/
export function quarterTarget(): { value: number; label: string } {
return { value: Math.round(quarterOpening() + quarterGoal().goal), label: `${QUARTER_LABEL} target` }
}
/** The month the book is read at, e.g. "August 2026". */
export function bookAsOf(): string {
return `${MONTH_NAME.format(monthStart(1))} ${monthStart(1).getUTCFullYear()}`
}
export type TopCustomer = {
company: string
plan: string
owner: string
mrr: number
/** Change in that account's MRR on the month, as a ratio. */
delta: number
}
const TOP_COUNT = 8
const YEAR_MS = 365 * 86_400_000
// Growth decays with an account's age, and the floor is what it decays past:
// the two meet at about fifteen months, which is where an account stops adding
// seats faster than it loses them.
const GROWTH_AT_NEW = 0.26
const SETTLED_DRIFT = 0.078
/**
* What an account did on the month. A subscription carries one rate rather than
* a history of them, so this is a rule over its age: an account still in its
* first year is adding seats fast, and one past its second is settled and
* drifting slightly down. A book this size holds both, so the column does too.
*/
function monthOnMonth(subscription: Subscription): number {
const years = (CLOSE_AT.getTime() - subscription.startedAt.getTime()) / YEAR_MS
return Math.round((GROWTH_AT_NEW * Math.exp(-years) - SETTLED_DRIFT) * 1000) / 1000
}
/** The largest accounts by MRR, with what each did on the month. */
export function topCustomers(): TopCustomer[] {
// Read per call, never held at module scope: an account or an owner written
// since the server started is what the next render shows.
const members = new Map(db.members.all().map((member) => [member.id, member]))
const customers = new Map(db.customers.all().map((customer) => [customer.id, customer]))
const prices = seatPrices()
return subscriptions()
.filter((subscription) => live(subscription, CLOSE_AT))
.sort((a, b) => billedCents(b, prices) - billedCents(a, prices))
.slice(0, TOP_COUNT)
.map((subscription) => {
const customer = customers.get(subscription.customerId)
return {
company: customer?.company ?? subscription.customerId,
plan: PLAN_NAME_BY_ID.get(subscription.planId) ?? "—",
owner: members.get(customer?.owner ?? "")?.name ?? "Unassigned",
mrr: billedCents(subscription, prices) / 100,
delta: monthOnMonth(subscription),
}
})
}
/** What those accounts add up to, as a share of the whole book. */
export function topCustomersShare(rows: TopCustomer[]): number {
return rows.reduce((sum, customer) => sum + customer.mrr, 0) / closingMrr()
}
/**
* How much of what customers pay for they actually use — the expansion signal.
* Seats are counted off the subscriptions against what their plans include;
* events and storage have no entity, so they come off `seeded("dashboard-02-usage")`.
*/
export function committedUsage() {
const rand = seeded("dashboard-02-usage")
// Seats bought is what the accounts pay for; how many of them are in use is
// not something a subscription records, and nor are events or storage.
const prices = seatPrices()
const seats = subscriptions()
.filter((subscription) => live(subscription, CLOSE_AT) && billedCents(subscription, prices) > 0)
.reduce((sum, subscription) => sum + subscription.seats, 0)
const used = (limit: number, floor: number, spread: number) => limit * (floor + rand() * spread)
return [
{ label: "Seats", used: Math.round(used(seats, 0.82, 0.1)), limit: seats, unit: "seats", compact: false },
{ label: "Events", used: Math.round(used(12e6, 0.62, 0.12)), limit: 12e6, unit: "events", compact: true },
{ label: "Storage", used: Math.round(used(10, 0.58, 0.14) * 10) / 10, limit: 10, unit: "TB", compact: false },
]
}
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 }
}
/** 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 }))
}"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 } }
}import { formatCompact, formatNumber } from "@/lib/format"
import { UsageMeter } from "@/components/ui/usage-meter"
import { Widget } from "@/components/ui/widget"
import { committedUsage } from "../data"
export function CommittedUsage() {
return (
<Widget
data-widget="widget-saas-revenue-committed-usage"
title="Committed usage"
description="How much of what customers pay for they actually use"
className="h-full"
footer="Accounts above 90% are the ones worth calling about an upgrade."
>
<div className="flex flex-col gap-5">
{committedUsage().map((meter) => (
<UsageMeter
key={meter.label}
size="sm"
label={meter.label}
used={meter.used}
limit={meter.limit}
unit={meter.unit}
tone="auto"
format={(value) =>
meter.compact ? formatCompact(value) : formatNumber(value, { maximumFractionDigits: 1 })
}
/>
))}
</div>
</Widget>
)
}"use client"
import { formatCurrency } from "@/lib/format"
import { ChartCard } from "@/components/ui/chart-card"
import { WaterfallChart } from "@/components/ui/waterfall-chart"
import { type MrrMovement } from "../data"
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
export type MrrMovementsProps = {
movements: MrrMovement[]
/** The book the twelve months opened against. */
opening: number
/** The month the book is read at, e.g. "August 2026". */
asOf: string
}
/** The year's walk from the opening book to the closing one: what started paying, less what stopped. */
export function MrrMovements({ movements, opening, asOf }: MrrMovementsProps) {
return (
<ChartCard
data-widget="widget-saas-revenue-mrr-movements"
title="MRR movements"
description={`Twelve months to ${asOf}, against an opening book of ${money(opening)}`}
height={280}
className="h-full"
footer="Net new is what started paying, less what stopped."
>
<WaterfallChart data={movements} height={280} valueFormatter={money} />
</ChartCard>
)
}import { formatCurrency } from "@/lib/format"
import { GoalTracker } from "@/components/ui/goal-tracker"
import { Widget } from "@/components/ui/widget"
import { quarterGoal, QUARTER_DAYS_LEFT } from "../data"
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
export function QuarterGoal() {
const goal = quarterGoal()
const remaining = goal.goal - goal.current
return (
<Widget
data-widget="widget-saas-revenue-quarter-goal"
title="Quarter to date"
description="Net new MRR booked since 1 July"
className="h-full"
footer={`${money(remaining)} left with ${QUARTER_DAYS_LEFT} days to go.`}
>
<GoalTracker
label={goal.label}
current={goal.current}
goal={goal.goal}
milestones={goal.milestones}
deadline={goal.deadline}
tone="auto"
format={money}
/>
</Widget>
)
}"use client"
import {
chartSurface,
downloadBlob,
exportFilename,
printPage,
svgToPng,
} from "@/lib/export"
import { notify } from "@/lib/notify"
import { ExportMenu, type ExportFormat } from "@/components/ui/export-menu"
/**
* The id this menu reads its plot from. It lives here rather than beside the
* chart because the chart's module reads the row store: importing it from a
* page-header island would ship that store to the browser for the sake of one
* string.
*/
export const REVENUE_CHART_ID = "revenue-by-plan"
/**
* The page header's controls. A client island because the export handler is a
* function, and functions cannot be handed to a client component from a server
* one — and because it now writes a file rather than logging an intention: the
* revenue chart by its own id, or the page under a printed masthead.
*/
export function RevenueActions() {
async function handleExport(format: ExportFormat) {
if (format === "pdf") {
printPage({ brand: "Northwind", title: "Revenue" })
return
}
const svg = chartSurface(document.getElementById(REVENUE_CHART_ID))
if (!svg) {
notify.error("The chart is still drawing.")
return
}
// Drawing the picture and saving it can each fail; neither was reported.
try {
const written = downloadBlob(exportFilename("revenue-by-plan", "png"), await svgToPng(svg))
notify[written ? "success" : "error"](
written ? "Downloaded the chart as PNG." : "This browser cannot save the file."
)
} catch {
notify.error("The chart could not be drawn as a picture.")
}
}
return <ExportMenu size="sm" formats={["png", "pdf"]} onExport={handleExport} />
}"use client"
import { formatCompact } from "@/lib/format"
import { ChartCard } from "@/components/ui/chart-card"
import type { ChartAnnotation } from "@/components/ui/chart-core"
import { StackedBarChart } from "@/components/ui/stacked-bar-chart"
import { type MonthlyRevenue, type PlanKey } from "../data"
import { REVENUE_CHART_ID } from "./revenue-actions"
// 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" })
// One colour a plan, largest plan first — the order planKeys() hands back.
const COLORS = ["chart-1", "chart-2", "chart-3", "chart-4"] as const
export type RevenueByPlanProps = {
/** Twelve closed months of the book, split by plan, oldest first. */
rows: MonthlyRevenue[]
/** The tiers the stack draws, largest first. */
plans: PlanKey[]
/** The book the quarter has to close on. */
target: { value: number; label: string }
/** The month the book is read at, e.g. "August 2026". */
asOf: string
}
/** The twelve months of the book stacked by plan, with the quarter's target drawn across them. */
export function RevenueByPlan({ rows, plans, target, asOf }: RevenueByPlanProps) {
const series = plans.map((plan, index) => ({ ...plan, color: COLORS[index] }))
// The number the board was given, drawn where the stack has to reach — a
// threshold rather than a fifth plan, and printed in the chart's own rows.
const annotations: ChartAnnotation[] = [
{ kind: "line", axis: "y", value: target.value, label: target.label, tone: "neutral" },
]
return (
<ChartCard
data-widget="widget-saas-revenue-revenue-by-plan"
id={REVENUE_CHART_ID}
title="Revenue by plan"
description={`Monthly recurring revenue, twelve months to ${asOf}`}
height={300}
className="h-full"
>
<StackedBarChart
data={rows}
index="month"
series={series}
height={300}
annotations={annotations}
valueFormatter={(value) => `$${formatCompact(value)}`}
indexFormatter={(value) => MONTH_LABEL.format(new Date(`${value}-01T00:00:00Z`))}
/>
</ChartCard>
)
}"use client"
import { formatCurrency } from "@/lib/format"
import { BigNumber } from "@/components/ui/big-number"
import { Card, CardContent } from "@/components/ui/card"
import { MetricDelta } from "@/components/ui/metric-delta"
import { StatList } from "@/components/ui/stat-list"
import { type RevenueHeadline } from "../data"
/**
* The closing book, counted up, and the four ratios beside it. An island for
* the count; its figures arrive from the server, so the book itself never
* reaches the browser.
*/
export function RevenueHero({ headline }: { headline: RevenueHeadline }) {
return (
<Card data-widget="widget-saas-revenue-revenue-hero">
<CardContent className="grid gap-8 md:grid-cols-[minmax(0,17rem)_1fr] md:items-center md:gap-10">
<div className="flex flex-col gap-3">
<BigNumber
label="Monthly recurring revenue"
value={headline.mrr}
delta={headline.delta}
format={(value) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })}
/>
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground">
<MetricDelta value={headline.yoy} size="sm" />
<span>on this time last year</span>
<span aria-hidden="true">·</span>
<span className="tabular-nums">{headline.customers.toLocaleString("en-US")} customers</span>
</p>
</div>
<StatList divided items={headline.stats} />
</CardContent>
</Card>
)
}"use client"
import * as React from "react"
import { formatCurrency, formatPercent } from "@/lib/format"
import { SectionHeader } from "@/components/ui/section-header"
import { SegmentedControl } from "@/components/ui/segmented-control"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"
import { type RevenueWindow } from "../data"
const OPTIONS = [
{ value: "1m", label: "Month" },
{ value: "3m", label: "Quarter" },
{ value: "12m", label: "Year" },
]
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
const share = (value: number) => formatPercent(value, { maximumFractionDigits: 1 })
/**
* The movement roll-up, at whichever length the reader asks for.
*
* A client island because the window is a choice, and the four figures are
* rolls rather than swaps: changing the window moves each number to its new
* reading, so it is visible which way it went without re-reading the label.
* Every window is computed on the server off the same book, so switching costs
* nothing and shows no spinner.
*/
export function RevenueRollups({ windows }: { windows: RevenueWindow[] }) {
const [key, setKey] = React.useState(windows[0]?.key)
const span = windows.find((window) => window.key === key) ?? windows[0]
const against = `on the ${span.label} before`
return (
<section data-widget="widget-saas-revenue-revenue-rollups" aria-label="Movement" className="flex flex-col gap-3">
<SectionHeader
as="h2"
size="sm"
title="Movement"
description={`What the book won and lost over the ${span.label}.`}
actions={
<SegmentedControl
size="sm"
aria-label="Roll-up window"
options={OPTIONS}
value={span.key}
onValueChange={(value) => setKey(value as RevenueWindow["key"])}
/>
}
/>
<StatCardGroup columns={4}>
<StatCard
label="New"
roll={{ value: span.won, format: money }}
delta={span.wonDelta}
description={against}
/>
<StatCard
label="Churn"
roll={{ value: span.lost, format: money }}
delta={span.lostDelta}
positiveIsGood={false}
description={against}
/>
<StatCard
label="Net new"
roll={{ value: span.net, format: money }}
delta={span.netDelta}
description={against}
/>
<StatCard
label="Growth"
roll={{ value: span.growth, format: share }}
delta={span.growthDelta}
description={against}
/>
</StatCardGroup>
</section>
)
}import { formatCurrency, formatPercent } from "@/lib/format"
import { PercentageBar } from "@/components/ui/percentage-bar"
import { Widget } from "@/components/ui/widget"
import { bookAsOf, enterpriseShare, revenueMix } from "../data"
export function RevenueMix() {
const share = enterpriseShare()
return (
<Widget
data-widget="widget-saas-revenue-revenue-mix"
title="Revenue mix"
description={`${bookAsOf()} MRR by plan`}
className="h-full"
footer={`Enterprise is ${formatPercent(share.now, { maximumFractionDigits: 0 })} of the book, up from ${formatPercent(share.yearAgo, { maximumFractionDigits: 0 })} a year ago.`}
>
<PercentageBar
segments={revenueMix()}
showLegend
showValues
format={(value, percent) =>
`${formatCurrency(value, "USD", { maximumFractionDigits: 0 })} · ${percent}%`
}
/>
</Widget>
)
}import { formatPercent } from "@/lib/format"
import { SimpleTable, type SimpleTableColumn } from "@/components/ui/simple-table"
import { CurrencyCell, DeltaCell } from "@/components/ui/table-cells"
import { Widget } from "@/components/ui/widget"
import { topCustomers, topCustomersShare, type TopCustomer } from "../data"
const COLUMNS: SimpleTableColumn<TopCustomer>[] = [
{ key: "company", header: "Account", cell: (row) => <span className="font-medium">{row.company}</span> },
{ key: "plan", header: "Plan", cell: (row) => <span className="text-muted-foreground">{row.plan}</span> },
{ key: "owner", header: "Owner", cell: (row) => <span className="text-muted-foreground">{row.owner}</span> },
{ key: "mrr", header: "MRR", align: "right", cell: (row) => <CurrencyCell value={row.mrr} /> },
{
key: "delta",
header: "On the month",
align: "right",
cell: (row) => <DeltaCell value={row.delta} />,
},
]
export function TopCustomers() {
const rows = topCustomers()
return (
<Widget
data-widget="widget-saas-revenue-top-customers"
title="Top customers"
description={`Top ${rows.length} accounts by MRR`}
className="h-full"
footer={`Together they are ${formatPercent(topCustomersShare(rows), { maximumFractionDigits: 0 })} of the book.`}
>
<SimpleTable size="sm" columns={COLUMNS} rows={rows} rowKey="company" hoverable />
</Widget>
)
}