feat(transactions): the period as a local ledger with Excel-style filters

The list loads the whole period (or twelve months with Y) and filters it
in the browser: a text line over descriptions, notes, accounts and
amounts, dropdowns with counts per type, category, budget, account,
currency, tag and subscription, group-by with subtotals per currency,
pinned filters on 1 to 9. Possible duplicates show with a delete for the
newer one, one account adds the balance column, s s sets a subscription
and s . repeats the last mass action.
This commit is contained in:
hh committed 2026-10-16 15:13:35 +00:00
1 parent 01c53d982d
commit 153a8cb191
9 files changed
+1693 -131

No files matched your search

@@ -5,7 +5,6 @@
import {
type Group,
type Signed,
signedAmount,
sumByCurrency,
} from "#lib/transactions/split.js";
@@ -16,9 +15,10 @@
["transfer", "Transferred"],
] as const;
interface Entry extends Signed {
interface Entry {
href?: string;
name: string;
sums: Signed[];
}
let {
@@ -36,25 +36,29 @@
})).filter((row) => row.sums.length > 0)
);
const size = (entry: Entry) =>
Math.max(...entry.sums.map((sum) => Math.abs(sum.amount)));
function aggregate(
list: Split[],
pick: (split: Split) => [string, string | undefined]
): { entries: Entry[]; more: number } {
const map = new Map<string, Entry>();
const map = new Map<string, { href?: string; splits: Split[] }>();
for (const split of list) {
const [name, href] = pick(split);
const money = signedAmount(split);
const key = `${name}|${money.code}`;
const entry = map.get(key);
if (entry) {
entry.amount += money.amount;
} else {
map.set(key, { ...money, href, name });
}
const entry = map.get(name) ?? { href, splits: [] };
entry.splits.push(split);
map.set(name, entry);
}
const sorted = [...map.values()].sort(
(a, b) => Math.abs(b.amount) - Math.abs(a.amount)
);
const sorted = [...map]
.map(([name, entry]) => ({
href: entry.href,
name,
sums: sumByCurrency(entry.splits).sort(
(a, b) => Math.abs(b.amount) - Math.abs(a.amount)
),
}))
.sort((a, b) => size(b) - size(a));
return { entries: sorted.slice(0, TOP), more: sorted.length - TOP };
}
@@ -127,8 +131,10 @@
{#each blocks as block (block.title)}
<Box body="py-1" class="max-md:hidden" title={block.title}>
<ul>
{#each block.entries as entry (`${entry.name}|${entry.code}`)}
<li class="flex h-6 items-center justify-between gap-3 px-3">
{#each block.entries as entry (entry.name)}
<li
class="flex min-h-6 items-start justify-between gap-3 px-3 py-0.5"
>
{#if entry.href}
<a class="truncate hover:underline" href={entry.href}
>{entry.name}</a
@@ -136,7 +142,11 @@
{:else}
<span class="truncate text-cladd-fg-soft">{entry.name}</span>
{/if}
{@render amount(entry)}
<span class="flex flex-col items-end">
{#each entry.sums as sum (sum.code)}
{@render amount(sum)}
{/each}
</span>
</li>
{:else}
<li class="px-3 py-1 text-cladd-fg-soft">