Add interactive analytics drill-downs
Some checks are pending
CI / verify (push) Waiting to run

Make demo search, filters, exports, and controls functional while exposing dedicated KPI detail pages with tested breakdowns.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-25 13:14:22 -07:00
parent 135be480e5
commit 49f2c28a46
22 changed files with 1090 additions and 104 deletions

View File

@@ -1,25 +1,51 @@
import { Building2, Download, TrendingUp } from "lucide-react";
import { Building2, TrendingUp } from "lucide-react";
import { getDashboardData } from "@/domain/analytics";
import { currency, percent } from "@/lib/format";
import { Card, PageHeader, secondaryButtonStyles } from "@/components/ui";
import { Card, PageHeader } from "@/components/ui";
import { PerformanceTable } from "@/components/performance-table";
import { CsvExportButton } from "@/components/csv-export-button";
export const metadata = { title: "Branch Performance" };
export default function BranchesPage() {
export default async function BranchesPage({
searchParams,
}: {
searchParams: Promise<{ query?: string }>;
}) {
const { branches } = getDashboardData();
const top = branches[0];
const { query = "" } = await searchParams;
const visibleBranches = query
? branches.filter((branch) =>
`${branch.name} ${branch.secondary}`.toLowerCase().includes(query.toLowerCase()),
)
: branches;
const top = visibleBranches[0];
return (
<div className="space-y-6">
<PageHeader eyebrow="Performance" title="Branches"
description="Compare revenue, sell-through, and inventory health across every location."
actions={<button className={secondaryButtonStyles}><Download className="size-3.5" />Export report</button>} />
actions={<CsvExportButton
filename="solaire-branch-performance.csv"
label="Export report"
rows={[
["Branch", "Location", "Revenue", "Units", "Sell-through", "Freshness"],
...visibleBranches.map((branch) => [
branch.name,
branch.secondary,
branch.revenue.toFixed(2),
branch.units,
branch.sellThrough.toFixed(1),
branch.freshness.toFixed(1),
]),
]}
/>} />
<div className="grid gap-4 md:grid-cols-3">
<Summary label="Top branch" value={top.name} detail={`${currency(top.revenue)} this month`} icon={<Building2 className="size-5" />} />
<Summary label="Best sell-through" value={percent(Math.max(...branches.map((b) => b.sellThrough)))} detail="Across the active catalog" icon={<TrendingUp className="size-5" />} />
<Summary label="Network revenue" value={currency(branches.reduce((s, b) => s + b.revenue, 0), true)} detail={`${branches.length} reporting locations`} icon={<Building2 className="size-5" />} />
<Summary label="Top branch" value={top?.name ?? "No match"} detail={top ? `${currency(top.revenue)} this month` : "Try another search"} icon={<Building2 className="size-5" />} />
<Summary label="Best sell-through" value={percent(visibleBranches.length ? Math.max(...visibleBranches.map((b) => b.sellThrough)) : 0)} detail="Across the active catalog" icon={<TrendingUp className="size-5" />} />
<Summary label="Network revenue" value={currency(visibleBranches.reduce((s, b) => s + b.revenue, 0), true)} detail={`${visibleBranches.length} reporting locations`} icon={<Building2 className="size-5" />} />
</div>
<PerformanceTable rows={branches} entityLabel="Branch" />
{query && <p className="text-xs font-semibold text-slate-500">Showing branch results for {query}.</p>}
<PerformanceTable rows={visibleBranches} entityLabel="Branch" />
</div>
);
}

View File

@@ -1,22 +1,16 @@
import Link from "next/link";
import { Download, Filter, Search } from "lucide-react";
import { getDashboardData } from "@/domain/analytics";
import { currency } from "@/lib/format";
import {
Card, FreshnessBadge, FreshnessBar, PageHeader, secondaryButtonStyles,
} from "@/components/ui";
import { Card, PageHeader } from "@/components/ui";
import { InventoryControls } from "@/components/inventory-controls";
export const metadata = { title: "Inventory" };
export default function InventoryPage() {
const { inventory } = getDashboardData();
const ordered = [...inventory].sort((a, b) => a.score - b.score);
return (
<div className="space-y-6">
<PageHeader eyebrow="Stock health" title="Inventory"
description="Track every active batch by location, age, value, and estimated remaining shelf life."
actions={<button className={secondaryButtonStyles}><Download className="size-3.5" />Export CSV</button>} />
description="Track every active batch by location, age, value, and estimated remaining shelf life." />
<div className="grid gap-3 sm:grid-cols-4">
{[
@@ -32,41 +26,7 @@ export default function InventoryPage() {
))}
</div>
<Card className="overflow-hidden">
<div className="flex flex-col gap-3 border-b border-slate-100 p-4 sm:flex-row">
<label className="relative flex-1">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-slate-400" />
<input placeholder="Search product or SKU…" className="h-10 w-full rounded-xl border border-slate-200 bg-slate-50 pl-9 pr-3 text-sm outline-none focus:border-emerald-400" />
</label>
<button className={secondaryButtonStyles}><Filter className="size-3.5" />Filter by branch</button>
<button className={secondaryButtonStyles}>All categories</button>
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[900px] text-left">
<thead><tr className="border-b border-slate-100 bg-slate-50/70 text-[10px] font-bold uppercase tracking-wider text-slate-400">
<th className="px-5 py-3">Product / batch</th><th className="px-4 py-3">Location</th>
<th className="px-4 py-3">Received</th><th className="px-4 py-3">Freshness</th>
<th className="px-4 py-3">On hand</th><th className="px-4 py-3">Value</th><th className="px-5 py-3">Health</th>
</tr></thead>
<tbody>
{ordered.map((item) => (
<tr key={item.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
<td className="px-5 py-4">
<Link href={`/products/${item.productId}`} className="text-sm font-bold hover:text-emerald-700">{item.product.name}</Link>
<p className="mt-0.5 text-[11px] text-slate-400">{item.product.sku} · {item.id.toUpperCase()}</p>
</td>
<td className="px-4 py-4"><p className="text-xs font-semibold">{item.branch.name}</p><p className="text-[11px] text-slate-400">{item.branch.city}</p></td>
<td className="px-4 py-4 text-xs text-slate-600">{new Date(item.receivedAt).toLocaleDateString("en-US", { month: "short", day: "numeric" })}<p className="text-[11px] text-slate-400">{item.ageDays} days ago</p></td>
<td className="px-4 py-4"><FreshnessBar value={item.score} status={item.status} /><p className="mt-1 text-[10px] text-slate-400">{item.remainingDays > 0 ? `${item.remainingDays} days remaining` : `${Math.abs(item.remainingDays)} days past estimate`}</p></td>
<td className="px-4 py-4 text-xs font-semibold">{item.quantityOnHand} {item.product.unit}s</td>
<td className="px-4 py-4 text-xs font-semibold">{currency(item.retailValue)}</td>
<td className="px-5 py-4"><FreshnessBadge status={item.status} /></td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
<InventoryControls inventory={inventory} />
<p className="text-center text-[11px] text-slate-400">Freshness is estimated from received date and configured shelf life; it is not a sensor reading.</p>
</div>
);

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { DashboardShell } from "@/components/dashboard-shell";
import { branches, products, salespeople } from "@/lib/demo-data";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -26,13 +27,43 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const searchItems = [
...products.map((product) => ({
id: product.id,
label: product.name,
detail: `${product.sku} · ${product.category}`,
href: `/products/${product.id}`,
keywords: `${product.name} ${product.sku} ${product.category}`,
type: "Product",
})),
...branches.map((branch) => ({
id: branch.id,
label: branch.name,
detail: `${branch.city}, ${branch.region}`,
href: `/branches?query=${encodeURIComponent(branch.city)}`,
keywords: `${branch.name} ${branch.city} ${branch.region}`,
type: "Branch",
})),
...salespeople.map((person) => {
const branch = branches.find((item) => item.id === person.branchId);
return {
id: person.id,
label: person.name,
detail: branch?.name ?? "Sales team",
href: `/sales-team?query=${encodeURIComponent(person.name)}`,
keywords: `${person.name} ${person.email} ${branch?.name ?? ""}`,
type: "Salesperson",
};
}),
];
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full">
<DashboardShell>{children}</DashboardShell>
<DashboardShell searchItems={searchItems}>{children}</DashboardShell>
</body>
</html>
);

View File

@@ -0,0 +1,59 @@
import Link from "next/link";
import { Boxes, CircleDollarSign, PackageX, TriangleAlert } from "lucide-react";
import { getMetricDetails } from "@/domain/analytics";
import { currency, number } from "@/lib/format";
import { BackToOverview, DetailStat } from "@/components/metric-detail";
import { BreakdownBarChart } from "@/components/charts";
import { Card, CardHeader, FreshnessBadge, FreshnessBar, PageHeader } from "@/components/ui";
import { CsvExportButton } from "@/components/csv-export-button";
import { freshnessLabels } from "@/domain/freshness";
export const metadata = { title: "At-Risk Exposure" };
export default function AtRiskPage() {
const { risk } = getMetricDetails();
const attention = risk.byStatus.filter((row) => row.status !== "fresh");
return (
<div className="space-y-6">
<BackToOverview />
<PageHeader eyebrow="Metric drill-down" title="At-risk exposure"
description="See the cost and retail value tied to stock that has reached at-risk or expired status."
actions={<CsvExportButton filename="solaire-at-risk-inventory.csv" rows={[
["Product", "SKU", "Batch", "Branch", "Status", "Freshness", "Units", "Cost exposure", "Retail exposure"],
...risk.affected.map((item) => [item.product.name, item.product.sku, item.id, item.branch.name, item.status, item.score, item.quantityOnHand, item.costExposure.toFixed(2), item.retailValue.toFixed(2)]),
]} />} />
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<DetailStat label="Cost exposure" value={currency(risk.costValue)} detail="Capital at risk" icon={<TriangleAlert className="size-5" />} />
<DetailStat label="Retail exposure" value={currency(risk.retailValue)} detail="Potential revenue affected" icon={<CircleDollarSign className="size-5" />} />
<DetailStat label="Affected units" value={number(risk.units)} detail="At-risk and expired cases" icon={<Boxes className="size-5" />} />
<DetailStat label="Affected batches" value={number(risk.batches)} detail="Prioritized below" icon={<PackageX className="size-5" />} />
</div>
<div className="grid gap-6 xl:grid-cols-[.8fr_1.2fr]">
<Card><CardHeader title="Exposure by status" description="Watch stock is shown for early intervention" /><div className="p-4"><BreakdownBarChart data={attention.map((row) => ({ label: freshnessLabels[row.status], value: row.costValue }))} valueLabel="Cost exposure" /></div></Card>
<Card className="overflow-hidden">
<CardHeader title="Affected inventory" description="Lowest freshness score first" />
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] text-left">
<thead><tr className="border-b border-slate-100 bg-slate-50/70 text-[10px] font-bold uppercase tracking-wider text-slate-400">
<th className="px-5 py-3">Product</th><th className="px-4 py-3">Branch</th><th className="px-4 py-3">Freshness</th><th className="px-4 py-3">Cost exposure</th><th className="px-5 py-3">Status</th>
</tr></thead>
<tbody>{risk.affected.map((item) => (
<tr key={item.id} className="border-b border-slate-100 last:border-0">
<td className="px-5 py-4"><Link href={`/products/${item.productId}`} className="text-sm font-bold hover:text-emerald-700">{item.product.name}</Link><p className="text-[11px] text-slate-400">{item.product.sku} · {item.id.toUpperCase()}</p></td>
<td className="px-4 py-4 text-xs font-semibold">{item.branch.name}</td>
<td className="px-4 py-4"><FreshnessBar value={item.score} status={item.status} /></td>
<td className="px-4 py-4 text-xs font-bold">{currency(item.costExposure)}</td>
<td className="px-5 py-4"><FreshnessBadge status={item.status} /></td>
</tr>
))}</tbody>
</table>
</div>
</Card>
</div>
<p className="text-center text-[11px] text-slate-400">Exposure is advisory and based on received-date freshness estimates. Confirm physical quality before acting.</p>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { Boxes, Layers3, Leaf, Scale } from "lucide-react";
import { getMetricDetails } from "@/domain/analytics";
import { number, percent } from "@/lib/format";
import { BackToOverview, DetailStat, RankedMetricTable } from "@/components/metric-detail";
import { BreakdownBarChart } from "@/components/charts";
import { Card, CardHeader, PageHeader } from "@/components/ui";
import { CsvExportButton } from "@/components/csv-export-button";
import { freshnessLabels } from "@/domain/freshness";
export const metadata = { title: "Freshness" };
export default function FreshnessPage() {
const { freshness } = getMetricDetails();
const totalUnits = freshness.byStatus.reduce((sum, row) => sum + row.units, 0);
return (
<div className="space-y-6">
<BackToOverview />
<PageHeader eyebrow="Metric drill-down" title="Average freshness"
description="Compare remaining shelf-life estimates across branches, categories, products, and stock-health states."
actions={<CsvExportButton filename="solaire-freshness.csv" rows={[
["Product", "SKU", "Batches", "Units", "Average score", "Weighted score"],
...freshness.byProduct.map((row) => [row.label, row.secondary, row.batches, row.units, row.averageScore.toFixed(1), row.weightedScore.toFixed(1)]),
]} />} />
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<DetailStat label="Batch average" value={percent(freshness.averageScore)} detail="Every batch weighted equally" icon={<Leaf className="size-5" />} />
<DetailStat label="Unit-weighted average" value={percent(freshness.weightedScore)} detail="Larger batches carry more weight" icon={<Scale className="size-5" />} />
<DetailStat label="Fresh units" value={percent(freshness.freshUnitsPercent)} detail="Units above the watch threshold" icon={<Boxes className="size-5" />} />
<DetailStat label="Tracked units" value={number(totalUnits)} detail="Cases across active batches" icon={<Layers3 className="size-5" />} />
</div>
<div className="grid gap-6 xl:grid-cols-[.8fr_1.2fr]">
<Card><CardHeader title="Units by freshness state" description="Current stock-health distribution" /><div className="p-4"><BreakdownBarChart data={freshness.byStatus.map((row) => ({ label: freshnessLabels[row.status], value: row.units }))} valueLabel="Cases" format="number" /></div></Card>
<Card className="overflow-hidden"><CardHeader title="Branch freshness" description="Lowest unit-weighted score first" /><RankedMetricTable rows={freshness.byBranch.map((row) => ({ ...row, value: row.weightedScore }))} metricLabel="Freshness" formatValue={percent} /></Card>
</div>
<Card className="overflow-hidden"><CardHeader title="Product freshness" description="Use low scores to prioritize inspection and movement" /><RankedMetricTable rows={freshness.byProduct.map((row) => ({ ...row, value: row.weightedScore }))} metricLabel="Freshness" formatValue={percent} /></Card>
<p className="text-center text-[11px] text-slate-400">Freshness is estimated from received date and configured shelf life; it is not a sensor or inspection reading.</p>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { Boxes, CircleDollarSign, PackageCheck, TrendingUp } from "lucide-react";
import { getMetricDetails } from "@/domain/analytics";
import { currency, number, percent } from "@/lib/format";
import { BackToOverview, DetailStat, RankedMetricTable } from "@/components/metric-detail";
import { BreakdownBarChart } from "@/components/charts";
import { Card, CardHeader, PageHeader } from "@/components/ui";
import { CsvExportButton } from "@/components/csv-export-button";
export const metadata = { title: "Inventory Value" };
export default function InventoryValuePage() {
const { inventoryValue } = getMetricDetails();
const marginPercent = inventoryValue.retailValue
? (inventoryValue.potentialMargin / inventoryValue.retailValue) * 100
: 0;
return (
<div className="space-y-6">
<BackToOverview />
<PageHeader eyebrow="Metric drill-down" title="Inventory value"
description="Understand where current retail value, cost basis, and potential margin are concentrated."
actions={<CsvExportButton filename="solaire-inventory-value.csv" rows={[
["Product", "SKU", "Units", "Retail value", "Cost basis", "Potential margin"],
...inventoryValue.byProduct.map((row) => [row.label, row.secondary, row.units, row.retailValue.toFixed(2), row.costValue.toFixed(2), row.potentialMargin.toFixed(2)]),
]} />} />
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<DetailStat label="Retail value" value={currency(inventoryValue.retailValue)} detail="At current list prices" icon={<CircleDollarSign className="size-5" />} />
<DetailStat label="Cost basis" value={currency(inventoryValue.costValue)} detail="Capital currently in stock" icon={<Boxes className="size-5" />} />
<DetailStat label="Potential margin" value={currency(inventoryValue.potentialMargin)} detail={`${percent(marginPercent)} at list price`} icon={<TrendingUp className="size-5" />} />
<DetailStat label="Units on hand" value={number(inventoryValue.units)} detail="Cases across active batches" icon={<PackageCheck className="size-5" />} />
</div>
<div className="grid gap-6 xl:grid-cols-[1fr_1.15fr]">
<Card><CardHeader title="Value by category" description="Retail value contribution" /><div className="p-4"><BreakdownBarChart data={inventoryValue.byCategory.map((row) => ({ label: row.label, value: row.retailValue }))} valueLabel="Retail value" /></div></Card>
<Card className="overflow-hidden"><CardHeader title="Branch concentration" description="Value and units by location" /><RankedMetricTable rows={inventoryValue.byBranch.map((row) => ({ ...row, value: row.retailValue }))} metricLabel="Retail value" formatValue={currency} /></Card>
</div>
<Card className="overflow-hidden"><CardHeader title="Product value" description="Highest-value products first" /><RankedMetricTable rows={inventoryValue.byProduct.map((row) => ({ ...row, value: row.retailValue }))} metricLabel="Retail value" formatValue={currency} /></Card>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { CalendarDays, CircleDollarSign, ReceiptText, ShoppingCart } from "lucide-react";
import { getDashboardData, getMetricDetails } from "@/domain/analytics";
import { currency, number, percent } from "@/lib/format";
import { BackToOverview, DetailStat, RankedMetricTable } from "@/components/metric-detail";
import { RevenueChart } from "@/components/charts";
import { Card, CardHeader, PageHeader } from "@/components/ui";
import { CsvExportButton } from "@/components/csv-export-button";
export const metadata = { title: "Revenue" };
export default function RevenuePage() {
const data = getDashboardData();
const { revenue } = getMetricDetails();
return (
<div className="space-y-6">
<BackToOverview />
<PageHeader eyebrow="Metric drill-down" title="Revenue · 30 days"
description="Trace revenue momentum to the branches, categories, and products creating it."
actions={<CsvExportButton filename="solaire-revenue-30d.csv" rows={[
["Product", "SKU", "Revenue", "Units", "Average price"],
...revenue.byProduct.map((row) => [row.label, row.secondary, row.revenue.toFixed(2), row.units, row.averagePrice.toFixed(2)]),
]} />} />
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<DetailStat label="30-day revenue" value={currency(revenue.current30d)} detail={`${percent(revenue.changePercent)} vs prior 30 days`} icon={<CircleDollarSign className="size-5" />} />
<DetailStat label="Prior 30 days" value={currency(revenue.prior30d)} detail="Complete comparison period" icon={<CalendarDays className="size-5" />} />
<DetailStat label="Units sold" value={number(revenue.units)} detail="Cases sold in current period" icon={<ShoppingCart className="size-5" />} />
<DetailStat label="Average daily revenue" value={currency(revenue.averageDailyRevenue)} detail={`${currency(revenue.averageOrderPrice)} per sales record`} icon={<ReceiptText className="size-5" />} />
</div>
<Card><CardHeader title="Revenue momentum" description="Four-day revenue buckets across the current period" /><div className="px-4 py-5"><RevenueChart data={data.trends} /></div></Card>
<div className="grid gap-6 xl:grid-cols-2">
<Card className="overflow-hidden"><CardHeader title="Revenue by branch" description="Highest contribution first" /><RankedMetricTable rows={revenue.byBranch.map((row) => ({ ...row, value: row.revenue }))} metricLabel="Revenue" formatValue={currency} /></Card>
<Card className="overflow-hidden"><CardHeader title="Revenue by product" description="Product-level performance" /><RankedMetricTable rows={revenue.byProduct.map((row) => ({ ...row, value: row.revenue }))} metricLabel="Revenue" formatValue={currency} /></Card>
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import Link from "next/link";
import {
ArrowDownRight, ArrowRight, ArrowUpRight, Boxes, CircleDollarSign,
ArrowRight, Boxes, CircleDollarSign,
Clock3, Leaf, RefreshCw, ShoppingCart, Sparkles, TriangleAlert,
} from "lucide-react";
import { getDashboardData } from "@/domain/analytics";
@@ -9,6 +9,7 @@ import {
Card, CardHeader, FreshnessBadge, FreshnessBar, PageHeader, secondaryButtonStyles,
} from "@/components/ui";
import { RevenueChart } from "@/components/charts";
import { MetricCard } from "@/components/metric-card";
export default function Home() {
const data = getDashboardData();
@@ -41,16 +42,16 @@ export default function Home() {
/>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<MetricCard label="Inventory value" value={currency(data.metrics.retailValue, true)}
<MetricCard href="/metrics/inventory-value" label="Inventory value" value={currency(data.metrics.retailValue, true)}
detail={`${number(data.metrics.unitsOnHand)} cases on hand`} change={data.metrics.change.unitsOnHand}
icon={<Boxes className="size-5" />} tone="emerald" />
<MetricCard label="Revenue · 30 days" value={currency(data.metrics.revenue30d, true)}
<MetricCard href="/metrics/revenue" label="Revenue · 30 days" value={currency(data.metrics.revenue30d, true)}
detail={`${percent(data.metrics.sellThrough30d)} sell-through`} change={data.metrics.change.revenue}
icon={<ShoppingCart className="size-5" />} tone="blue" />
<MetricCard label="At-risk exposure" value={currency(data.metrics.atRiskValue, true)}
<MetricCard href="/metrics/at-risk" label="At-risk exposure" value={currency(data.metrics.atRiskValue, true)}
detail={`${riskItems.length} batches need attention`} change={data.metrics.change.atRisk}
icon={<TriangleAlert className="size-5" />} tone="amber" invertChange />
<MetricCard label="Average freshness" value={percent(data.metrics.averageFreshness)}
<MetricCard href="/metrics/freshness" label="Average freshness" value={percent(data.metrics.averageFreshness)}
detail="Across active inventory" change={3.1}
icon={<Leaf className="size-5" />} tone="violet" />
</div>
@@ -139,27 +140,3 @@ export default function Home() {
</div>
);
}
function MetricCard({ label, value, detail, change, icon, tone, invertChange = false }: {
label: string; value: string; detail: string; change: number; icon: React.ReactNode;
tone: "emerald" | "blue" | "amber" | "violet"; invertChange?: boolean;
}) {
const positive = invertChange ? change < 0 : change > 0;
const tones = {
emerald: "bg-emerald-50 text-emerald-700", blue: "bg-blue-50 text-blue-700",
amber: "bg-amber-50 text-amber-700", violet: "bg-violet-50 text-violet-700",
};
return (
<Card className="p-5">
<div className="flex items-start justify-between">
<span className={`grid size-10 place-items-center rounded-xl ${tones[tone]}`}>{icon}</span>
<span className={`flex items-center text-[11px] font-bold ${positive ? "text-emerald-600" : "text-rose-600"}`}>
{change > 0 ? <ArrowUpRight className="size-3.5" /> : <ArrowDownRight className="size-3.5" />}{Math.abs(change)}%
</span>
</div>
<p className="mt-5 text-xs font-semibold text-slate-500">{label}</p>
<p className="mt-1 text-2xl font-bold tracking-tight">{value}</p>
<p className="mt-1 text-[11px] text-slate-400">{detail}</p>
</Card>
);
}

View File

@@ -6,19 +6,30 @@ import { PerformanceTable } from "@/components/performance-table";
export const metadata = { title: "Sales Team" };
export default function SalesTeamPage() {
export default async function SalesTeamPage({
searchParams,
}: {
searchParams: Promise<{ query?: string }>;
}) {
const { salespeople } = getDashboardData();
const totalRevenue = salespeople.reduce((sum, row) => sum + row.revenue, 0);
const { query = "" } = await searchParams;
const visiblePeople = query
? salespeople.filter((person) =>
`${person.name} ${person.secondary}`.toLowerCase().includes(query.toLowerCase()),
)
: salespeople;
const totalRevenue = visiblePeople.reduce((sum, row) => sum + row.revenue, 0);
return (
<div className="space-y-6">
<PageHeader eyebrow="Performance" title="Sales team"
description="See who is moving the most produce and whether those sales are improving stock health." />
<div className="grid gap-4 md:grid-cols-3">
<TeamMetric icon={<Award className="size-5" />} label="Sales leader" value={salespeople[0].name} detail={currency(salespeople[0].revenue)} />
<TeamMetric icon={<Award className="size-5" />} label="Sales leader" value={visiblePeople[0]?.name ?? "No match"} detail={visiblePeople[0] ? currency(visiblePeople[0].revenue) : "Try another search"} />
<TeamMetric icon={<Target className="size-5" />} label="Team revenue" value={currency(totalRevenue, true)} detail="Last 30 days" />
<TeamMetric icon={<Users className="size-5" />} label="Active sellers" value={number(salespeople.length)} detail="Across four branches" />
<TeamMetric icon={<Users className="size-5" />} label="Active sellers" value={number(visiblePeople.length)} detail="Across four branches" />
</div>
<PerformanceTable rows={salespeople} entityLabel="Salesperson" />
{query && <p className="text-xs font-semibold text-slate-500">Showing salesperson results for {query}.</p>}
<PerformanceTable rows={visiblePeople} entityLabel="Salesperson" />
<Card className="border-blue-100 bg-blue-50/60 p-5">
<p className="text-sm font-bold text-blue-900">Salesforce mapping preview</p>
<p className="mt-1 text-xs leading-5 text-blue-700">Demo sellers and orders use Solaires normalized model. A future Salesforce adapter can map Users, Accounts, Orders, Opportunities, and custom branch fields without changing these analytics.</p>