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:
2026-03-09 12:03:39 +11:00
parent e3aa17acdd
commit f90ba332bd
2 changed files with 124 additions and 47 deletions
+37 -2
View File
@@ -1,7 +1,8 @@
"use client";
import { useState, useCallback } from "react";
import { useTransactions, useBanks, useUpdateTransaction, useBulkAction, useTags } from "@/lib/hooks";
import { useState, useCallback, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { useTransactions, useBanks, useUpdateTransaction, useBulkAction, useTags, useStatement } from "@/lib/hooks";
import { CATEGORIES, formatCategory } from "@/lib/categories";
import { SplitModal } from "@/components/split-modal";
import { TagPicker } from "@/components/tag-picker";
@@ -98,12 +99,24 @@ function InlineEdit({
}
export default function TransactionsPage() {
return (
<Suspense fallback={<p className="text-zinc-500 text-sm">Loading...</p>}>
<TransactionsContent />
</Suspense>
);
}
function TransactionsContent() {
const searchParams = useSearchParams();
const initialStatementId = searchParams.get("statement_id") || "";
const [filters, setFilters] = useState({
from: "",
to: "",
category: "",
bank_name: "",
search: "",
statement_id: initialStatementId,
tag_id: "",
sort_by: "transaction_date",
sort_dir: "desc",
@@ -118,6 +131,7 @@ export default function TransactionsPage() {
const { data, isLoading } = useTransactions(filters);
const { data: banks } = useBanks();
const { data: tags } = useTags();
const { data: statementInfo } = useStatement(parseInt(filters.statement_id) || 0);
const updateTxn = useUpdateTransaction();
const bulkAction = useBulkAction();
@@ -161,6 +175,27 @@ export default function TransactionsPage() {
<div>
<h2 className="text-xl font-semibold mb-4">Transactions</h2>
{/* Statement context banner */}
{filters.statement_id && statementInfo && (
<div className="flex items-center gap-3 mb-4 px-3 py-2 bg-indigo-950/40 border border-indigo-800/50 rounded-lg text-sm">
<span className="text-indigo-300 font-medium">{statementInfo.bank_name}</span>
{statementInfo.billing_start_date && statementInfo.billing_end_date && (
<span className="text-zinc-400">
{new Date(statementInfo.billing_start_date).toLocaleDateString("en-AU", { day: "2-digit", month: "short" })}
{" "}
{new Date(statementInfo.billing_end_date).toLocaleDateString("en-AU", { day: "2-digit", month: "short", year: "numeric" })}
</span>
)}
<span className="text-zinc-500 text-xs">{statementInfo.transaction_count} transactions</span>
<button
onClick={() => setFilters((f) => ({ ...f, statement_id: "", offset: 0 }))}
className="ml-auto text-zinc-500 hover:text-zinc-200 text-xs px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
>
× Clear filter
</button>
</div>
)}
{/* Filter bar */}
<div className="flex flex-wrap gap-3 mb-4">
<input