feat(transactions): add imported date column, split filter, and sortable columns
- Show created_at as "Imported" column in transactions and shared views - For reconciled transactions, show original CSV import date (not statement processing date) via LEFT JOIN on reconciled_with_id - Add has_split filter (all/split only/unsplit only) to transactions page - Transactions table: sortable by imported date; split filter dropdown - Shared table: client-side sort by date, imported, and amount
This commit is contained in:
@@ -476,6 +476,7 @@ function TransactionsContent() {
|
||||
offset: 0,
|
||||
amount_min: undefined as number | undefined,
|
||||
amount_max: undefined as number | undefined,
|
||||
has_split: "" as string,
|
||||
});
|
||||
const [queryInput, setQueryInput] = useState("");
|
||||
const [queryTokens, setQueryTokens] = useState<QueryToken[]>([]);
|
||||
@@ -610,7 +611,7 @@ function TransactionsContent() {
|
||||
value={queryInput}
|
||||
onChange={(e) => handleQueryChange(e.target.value)}
|
||||
placeholder="Search… or >500 <=1500 200-800"
|
||||
className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm w-64 font-mono placeholder:font-sans placeholder:text-zinc-600"
|
||||
className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm w-full sm:w-64 font-mono placeholder:font-sans placeholder:text-zinc-600"
|
||||
/>
|
||||
{queryTokens.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@@ -677,6 +678,15 @@ function TransactionsContent() {
|
||||
onChange={(v) => setFilters((f) => ({ ...f, transaction_types: v, offset: 0 }))}
|
||||
placeholder="All Types"
|
||||
/>
|
||||
<select
|
||||
value={filters.has_split}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, has_split: e.target.value, offset: 0 }))}
|
||||
className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-300"
|
||||
>
|
||||
<option value="">All Splits</option>
|
||||
<option value="yes">Split only</option>
|
||||
<option value="no">Unsplit only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Bulk action bar */}
|
||||
@@ -749,10 +759,10 @@ function TransactionsContent() {
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto border border-zinc-800 rounded-lg">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full text-sm min-w-[900px]">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-800 bg-zinc-900/50">
|
||||
<th className="p-2 w-8">
|
||||
<th className="p-2 w-8 sticky left-0 z-10 bg-zinc-900">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data?.data.length ? selected.size === data.data.length : false}
|
||||
@@ -761,11 +771,17 @@ function TransactionsContent() {
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
className="p-2 text-left cursor-pointer hover:text-white"
|
||||
className="p-2 text-left cursor-pointer hover:text-white sticky left-8 z-10 bg-zinc-900 border-r border-zinc-800/80 whitespace-nowrap"
|
||||
onClick={() => toggleSort("transaction_date")}
|
||||
>
|
||||
Date {filters.sort_by === "transaction_date" && (filters.sort_dir === "desc" ? "\u2193" : "\u2191")}
|
||||
</th>
|
||||
<th
|
||||
className="p-2 text-left text-zinc-500 cursor-pointer hover:text-white whitespace-nowrap"
|
||||
onClick={() => toggleSort("created_at")}
|
||||
>
|
||||
Imported {filters.sort_by === "created_at" && (filters.sort_dir === "desc" ? "\u2193" : "\u2191")}
|
||||
</th>
|
||||
<th className="p-2 text-left">Description</th>
|
||||
<th className="p-2 text-left">Merchant</th>
|
||||
<th
|
||||
@@ -783,9 +799,9 @@ function TransactionsContent() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr><td colSpan={10} className="p-8 text-center text-zinc-500">Loading...</td></tr>
|
||||
<tr><td colSpan={11} className="p-8 text-center text-zinc-500">Loading...</td></tr>
|
||||
) : !data?.data.length ? (
|
||||
<tr><td colSpan={10} className="p-8 text-center text-zinc-500">No transactions found</td></tr>
|
||||
<tr><td colSpan={11} className="p-8 text-center text-zinc-500">No transactions found</td></tr>
|
||||
) : (
|
||||
data.data.map((t) => (
|
||||
<tr
|
||||
@@ -794,7 +810,7 @@ function TransactionsContent() {
|
||||
selected.has(t.id) ? "bg-zinc-800/40" : ""
|
||||
}`}
|
||||
>
|
||||
<td className="p-2">
|
||||
<td className={`p-2 sticky left-0 z-10 ${selected.has(t.id) ? "bg-zinc-800" : "bg-zinc-950"}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(t.id)}
|
||||
@@ -802,7 +818,8 @@ function TransactionsContent() {
|
||||
className="accent-blue-600"
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2 whitespace-nowrap">{formatDate(t.transaction_date)}</td>
|
||||
<td className={`p-2 whitespace-nowrap sticky left-8 z-10 border-r border-zinc-800/80 ${selected.has(t.id) ? "bg-zinc-800" : "bg-zinc-950"}`}>{formatDate(t.transaction_date)}</td>
|
||||
<td className="p-2 whitespace-nowrap text-zinc-500 text-xs">{formatDate(t.created_at)}</td>
|
||||
<td className="p-2 max-w-xs">
|
||||
<p className="truncate" title={t.description}>{t.description}</p>
|
||||
{t.notes && (
|
||||
|
||||
Reference in New Issue
Block a user