35a5be97b0
Next.js 16 personal finance dashboard connected to postgres-personal. Phase 1 (Foundation): - API routes: GET /api/transactions (paginated, filterable, sortable), GET /api/statements, GET /api/merchants - Transactions data table with date/category/bank/search filters, pagination, sort - Statements card grid with period, due date, amount, transaction count - Sidebar layout with nav for all planned sections Phase 2 (Normalisation): - PATCH /api/transactions/[id] — upsert transaction_overrides - POST /api/transactions/bulk — bulk categorize/normalize - Inline click-to-edit category (22 options) and merchant name - Blue dot override indicator, bulk action bar - Effective values via COALESCE(override, llm_value) pattern Stack: Next.js 16 (App Router, standalone), Prisma 7.x + @prisma/adapter-pg, TanStack Query, Tailwind CSS. Auth via Traefik chain-oauth@file.
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useStatements } from "@/lib/hooks";
|
|
|
|
function formatDate(d: string | null) {
|
|
if (!d) return "-";
|
|
return new Date(d).toLocaleDateString("en-AU", {
|
|
day: "2-digit",
|
|
month: "short",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
function formatCurrency(amount: number | null, currency = "AUD") {
|
|
if (amount === null || amount === undefined) return "-";
|
|
return new Intl.NumberFormat("en-AU", {
|
|
style: "currency",
|
|
currency,
|
|
}).format(amount);
|
|
}
|
|
|
|
export default function StatementsPage() {
|
|
const { data: statements, isLoading } = useStatements();
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="text-xl font-semibold mb-4">Statements</h2>
|
|
|
|
{isLoading ? (
|
|
<p className="text-zinc-500">Loading...</p>
|
|
) : !statements?.length ? (
|
|
<p className="text-zinc-500">No statements found</p>
|
|
) : (
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{statements.map((s) => (
|
|
<div
|
|
key={s.id}
|
|
className="border border-zinc-800 rounded-lg p-4 bg-zinc-900/50 hover:border-zinc-700 transition-colors"
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="font-medium">{s.bank_name}</h3>
|
|
<span className="text-xs text-zinc-500">{s.currency}</span>
|
|
</div>
|
|
{s.card_name && (
|
|
<p className="text-sm text-zinc-400 mb-2">{s.card_name}</p>
|
|
)}
|
|
<div className="text-sm text-zinc-400 space-y-1">
|
|
<p>Account: {s.account_number}</p>
|
|
<p>
|
|
Period: {formatDate(s.billing_start_date)} - {formatDate(s.billing_end_date)}
|
|
</p>
|
|
<p>Due: {formatDate(s.payment_due_date)}</p>
|
|
</div>
|
|
<div className="mt-3 pt-3 border-t border-zinc-800 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-lg font-semibold text-red-400">
|
|
{formatCurrency(s.total_amount_due, s.currency)}
|
|
</p>
|
|
<p className="text-xs text-zinc-500">
|
|
{s.transaction_count} transactions
|
|
</p>
|
|
</div>
|
|
<Link
|
|
href={`/transactions?statement_id=${s.id}`}
|
|
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 rounded text-sm transition-colors"
|
|
>
|
|
View
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|