/ecommerce/salesSales dashboard
A sales page: bookings, open pipeline and win rate, the quarter's quota as a goal tracker, booked against fulfilled value by month, the pipeline by stage, the rep leaderboard, and every deal on the board.
The page is a server component inside AppShell: it reads the order book through db, turns it into deals, and hands plain rows to the client islands that draw them, so the data layer stays off the browser bundle. Deals are db.orders, the accounts behind them db.customers, and the reps 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 at 15% over the quarter before it and rounded to the nearest $25k. The revenue chart covers only the complete months on the book, because the 180-day order window opens mid-March. Composes AppShell, PageHeader, StatCardGroup, StatCard, GoalTracker, DashboardGrid, ChartCard, AreaChart, Widget, FunnelChart, RankList, DataTable, StatusBadge, CurrencyCell, DateCell and TruncateCell.
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 { DealsTable } from "./components/deals-table"
import { PipelineByStage } from "./components/pipeline-by-stage"
import { RepsLeaderboard } from "./components/reps-leaderboard"
import { RevenueByMonth } from "./components/revenue-by-month"
import { SalesStats } from "./components/sales-stats"
import {
currentUser,
deals,
lastUpdated,
MONTHS,
openDeals,
pipelineByStage,
quota,
repLeaderboard,
revenueByMonth,
salesStats,
shellNotifications,
} from "./data"
import { NAV, ROUTES } from "@/lib/dashboards/ecommerce/nav"
/**
* The sales page. A server component: it reads the order book through `db`,
* turns it into deals, 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 SalesPage() {
return (
<AppShell
nav={NAV}
activeHref={ROUTES.sales}
user={currentUser()}
notifications={shellNotifications()}
now={REFERENCE_DATE}
onSignOut={signOut}
>
<PageHeader
title="Sales"
description="Bookings, pipeline and the quarter's target."
meta={lastUpdated()}
/>
<SalesStats stats={salesStats()} quota={quota()} />
<DashboardGrid>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<RevenueByMonth rows={revenueByMonth()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<PipelineByStage steps={pipelineByStage()} openDeals={openDeals()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<RepsLeaderboard rows={repLeaderboard()} months={MONTHS} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<DealsTable rows={deals()} />
</DashboardGridItem>
</DashboardGrid>
</AppShell>
)
}Install
npx shadcn@latest add @vibra/ecommerce-salesNeeds 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 { DealsTable } from "./components/deals-table"
import { PipelineByStage } from "./components/pipeline-by-stage"
import { RepsLeaderboard } from "./components/reps-leaderboard"
import { RevenueByMonth } from "./components/revenue-by-month"
import { SalesStats } from "./components/sales-stats"
import {
currentUser,
deals,
lastUpdated,
MONTHS,
openDeals,
pipelineByStage,
quota,
repLeaderboard,
revenueByMonth,
salesStats,
shellNotifications,
} from "./data"
import { NAV, ROUTES } from "@/lib/dashboards/ecommerce/nav"
/**
* The sales page. A server component: it reads the order book through `db`,
* turns it into deals, 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 SalesPage() {
return (
<AppShell
nav={NAV}
activeHref={ROUTES.sales}
user={currentUser()}
notifications={shellNotifications()}
now={REFERENCE_DATE}
onSignOut={signOut}
>
<PageHeader
title="Sales"
description="Bookings, pipeline and the quarter's target."
meta={lastUpdated()}
/>
<SalesStats stats={salesStats()} quota={quota()} />
<DashboardGrid>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<RevenueByMonth rows={revenueByMonth()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<PipelineByStage steps={pipelineByStage()} openDeals={openDeals()} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 4 }}>
<RepsLeaderboard rows={repLeaderboard()} months={MONTHS} />
</DashboardGridItem>
<DashboardGridItem colSpan={{ base: 12, lg: 8 }}>
<DealsTable rows={deals()} />
</DashboardGridItem>
</DashboardGrid>
</AppShell>
)
}/**
* 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()}`
}"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 } }
}"use client"
import * as React from "react"
import { ArrowRightIcon, CalendarPlusIcon, HandshakeIcon } 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, TruncateCell } from "@/components/ui/table-cells"
import { Widget } from "@/components/ui/widget"
import { type Deal } from "../data"
// Stages are not status words, so none of them resolve by default: early stages
// read as information, the two closed outcomes as the outcome they are.
const STAGE_MAP: Record<string, StatusVariant> = {
Qualified: "info",
Discovery: "info",
Proposal: "primary",
Negotiation: "warning",
"Closed won": "success",
"Closed lost": "danger",
}
const STAGE_FACET: DataTableFacet<Deal>[] = [
{
columnId: "stage",
title: "Stage",
options: Object.keys(STAGE_MAP).map((stage) => ({ label: stage, value: stage })),
},
]
export function DealsTable({ rows }: { rows: Deal[] }) {
// The title names the table too, so a screen reader announces it by name.
const titleId = React.useId()
const columns = React.useMemo<DataTableColumnDef<Deal>[]>(
() => [
{
accessorKey: "number",
header: ({ column }) => <DataTableColumnHeader column={column} title="Deal" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.number}</span>,
meta: { label: "Deal" },
},
{
accessorKey: "account",
header: ({ column }) => <DataTableColumnHeader column={column} title="Account" />,
cell: ({ row }) => <TruncateCell maxWidth={200}>{row.original.account}</TruncateCell>,
meta: { label: "Account" },
},
{
accessorKey: "rep",
header: ({ column }) => <DataTableColumnHeader column={column} title="Rep" />,
cell: ({ row }) => <span className="text-muted-foreground">{row.original.rep}</span>,
meta: { label: "Rep" },
},
{
accessorKey: "stage",
header: ({ column }) => <DataTableColumnHeader column={column} title="Stage" />,
cell: ({ row }) => <StatusBadge status={row.original.stage} map={STAGE_MAP} />,
meta: { label: "Stage" },
},
{
accessorKey: "value",
header: ({ column }) => <DataTableColumnHeader column={column} title="Value" />,
cell: ({ row }) => <CurrencyCell value={row.original.value} />,
meta: { align: "right", label: "Value" },
},
{
accessorKey: "placedAt",
header: ({ column }) => <DataTableColumnHeader column={column} title="Opened" />,
cell: ({ row }) => (
<DateCell date={row.original.placedAt} className="text-muted-foreground" />
),
meta: { align: "right", label: "Opened" },
},
{
id: "actions",
size: 44,
enableSorting: false,
enableHiding: false,
cell: ({ row }) => (
<DataTableRowActions
label={`Open menu for ${row.original.number}`}
actions={[
{ label: "Open deal", icon: <ArrowRightIcon />, onSelect: () => {} },
{ label: "Log a call", icon: <CalendarPlusIcon />, onSelect: () => {} },
{
label: "Mark closed won",
icon: <HandshakeIcon />,
separatorBefore: true,
onSelect: () => {},
},
]}
/>
),
},
],
[]
)
return (
<Widget titleId={titleId} data-widget="widget-ecommerce-sales-deals-table" title="Deals" description="Every deal on the board, newest first">
<DataTable
aria-labelledby={titleId}
size="sm"
columns={columns}
data={rows}
pageSize={8}
searchKey="account"
searchPlaceholder="Search accounts…"
facets={STAGE_FACET}
enableRowSelection={false}
getRowId={(deal) => deal.id}
initialSorting={[{ id: "placedAt", desc: true }]}
/>
</Widget>
)
}"use client"
import { formatCurrency } from "@/lib/format"
import { FunnelChart } from "@/components/ui/funnel-chart"
import { Widget } from "@/components/ui/widget"
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
export type PipelineByStageProps = {
steps: { label: string; value: number }[]
openDeals: number
}
/** Open value that has reached each stage, so the funnel narrows as it should. */
export function PipelineByStage({ steps, openDeals }: PipelineByStageProps) {
return (
<Widget
data-widget="widget-ecommerce-sales-pipeline-by-stage"
title="Pipeline by stage"
description="Open value that has reached each stage"
footer={`${openDeals} deals open`}
className="h-full"
>
<FunnelChart steps={steps} valueFormatter={money} />
</Widget>
)
}"use client"
import { formatCurrency } from "@/lib/format"
import { RankList } from "@/components/ui/rank-list"
import { Widget } from "@/components/ui/widget"
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
export type RepsLeaderboardProps = {
rows: { label: string; value: number }[]
months: number
}
/** Who closed what: every deal credits the rep who owns the account behind it. */
export function RepsLeaderboard({ rows, months }: RepsLeaderboardProps) {
return (
<Widget
data-widget="widget-ecommerce-sales-reps-leaderboard"
title="Reps by bookings"
description={`Closed-won value, last ${months} months`}
className="h-full"
>
<RankList items={rows} showRank color="chart-2" format={money} />
</Widget>
)
}"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>
)
}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>
)
}