163 lines
4.8 KiB
TypeScript
163 lines
4.8 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";
|
|
import { CardSkeleton } from "@/components/skeleton";
|
|
|
|
interface Transaction {
|
|
date: string;
|
|
vendor: string;
|
|
amount: string | number;
|
|
currency?: string;
|
|
order_number?: string;
|
|
email_account?: string;
|
|
message_id?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
function getExpenseAccountColor(name: string): string {
|
|
const lower = name.toLowerCase();
|
|
if (lower.includes("gmail") || lower.includes("lzbellina")) return "text-blue-400";
|
|
if (lower.includes("dvish")) return "text-amber-400";
|
|
if (lower.includes("proton") || lower.includes("admin")) return "text-violet-400";
|
|
return "text-muted-foreground";
|
|
}
|
|
|
|
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) => {
|
|
const amt = Number(row.amount || 0);
|
|
return (
|
|
<span className={amt >= 0 ? "text-green-400" : "text-red-400"}>
|
|
${amt.toFixed(2)} {row.currency ?? ""}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{ key: "order_number", label: "Order #" },
|
|
{
|
|
key: "email_account",
|
|
label: "Account",
|
|
render: (row) => (
|
|
<span className={`truncate max-w-[120px] block text-xs ${getExpenseAccountColor(String(row.email_account ?? ""))}`}>
|
|
{String(row.email_account ?? "")}
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
const accounts = transactions
|
|
? [...new Set(transactions.map((t) => String(t.email_account ?? "")).filter(Boolean))]
|
|
: [];
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<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-5">
|
|
<StatCard
|
|
label="Total Spend"
|
|
value={summary ? `$${summary.total.toFixed(2)}` : "--"}
|
|
sub={summary?.month}
|
|
/>
|
|
<StatCard
|
|
label="Transactions"
|
|
value={summary?.count ?? "--"}
|
|
sub="this month"
|
|
/>
|
|
<StatCard
|
|
label="Top Vendor"
|
|
value={
|
|
summary?.top_vendors?.[0]?.vendor ?? "--"
|
|
}
|
|
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-base font-semibold">Top Vendors</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!summary ? (
|
|
<CardSkeleton lines={4} />
|
|
) : (
|
|
<div className="space-y-3">
|
|
{summary.top_vendors.map((v) => (
|
|
<div key={v.vendor} className="space-y-1.5">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-foreground">{v.vendor}</span>
|
|
<span className="text-green-400">
|
|
${v.amount.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
<div className="glass-bar-track h-2">
|
|
<div
|
|
className="h-full glass-bar-fill bg-gradient-to-r from-blue-500 to-violet-500 transition-all duration-700"
|
|
style={{
|
|
width: `${(v.amount / maxVendor) * 100}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Transactions Table */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base font-semibold">Transactions</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DataTable<Transaction>
|
|
data={transactions ?? []}
|
|
columns={txColumns}
|
|
searchKey="vendor"
|
|
filterKey="email_account"
|
|
filterOptions={accounts}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|