Sanitized mirror from private repository - 2026-04-05 06:40:46 UTC
This commit is contained in:
150
dashboard/ui/app/expenses/page.tsx
Normal file
150
dashboard/ui/app/expenses/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"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 {
|
||||
date: string;
|
||||
vendor: string;
|
||||
amount: string | number;
|
||||
currency?: string;
|
||||
order_number?: string;
|
||||
email_account?: string;
|
||||
message_id?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
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">
|
||||
${Number(row.amount || 0).toFixed(2)} {row.currency ?? ""}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ key: "order_number", label: "Order #" },
|
||||
{
|
||||
key: "email_account",
|
||||
label: "Account",
|
||||
render: (row) => (
|
||||
<span className="text-muted-foreground truncate max-w-[120px] block text-[10px]">
|
||||
{String(row.email_account ?? "")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const accounts = transactions
|
||||
? [...new Set(transactions.map((t) => String(t.email_account ?? "")).filter(Boolean))]
|
||||
: [];
|
||||
|
||||
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="email_account"
|
||||
filterOptions={accounts}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user