feat(statements): table layout + statement-scoped transaction view
- Statements page: replace card grid with compact table showing bank, account, period, due date, currency, amount (due for CC / balance for bank), transaction count, and View button - Transactions page: wrap in Suspense, read statement_id from URL search params on load; show a dismissible indigo banner with bank name and billing period when filtering by statement; × Clear filter removes it
This commit is contained in:
+87
-45
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import { useStatements } from "@/lib/hooks";
|
||||
|
||||
function formatDate(d: string | null) {
|
||||
if (!d) return "-";
|
||||
if (!d) return "—";
|
||||
return new Date(d).toLocaleDateString("en-AU", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
@@ -12,12 +12,22 @@ function formatDate(d: string | null) {
|
||||
});
|
||||
}
|
||||
|
||||
function formatCurrency(amount: number | null, currency = "AUD") {
|
||||
if (amount === null || amount === undefined) return "-";
|
||||
function formatPeriod(start: string | null, end: string | null) {
|
||||
if (!start && !end) return "—";
|
||||
const fmt = (d: string) =>
|
||||
new Date(d).toLocaleDateString("en-AU", { day: "2-digit", month: "short", year: "2-digit" });
|
||||
if (!start) return `until ${fmt(end!)}`;
|
||||
if (!end) return `from ${fmt(start)}`;
|
||||
return `${fmt(start)} – ${fmt(end)}`;
|
||||
}
|
||||
|
||||
function formatAmount(n: number | null): string {
|
||||
if (n === null || n === undefined) return "—";
|
||||
return new Intl.NumberFormat("en-AU", {
|
||||
style: "currency",
|
||||
currency,
|
||||
}).format(amount);
|
||||
currency: "AUD",
|
||||
minimumFractionDigits: 2,
|
||||
}).format(Number(n));
|
||||
}
|
||||
|
||||
export default function StatementsPage() {
|
||||
@@ -28,48 +38,80 @@ export default function StatementsPage() {
|
||||
<h2 className="text-xl font-semibold mb-4">Statements</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-zinc-500">Loading...</p>
|
||||
<p className="text-zinc-500 text-sm">Loading...</p>
|
||||
) : !statements?.length ? (
|
||||
<p className="text-zinc-500">No statements found</p>
|
||||
<p className="text-zinc-500 text-sm">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 className="border border-zinc-700 rounded-xl overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-800 bg-zinc-900">
|
||||
<th className="text-left px-4 py-2.5 text-xs text-zinc-500 font-medium">Bank</th>
|
||||
<th className="text-left px-4 py-2.5 text-xs text-zinc-500 font-medium">Account</th>
|
||||
<th className="text-left px-4 py-2.5 text-xs text-zinc-500 font-medium">Period</th>
|
||||
<th className="text-left px-4 py-2.5 text-xs text-zinc-500 font-medium">Due / Updated</th>
|
||||
<th className="text-left px-4 py-2.5 text-xs text-zinc-500 font-medium">Ccy</th>
|
||||
<th className="text-right px-4 py-2.5 text-xs text-zinc-500 font-medium">Amount</th>
|
||||
<th className="text-right px-4 py-2.5 text-xs text-zinc-500 font-medium">Txns</th>
|
||||
<th className="px-4 py-2.5"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statements.map((s) => {
|
||||
const isCreditCard = Number(s.credit_limit) > 0 || s.payment_due_date != null;
|
||||
const displayAmount = isCreditCard ? s.total_amount_due : s.closing_balance;
|
||||
const amountLabel = isCreditCard ? "due" : "balance";
|
||||
|
||||
return (
|
||||
<tr key={s.id} className="border-b border-zinc-800/50 hover:bg-zinc-800/20 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium truncate max-w-[180px]" title={s.bank_name}>
|
||||
{s.bank_name}
|
||||
</div>
|
||||
{s.card_name && (
|
||||
<div className="text-xs text-zinc-500 truncate max-w-[180px]">{s.card_name}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-zinc-400 font-mono text-xs">
|
||||
{s.account_number}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-zinc-400 whitespace-nowrap">
|
||||
{formatPeriod(s.billing_start_date, s.billing_end_date)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-zinc-400 whitespace-nowrap">
|
||||
{isCreditCard
|
||||
? formatDate(s.payment_due_date)
|
||||
: formatDate(s.billing_end_date)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-zinc-500 text-xs">
|
||||
{s.currency}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums">
|
||||
{displayAmount !== null && displayAmount !== undefined ? (
|
||||
<span className={isCreditCard ? "text-red-400" : "text-zinc-300"}>
|
||||
{formatAmount(displayAmount)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-zinc-600">—</span>
|
||||
)}
|
||||
<div className="text-xs text-zinc-600">{amountLabel}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-zinc-500">
|
||||
{s.transaction_count}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Link
|
||||
href={`/transactions?statement_id=${s.id}`}
|
||||
className="px-3 py-1 bg-zinc-800 hover:bg-zinc-700 rounded text-xs transition-colors whitespace-nowrap"
|
||||
>
|
||||
View →
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user