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:
2026-05-10 16:04:54 +10:00
parent 4a49add277
commit 0c1f88ed9c
6 changed files with 97 additions and 25 deletions
+44 -7
View File
@@ -270,10 +270,30 @@ function PaymentHistory({ participantId, currentUserId }: { participantId: numbe
}
// ── Main page ─────────────────────────────────────────────────────────────────
type SortCol = "transaction_date" | "created_at" | "amount";
export default function SharedPage() {
const [tagIds, setTagIds] = useState<string[]>([]);
const [sortCol, setSortCol] = useState<SortCol>("transaction_date");
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
const realTagIds = tagIds.filter((id) => id !== "untagged");
const { data: transactions = [], isLoading: txLoading } = useSharedTransactions(tagIds);
const { data: rawTransactions = [], isLoading: txLoading } = useSharedTransactions(tagIds);
const transactions = [...rawTransactions].sort((a, b) => {
const av = sortCol === "amount" ? Number(a.amount) : new Date(a[sortCol]).getTime();
const bv = sortCol === "amount" ? Number(b.amount) : new Date(b[sortCol]).getTime();
return sortDir === "desc" ? bv - av : av - bv;
});
function toggleSort(col: SortCol) {
if (sortCol === col) setSortDir((d) => (d === "desc" ? "asc" : "desc"));
else { setSortCol(col); setSortDir("desc"); }
}
function SortIcon({ col }: { col: SortCol }) {
if (sortCol !== col) return <span className="text-zinc-600 ml-0.5"></span>;
return <span className="ml-0.5">{sortDir === "desc" ? "↓" : "↑"}</span>;
}
const { data: balances = [], isLoading: balLoading } = useParticipantBalances(realTagIds);
const { data: me } = useCurrentUser();
const [addingParticipant, setAddingParticipant] = useState(false);
@@ -353,7 +373,7 @@ export default function SharedPage() {
</div>
{/* Transaction list */}
<div className="bg-zinc-900 border border-zinc-700 rounded-xl overflow-hidden">
<div className="bg-zinc-900 border border-zinc-700 rounded-xl overflow-x-auto">
<div className="px-4 py-3 border-b border-zinc-800">
<h3 className="text-sm font-medium">Split Transactions</h3>
</div>
@@ -364,12 +384,28 @@ export default function SharedPage() {
No split transactions yet. Use the Split button on any transaction.
</p>
) : (
<table className="w-full text-sm">
<table className="w-full text-sm min-w-[520px]">
<thead>
<tr className="border-b border-zinc-800">
<th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Date</th>
<th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Description</th>
<th className="text-right px-4 py-2 text-xs text-zinc-500 font-medium">Amount</th>
<th
className="text-left px-4 py-2 text-xs text-zinc-500 font-medium cursor-pointer hover:text-white whitespace-nowrap"
onClick={() => toggleSort("transaction_date")}
>
Date <SortIcon col="transaction_date" />
</th>
<th
className="text-left px-4 py-2 text-xs text-zinc-500 font-medium cursor-pointer hover:text-white whitespace-nowrap"
onClick={() => toggleSort("created_at")}
>
Imported <SortIcon col="created_at" />
</th>
<th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium sticky left-0 z-10 bg-zinc-900 border-r border-zinc-800/80">Description</th>
<th
className="text-right px-4 py-2 text-xs text-zinc-500 font-medium cursor-pointer hover:text-white"
onClick={() => toggleSort("amount")}
>
Amount <SortIcon col="amount" />
</th>
<th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Splits</th>
<th className="px-4 py-2"></th>
</tr>
@@ -380,7 +416,8 @@ export default function SharedPage() {
return (
<tr key={tx.id} className="border-b border-zinc-800/50 hover:bg-zinc-800/30">
<td className="px-4 py-3 text-zinc-400 whitespace-nowrap">{formatDate(tx.transaction_date)}</td>
<td className="px-4 py-3 max-w-xs">
<td className="px-4 py-3 text-zinc-500 text-xs whitespace-nowrap">{formatDate(tx.created_at)}</td>
<td className="px-4 py-3 max-w-xs sticky left-0 z-10 bg-zinc-900 border-r border-zinc-800/80">
<p className="font-medium break-words">{tx.effective_merchant || tx.description}</p>
{tx.effective_merchant && (
<p className="text-xs text-zinc-500 break-words">{tx.description}</p>