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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user