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
+1
View File
@@ -26,6 +26,7 @@ interface TransactionFilters {
offset?: number;
amount_min?: number;
amount_max?: number;
has_split?: string;
}
function buildParams(filters: TransactionFilters): string {
+12 -2
View File
@@ -82,6 +82,7 @@ interface TransactionFilters {
offset?: number;
amount_min?: number;
amount_max?: number;
has_split?: string;
}
export async function getTransactions(ownerId: number, filters: TransactionFilters) {
@@ -148,10 +149,15 @@ export async function getTransactions(ownerId: number, filters: TransactionFilte
conditions.push(`t.amount <= $${paramIdx++}`);
params.push(filters.amount_max);
}
if (filters.has_split === "yes") {
conditions.push(`EXISTS (SELECT 1 FROM transaction_splits ts_f WHERE ts_f.transaction_id = t.id)`);
} else if (filters.has_split === "no") {
conditions.push(`NOT EXISTS (SELECT 1 FROM transaction_splits ts_f WHERE ts_f.transaction_id = t.id)`);
}
const where = `WHERE ${conditions.join(" AND ")}`;
const sortCol = filters.sort_by === "amount" ? "t.amount" : "t.transaction_date";
const sortCol = filters.sort_by === "amount" ? "t.amount" : filters.sort_by === "created_at" ? "t.created_at" : "t.transaction_date";
const sortDir = filters.sort_dir === "asc" ? "ASC" : "DESC";
const limit = filters.limit || 50;
const offset = filters.offset || 0;
@@ -174,12 +180,14 @@ export async function getTransactions(ownerId: number, filters: TransactionFilte
COALESCE(s.bank_name, 'Manual') as bank_name,
COALESCE(t.owner_id, s.owner_id) as owner_id,
p.name as owner_name,
COALESCE(src.created_at, t.created_at) as created_at,
txn_tags.tags,
txn_splits.splits
FROM transactions t
LEFT JOIN transaction_overrides o ON o.transaction_id = t.id
LEFT JOIN statements s ON s.id = t.statement_id
LEFT JOIN participants p ON p.id = COALESCE(t.owner_id, s.owner_id)
LEFT JOIN transactions src ON src.reconciled_with_id = t.id AND src.statement_id IS NULL
LEFT JOIN LATERAL (
SELECT COALESCE(json_agg(json_build_object('id', tg.id, 'name', tg.name, 'color', tg.color) ORDER BY tg.name), '[]'::json) as tags
FROM transaction_tags tt
@@ -552,6 +560,7 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
COALESCE(s.bank_name, 'Manual') as bank_name,
COALESCE(t.owner_id, s.owner_id) as owner_id,
p_owner.name as owner_name,
COALESCE(src.created_at, t.created_at) as created_at,
json_agg(json_build_object(
'split_id', ts.id,
'participant_id', ts.participant_id,
@@ -565,6 +574,7 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
LEFT JOIN transaction_overrides o ON o.transaction_id = t.id
LEFT JOIN statements s ON s.id = t.statement_id
LEFT JOIN participants p_owner ON p_owner.id = COALESCE(t.owner_id, s.owner_id)
LEFT JOIN transactions src ON src.reconciled_with_id = t.id AND src.statement_id IS NULL
WHERE (
(
COALESCE(t.owner_id, s.owner_id) = $1
@@ -575,7 +585,7 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
)
)
${tagClause}
GROUP BY t.id, o.category_override, o.merchant_normalized, o.notes, s.bank_name, s.owner_id, p_owner.name
GROUP BY t.id, o.category_override, o.merchant_normalized, o.notes, s.bank_name, s.owner_id, p_owner.name, src.created_at
ORDER BY t.transaction_date DESC
`, params);