feat(accounts): friends ledger for debts, settle up and offset, asset accounts by currency
Liabilities show one card per friend with who owes whom, last movement, average days to pay back and twelve months of lending. s settles up, + lends more, o offsets two debts through the prefilled editor. Asset accounts group by currency with totals, inactive ones behind i.
This commit is contained in:
1 parent
60b099e917
commit
6f39343e07
9 files changed
+1103
-64
No files matched your search
@@ -13,6 +13,7 @@
|
||||
import Sparkline from "#lib/components/charts/Sparkline.svelte";
|
||||
import {
|
||||
balanceRange,
|
||||
byCurrency,
|
||||
type ChartSet,
|
||||
seriesPoints,
|
||||
} from "#lib/components/dashboard/data.js";
|
||||
@@ -123,15 +124,33 @@
|
||||
let armed = $state(false);
|
||||
let failure = $state("");
|
||||
|
||||
const ROLE_RANK: Record<string, number> = {
|
||||
cashWalletAsset: 0,
|
||||
ccAsset: 1,
|
||||
defaultAsset: 2,
|
||||
savingAsset: 4,
|
||||
sharedAsset: 3,
|
||||
};
|
||||
const perCurrency = $derived(type === "asset" && active);
|
||||
const showInactive = $derived(
|
||||
perCurrency && page.url.searchParams.has("inactive")
|
||||
);
|
||||
|
||||
const accounts = createQuery(() => ({
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: () =>
|
||||
all<Account>("/v1/accounts", {
|
||||
...balanceRange(period.start, period.end),
|
||||
active: String(active),
|
||||
...(showInactive ? {} : { active: String(active) }),
|
||||
type,
|
||||
}),
|
||||
queryKey: ["accounts-index", type, active, period.start, period.end],
|
||||
queryKey: [
|
||||
"accounts-index",
|
||||
type,
|
||||
showInactive ? "all" : active,
|
||||
period.start,
|
||||
period.end,
|
||||
],
|
||||
}));
|
||||
const others = createQuery(() => ({
|
||||
queryFn: () =>
|
||||
@@ -228,8 +247,12 @@
|
||||
const sorted = $derived.by(() => {
|
||||
const list = [...matching];
|
||||
if (!sortKey) {
|
||||
const rank = (item: Item<Account>) =>
|
||||
perCurrency ? (ROLE_RANK[item.attributes.account_role ?? ""] ?? 5) : 0;
|
||||
return list.sort(
|
||||
(x, y) => (x.attributes.order ?? 0) - (y.attributes.order ?? 0)
|
||||
(x, y) =>
|
||||
rank(x) - rank(y) ||
|
||||
(x.attributes.order ?? 0) - (y.attributes.order ?? 0)
|
||||
);
|
||||
}
|
||||
const dir = descending ? -1 : 1;
|
||||
@@ -243,9 +266,15 @@
|
||||
});
|
||||
});
|
||||
const grouped = $derived(
|
||||
sorted.some((item) => item.attributes.object_group_title)
|
||||
perCurrency || sorted.some((item) => item.attributes.object_group_title)
|
||||
);
|
||||
const groups = $derived.by(() => {
|
||||
if (perCurrency) {
|
||||
return byCurrency(
|
||||
sorted,
|
||||
(item) => item.attributes.currency_code ?? ""
|
||||
).map(([code, items]) => ({ items, title: code }));
|
||||
}
|
||||
if (!grouped) {
|
||||
return [{ items: sorted, title: "" }];
|
||||
}
|
||||
@@ -327,6 +356,18 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
...(perCurrency
|
||||
? [
|
||||
{
|
||||
group: "Accounts",
|
||||
keys: "i",
|
||||
label: showInactive
|
||||
? "Hide inactive accounts"
|
||||
: "Show inactive accounts",
|
||||
run: () => goto(inactiveHref(), { replace: true }),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
id: "accounts",
|
||||
items: () => rows,
|
||||
@@ -335,6 +376,18 @@
|
||||
});
|
||||
nav.use();
|
||||
|
||||
function inactiveHref() {
|
||||
const url = new URL(page.url.href);
|
||||
if (showInactive) {
|
||||
url.searchParams.delete("inactive");
|
||||
} else {
|
||||
url.searchParams.set("inactive", "1");
|
||||
}
|
||||
url.searchParams.set("start", period.start);
|
||||
url.searchParams.set("end", period.end);
|
||||
return url.pathname + url.search;
|
||||
}
|
||||
|
||||
function onfilterkey(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && filter) {
|
||||
filter = "";
|
||||
@@ -417,6 +470,47 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet sums(
|
||||
t: Total,
|
||||
label: string,
|
||||
head = false
|
||||
)}
|
||||
{#each columns as col, c (col.key)}
|
||||
<td
|
||||
class="px-3 {col.num ? "text-right" : ""} {col.wide
|
||||
? "hidden md:table-cell"
|
||||
: ""}"
|
||||
>
|
||||
{#if c === 0}
|
||||
<span
|
||||
class="font-semibold text-[12px] {head
|
||||
? "text-cladd-fg"
|
||||
: "text-cladd-fg-soft"}"
|
||||
>{label}</span
|
||||
>
|
||||
{#if head}
|
||||
<Amount
|
||||
{...t}
|
||||
amount={t.difference}
|
||||
class="ml-2 text-[12px] md:hidden"
|
||||
signed
|
||||
/>
|
||||
{/if}
|
||||
{:else if col.key === "balance"}
|
||||
<span class="num font-semibold"
|
||||
>{formatMoney({ ...t, amount: t.balance })}</span
|
||||
>
|
||||
{:else if col.key === "due"}
|
||||
<span class="num font-semibold"
|
||||
>{formatMoney({ ...t, amount: t.due })}</span
|
||||
>
|
||||
{:else if col.key === "difference"}
|
||||
<Amount {...t} amount={t.difference} class="font-semibold" />
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
<PageHeader
|
||||
crumbs={[{ label: title }]}
|
||||
subtitle={active ? undefined : "Archive"}
|
||||
@@ -502,7 +596,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each groups as group (group.title)}
|
||||
{#if grouped}
|
||||
{#if perCurrency}
|
||||
{@const t = totals.find((x) => x.code === group.title)}
|
||||
<tr class="h-8 border-cladd-outline border-t-2">
|
||||
{#if t}
|
||||
{@render sums(t, group.title, true)}
|
||||
{/if}
|
||||
</tr>
|
||||
{:else if grouped}
|
||||
<tr class="h-8 border-cladd-outline border-t">
|
||||
<th
|
||||
class="px-3 text-left font-semibold text-[12px] text-cladd-fg"
|
||||
@@ -517,7 +618,9 @@
|
||||
{@const a = item.attributes}
|
||||
{@const i = index.get(item.id) ?? -1}
|
||||
<tr
|
||||
class="h-7 border-cladd-outline/60 border-t {rowClass(i)}"
|
||||
class="h-7 border-cladd-outline/60 border-t {rowClass(
|
||||
i
|
||||
)} {a.active ? "" : "opacity-60"}"
|
||||
data-id={item.id}
|
||||
id={nav.rowId(i)}
|
||||
onclick={(event) => nav.click(i, event)}
|
||||
@@ -603,38 +706,11 @@
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
{#if totals.length > 0}
|
||||
{#if totals.length > 0 && !perCurrency}
|
||||
<tfoot class="border-cladd-outline border-t-2">
|
||||
{#each totals as t (t.code)}
|
||||
<tr class="h-7">
|
||||
{#each columns as col, c (col.key)}
|
||||
<td
|
||||
class="px-3 {col.num ? "text-right" : ""} {col.wide
|
||||
? "hidden md:table-cell"
|
||||
: ""}"
|
||||
>
|
||||
{#if c === 0}
|
||||
<span
|
||||
class="font-semibold text-[12px] text-cladd-fg-soft"
|
||||
>Total in {t.code}</span
|
||||
>
|
||||
{:else if col.key === "balance"}
|
||||
<span class="num font-semibold"
|
||||
>{formatMoney({ ...t, amount: t.balance })}</span
|
||||
>
|
||||
{:else if col.key === "due"}
|
||||
<span class="num font-semibold"
|
||||
>{formatMoney({ ...t, amount: t.due })}</span
|
||||
>
|
||||
{:else if col.key === "difference"}
|
||||
<Amount
|
||||
{...t}
|
||||
amount={t.difference}
|
||||
class="font-semibold"
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{@render sums(t, `Total in ${t.code}`)}
|
||||
</tr>
|
||||
{/each}
|
||||
</tfoot>
|
||||
@@ -690,7 +766,17 @@
|
||||
{#if failure}
|
||||
<span class="text-expense">{failure}</span>
|
||||
{/if}
|
||||
{#if active && otherCount > 0}
|
||||
{#if perCurrency && (showInactive || otherCount > 0)}
|
||||
<a
|
||||
class="ml-auto text-cladd-fg-soft hover:underline"
|
||||
data-sveltekit-replacestate
|
||||
href={inactiveHref()}
|
||||
>{showInactive
|
||||
? "Hide inactive accounts"
|
||||
: `Show ${otherCount} inactive ${otherCount === 1 ? "account" : "accounts"}`}
|
||||
<span class="kbd">i</span></a
|
||||
>
|
||||
{:else if active && otherCount > 0}
|
||||
<a
|
||||
class="ml-auto text-cladd-fg-soft hover:underline"
|
||||
href="/inactive-accounts/{type}"
|
||||
|
||||
Reference in new issue
Block a user