← back to Govarbitrage
src/app/reports/page.tsx
111 lines
import Link from "next/link";
import { getReports, type GroupPerf } from "@/lib/reports";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { formatMoney, formatPercent } from "@/lib/utils";
export const dynamic = "force-dynamic";
export const metadata = { title: "Reports — GovArbitrage" };
function PerfTable({ title, rows, label }: { title: string; rows: GroupPerf[]; label: string }) {
return (
<Card>
<CardHeader>
<CardTitle className="text-foreground">{title}</CardTitle>
</CardHeader>
<CardContent>
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-muted-foreground">
<th className="py-1">{label}</th>
<th className="py-1 text-right">Count</th>
<th className="py-1 text-right">Avg ROI</th>
<th className="py-1 text-right">Total Net</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.key} className="border-t">
<td className="py-1">{r.key.replace(/_/g, " ")}</td>
<td className="py-1 text-right tabular-nums">{r.count}</td>
<td className="py-1 text-right tabular-nums">{formatPercent(r.avgRoi)}</td>
<td className="py-1 text-right tabular-nums">{formatMoney(r.totalNet)}</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
);
}
export default async function ReportsPage() {
const rep = await getReports();
const cards = [
{ title: "Total Expected Profit", value: formatMoney(rep.totalExpectedProfit) },
{ title: "Average ROI", value: formatPercent(rep.avgRoi) },
{ title: "Cash Required", value: formatMoney(rep.cashRequired) },
{ title: "Auction Success Rate", value: formatPercent(rep.auctionSuccessRate) },
];
return (
<main className="mx-auto max-w-5xl space-y-5 p-5">
<div className="text-sm">
<Link href="/" className="text-primary hover:underline">
← Dashboard
</Link>
</div>
<h1 className="text-2xl font-semibold tracking-tight">Reports</h1>
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
{cards.map((c) => (
<Card key={c.title}>
<CardHeader className="pb-1">
<CardTitle>{c.title}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold tabular-nums">{c.value}</div>
</CardContent>
</Card>
))}
</div>
<div className="grid gap-4 md:grid-cols-2">
<PerfTable title="Category Performance" rows={rep.categoryPerformance} label="Category" />
<PerfTable title="Marketplace / Source Performance" rows={rep.sourcePerformance} label="Source" />
</div>
<Card>
<CardHeader>
<CardTitle className="text-foreground">Inventory by Research Status</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-4 text-sm">
{rep.inventoryByStatus.map((s) => (
<div key={s.status}>
<span className="text-muted-foreground">{s.status}: </span>
<span className="font-semibold tabular-nums">{s.count}</span>
</div>
))}
</CardContent>
</Card>
{rep.estimatedVsActual.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-foreground">Estimated vs Actual (won lots)</CardTitle>
</CardHeader>
<CardContent className="space-y-1 text-sm">
{rep.estimatedVsActual.map((e) => (
<div key={e.title} className="flex justify-between">
<span className="truncate">{e.title}</span>
<span className="tabular-nums">
{formatMoney(e.estimated)} → {formatMoney(e.actual)}
</span>
</div>
))}
</CardContent>
</Card>
)}
</main>
);
}