feat(accounts): merchants and payers as a league table

Expense and revenue accounts list this month per currency, share,
visits, average, twelve months, typical and the change against it, last
visit and category; quiet ones fold into one line behind i. 1 to 5 sort,
; groups by category with subtotals, x and s c set the category on all
of a merchant's transactions in the period.
This commit is contained in:
hh committed 2026-10-16 15:35:38 +00:00
1 parent 26903b0066
commit fffb770cac
7 files changed
+1352

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
import { createQuery, keepPreviousData } from "@tanstack/svelte-query";
import { lists } from "#lib/api/lists.js";
import type { TransactionGroup } from "#lib/api/types.js";
import { ratesQuery } from "#lib/dashboard/primary.svelte.js";
import { today } from "#lib/dates.js";
import { period } from "#lib/period.svelte.js";
import { ledgerQuery, ledgerWindow, ownFlows } from "#lib/reports/ledger.js";
import { pages } from "#lib/reports/queries.js";
import { typicalMonths } from "#lib/reports/typical.js";
import { session } from "#lib/session.svelte.js";
import { board, type Kind, monthDay } from "./merchants.js";
export function useMerchants(kind: () => Kind) {
const now = today();
const current = ledgerWindow(now);
const anchor = $derived(period.end >= current.start ? now : period.end);
const window = $derived(ledgerWindow(anchor));
const inside = $derived(
period.start >= window.start && period.end <= window.end
);
const accounts = createQuery(() => lists.accounts("all"));
const ledger = createQuery(() => ({
...ledgerQuery(anchor),
placeholderData: keepPreviousData,
}));
const extra = createQuery(() => ({
enabled: !inside,
placeholderData: keepPreviousData,
queryFn: () =>
pages<TransactionGroup>("/v1/transactions", {
end: period.end,
start: period.start,
type: "default",
}),
queryKey: ["merchants-period", period.start, period.end],
}));
const rates = createQuery(() => ({ ...ratesQuery(), enabled: true }));
const flows = $derived(
ledger.data && accounts.data ? ownFlows(ledger.data, accounts.data) : []
);
const periodFlows = $derived.by(() => {
if (inside) {
return flows;
}
return extra.data && accounts.data
? ownFlows(extra.data, accounts.data)
: [];
});
const day = $derived(monthDay(period.range, now));
const result = $derived.by(() => {
if (!(accounts.data && ledger.data && (inside || extra.data))) {
return null;
}
const month = period.end.slice(0, 7);
const since = flows.reduce(
(first, flow) => (flow.date < first ? flow.date : first),
now
);
const primary = session.primary?.code ?? "";
return board(accounts.data, flows, periodFlows, {
day,
kind: kind(),
months: window.months.filter((m) => m <= month),
past: typicalMonths(window.months, period.end, since),
range: period.range,
rate: (code) => rates.data?.[code]?.rate ?? (code === primary ? 1 : 0),
});
});
return {
get board() {
return result;
},
get day() {
return day;
},
get error() {
return accounts.error ?? ledger.error ?? extra.error ?? null;
},
get loading() {
return result === null || rates.isPending;
},
get stale() {
return ledger.isFetching || extra.isPlaceholderData;
},
};
}