feat(transactions): the transaction page as a receipt with context
The amount is the headline, notes come first at full width, then the last twelve visits to the same place as a dot timeline next to the paying account's days around it with the running balance. Fields edit in place: j and k move, Enter opens and saves, Esc cancels, s c, s b, s t and s s jump to classification. A matching subscription shows as a suggestion, L links it.
This commit is contained in:
1 parent
58b356ea8b
commit
349a6a0b32
14 files changed
+2137
-683
No files matched your search
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import type { Item, TransactionGroup } from "#lib/api/types.js";
|
||||
import Place from "./Place.svelte";
|
||||
import SameDays from "./SameDays.svelte";
|
||||
import { loadPlace, placeOf, visitOf, visitsIn } from "./usual.js";
|
||||
|
||||
const OWN = {
|
||||
deposit: "destination",
|
||||
transfer: "source",
|
||||
withdrawal: "source",
|
||||
} as const;
|
||||
const TONE: Record<string, string> = {
|
||||
deposit: "text-income",
|
||||
transfer: "text-transfer",
|
||||
withdrawal: "text-expense",
|
||||
};
|
||||
|
||||
let { group }: { group: Item<TransactionGroup> } = $props();
|
||||
|
||||
const splits = $derived(group.attributes.transactions);
|
||||
const first = $derived(splits[0]);
|
||||
const day = $derived(first.date.slice(0, 10));
|
||||
const currency = $derived({
|
||||
code: first.currency_code,
|
||||
decimals: first.currency_decimal_places,
|
||||
symbol: first.currency_symbol,
|
||||
});
|
||||
const currencies = $derived(
|
||||
[
|
||||
currency,
|
||||
{
|
||||
code: first.foreign_currency_code,
|
||||
decimals: first.foreign_currency_decimal_places,
|
||||
symbol: first.foreign_currency_symbol,
|
||||
},
|
||||
].filter((option) => option.code)
|
||||
);
|
||||
const place = $derived(placeOf(splits));
|
||||
const own = $derived.by(() => {
|
||||
const side = OWN[first.type as keyof typeof OWN];
|
||||
const id = side ? first[`${side}_id`] : null;
|
||||
return side && id ? { id, name: first[`${side}_name`] ?? "" } : null;
|
||||
});
|
||||
|
||||
const history = createQuery(() => ({
|
||||
enabled: place !== null,
|
||||
queryFn: () =>
|
||||
place ? loadPlace(place, group.id, day) : Promise.resolve([]),
|
||||
queryKey: ["receipt", "place", group.id, place?.id, day],
|
||||
}));
|
||||
|
||||
const best = $derived.by(() => {
|
||||
if (!place) {
|
||||
return null;
|
||||
}
|
||||
const options = currencies.flatMap((option) => {
|
||||
const current = visitOf(group, place, option.code ?? "");
|
||||
return current
|
||||
? [
|
||||
{
|
||||
currency: option,
|
||||
current,
|
||||
past: visitsIn(history.data ?? [], place, option.code ?? ""),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
return options.reduce<(typeof options)[number] | null>(
|
||||
(top, option) =>
|
||||
top && top.past.length >= option.past.length ? top : option,
|
||||
null
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if best || own}
|
||||
<div class="grid items-stretch gap-3 lg:grid-cols-2">
|
||||
{#if place && best}
|
||||
<Place
|
||||
currency={best.currency}
|
||||
current={best.current}
|
||||
loading={history.isPending}
|
||||
past={best.past}
|
||||
{place}
|
||||
tone={TONE[first.type] ?? "text-cladd-fg"}
|
||||
/>
|
||||
{/if}
|
||||
{#if own}
|
||||
<SameDays account={own} {currency} {day} groupId={group.id} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in new issue
Block a user