139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { usePoll } from "@/lib/use-poll";
|
|
import type { ExpenseSummary } from "@/lib/types";
|
|
import { StatCard } from "@/components/stat-card";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { DataTable, Column } from "@/components/data-table";
|
|
|
|
interface Transaction {
|
|
id: string;
|
|
date: string;
|
|
vendor: string;
|
|
amount: number;
|
|
category: string;
|
|
}
|
|
|
|
export default function ExpensesPage() {
|
|
const { data: summary } = usePoll<ExpenseSummary>(
|
|
"/api/expenses/summary",
|
|
120000
|
|
);
|
|
const { data: expenseData } = usePoll<Transaction[] | { count: number; expenses: Transaction[] }>(
|
|
"/api/expenses",
|
|
120000
|
|
);
|
|
const transactions = Array.isArray(expenseData) ? expenseData : (expenseData?.expenses ?? []);
|
|
|
|
const maxVendor =
|
|
summary?.top_vendors.reduce(
|
|
(max, v) => Math.max(max, v.amount),
|
|
0
|
|
) ?? 1;
|
|
|
|
const txColumns: Column<Transaction>[] = [
|
|
{ key: "date", label: "Date" },
|
|
{
|
|
key: "vendor",
|
|
label: "Vendor",
|
|
render: (row) => (
|
|
<span className="font-medium text-foreground">{row.vendor}</span>
|
|
),
|
|
},
|
|
{
|
|
key: "amount",
|
|
label: "Amount",
|
|
render: (row) => (
|
|
<span className="text-foreground">
|
|
${row.amount.toFixed(2)}
|
|
</span>
|
|
),
|
|
},
|
|
{ key: "category", label: "Category" },
|
|
];
|
|
|
|
const categories = transactions
|
|
? [...new Set(transactions.map((t) => t.category))]
|
|
: [];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-lg font-semibold">Expenses</h1>
|
|
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<StatCard
|
|
label="Total Spend"
|
|
value={summary ? `$${summary.total.toFixed(2)}` : "\u2014"}
|
|
sub={summary?.month}
|
|
/>
|
|
<StatCard
|
|
label="Transactions"
|
|
value={summary?.count ?? "\u2014"}
|
|
sub="this month"
|
|
/>
|
|
<StatCard
|
|
label="Top Vendor"
|
|
value={
|
|
summary?.top_vendors?.[0]?.vendor ?? "\u2014"
|
|
}
|
|
sub={
|
|
summary?.top_vendors?.[0]
|
|
? `$${summary.top_vendors[0].amount.toFixed(2)}`
|
|
: undefined
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Top Vendors Bar Chart */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Top Vendors</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!summary ? (
|
|
<p className="text-xs text-muted-foreground">Loading...</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{summary.top_vendors.map((v) => (
|
|
<div key={v.vendor} className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-foreground">{v.vendor}</span>
|
|
<span className="text-muted-foreground">
|
|
${v.amount.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
<div className="h-2 rounded-full bg-secondary overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full bg-primary transition-all"
|
|
style={{
|
|
width: `${(v.amount / maxVendor) * 100}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Transactions Table */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Transactions</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DataTable<Transaction>
|
|
data={transactions ?? []}
|
|
columns={txColumns}
|
|
searchKey="vendor"
|
|
filterKey="category"
|
|
filterOptions={categories}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|