feat(dashboard,accounts): reconcile, debts box, per-currency charts, validated series palette

This commit is contained in:
hh committed 2026-10-16 03:58:49 +00:00
1 parent 189a455308
commit eebc9d2b41
14 files changed
+101 -32

No files matched your search

@@ -385,14 +385,18 @@
/>
{#snippet footer()}
{#if everything}
<a class="text-brand hover:underline" href={showUrl(id)}
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href={showUrl(id)}
>Show the current period and overview</a
>
<span class="ml-auto"
><Pager href={pageHref} page={pageNo} {pages} /></span
>
{:else}
<a class="text-brand hover:underline" href="{showUrl(id)}/all"
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="{showUrl(id)}/all"
>Show all transactions without grouping them by date</a
>
{/if}
@@ -68,14 +68,19 @@
}
const COLUMNS: Record<Key, Column> = {
active: { key: "active", label: "Is active?" },
active: { key: "active", label: "Is active?", wide: true },
activity: { key: "activity", label: "Last activity", wide: true },
balance: { key: "balance", label: "Current balance", num: true },
difference: { key: "difference", label: "Balance difference", num: true },
direction: { key: "direction", label: "Liability in/out" },
due: { key: "due", label: "Amount due", num: true },
difference: {
key: "difference",
label: "Balance difference",
num: true,
wide: true,
},
direction: { key: "direction", label: "Liability in/out", wide: true },
due: { key: "due", label: "Amount due", num: true, wide: true },
interest: { key: "interest", label: "Interest (period)", wide: true },
liability: { key: "liability", label: "Type of liability" },
liability: { key: "liability", label: "Type of liability", wide: true },
name: { key: "name", label: "Name" },
number: { key: "number", label: "Account number", wide: true },
role: { key: "role", label: "Role", wide: true },
@@ -163,7 +168,14 @@
);
const otherCount = $derived(others.data?.meta.pagination?.total ?? 0);
const columns = $derived(LAYOUT[type].map((k) => COLUMNS[k]));
const numbered = $derived(
(accounts.data ?? []).some((item) => formatIban(item.attributes))
);
const columns = $derived(
LAYOUT[type]
.filter((k) => k !== "number" || numbered)
.map((k) => COLUMNS[k])
);
const sortParam = $derived(page.url.searchParams.get("sort") ?? "");
const sortKey = $derived(sortParam.replace("-", "") as Key | "");
const descending = $derived(sortParam.startsWith("-"));
@@ -447,7 +459,9 @@
: "Nothing archived"}
</p>
{#if active}
<a class="text-brand underline" href={createHref}
<a
class="text-brand dark:text-(--ff-series-1) underline"
href={createHref}
>Create a new {TYPE_ONE[type]}</a
>
{/if}
@@ -456,7 +470,7 @@
<ContextMenu>
<ContextMenuTrigger class="block overflow-x-auto" {oncontextmenu}>
<table
class="w-full min-w-[720px] border-collapse text-[13px] transition-opacity {accounts.isPlaceholderData
class="w-full md:min-w-[720px] border-collapse text-[13px] transition-opacity {accounts.isPlaceholderData
? "opacity-60"
: ""}"
>
@@ -518,7 +532,7 @@
>
{#if col.key === "name"}
<a
class="block max-w-[22rem] truncate text-brand hover:underline"
class="block max-w-[22rem] truncate text-brand dark:text-(--ff-series-1) hover:underline"
href={showUrl(item.id)}
title={a.name}
>{a.name}</a
@@ -668,7 +682,9 @@
</ContextMenu>
{/if}
{#snippet footer()}
<a class="text-brand hover:underline" href={createHref}
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href={createHref}
>Create a new {TYPE_ONE[type]}</a
>
{#if failure}
+4 -2
View File
@@ -361,7 +361,9 @@
{#if a && a.type !== "asset"}
<p class="text-expense">
You can only reconcile asset accounts.
<a class="text-brand underline" href={showUrl(id)}>Back to {a.name}</a>
<a class="text-brand dark:text-(--ff-series-1) underline" href={showUrl(id)}
>Back to {a.name}</a
>
</p>
{:else}
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_25rem]">
@@ -512,7 +514,7 @@
<p class="text-cladd-fg-soft">
The first correction for this account has to be booked in
<a
class="text-brand underline"
class="text-brand dark:text-(--ff-series-1) underline"
data-sveltekit-reload
href="{page.url.pathname}?ffui=old"
>Firefly's own reconcile page</a
+26 -7
View File
@@ -9,6 +9,8 @@
const GAP = 2;
const RADIUS = 4;
const WIDE_BASE = 200;
const CHAR = 7;
let {
keys,
@@ -27,6 +29,7 @@
} = $props();
let list = $state<HTMLElement>();
let width = $state(0);
const stacks = $derived(rows.map((row) => ({ row, ...stack(row, keys) })));
const lo = $derived(Math.min(0, ...stacks.map((s) => s.negative)));
@@ -34,6 +37,14 @@
const span = $derived(hi - lo || 1);
const pos = (value: number) => ((value - lo) / span) * 100;
const total = (s: (typeof stacks)[number]) => s.positive + s.negative;
const valueText = (s: (typeof stacks)[number]) =>
s.row.note ?? (s.row.format ?? format)(total(s));
const wide = $derived(
width === 0 ||
width >=
WIDE_BASE +
Math.max(0, ...stacks.map((x) => valueText(x).length)) * CHAR
);
function box({ from, to, outer }: Segment, first: boolean) {
const up = to > from;
@@ -70,16 +81,20 @@
}
</script>
<div class="@container">
<div bind:clientWidth={width}>
<ul
aria-label={label}
class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 text-[12px] @min-[30rem]:grid-cols-(--cols)"
class="grid items-center gap-x-3 text-[12px]"
bind:this={list}
style:--cols="{labelWidth} minmax(4rem, 1fr) auto"
style:grid-template-columns={wide
? `${labelWidth} minmax(4rem, 1fr) auto`
: "minmax(0, 1fr) auto"}
>
{#each stacks as s (s.row.key)}
<li
class="col-span-2 grid grid-cols-subgrid items-center gap-y-1 rounded-sm px-1 py-1.5 focus-within:bg-row-cursor hover:bg-row-hover @min-[30rem]:col-span-3 @min-[30rem]:h-7 @min-[30rem]:py-0"
class="grid grid-cols-subgrid items-center rounded-sm px-1 focus-within:bg-row-cursor hover:bg-row-hover {wide
? "col-span-3 h-7"
: "col-span-2 gap-y-1 py-1.5"}"
>
{#if s.row.href}
<a
@@ -109,7 +124,9 @@
>
{/if}
<div
class="relative col-span-2 row-start-2 h-3 @min-[30rem]:col-span-1 @min-[30rem]:col-start-2 @min-[30rem]:row-start-1"
class="relative h-3 {wide
? "col-start-2 row-start-1"
: "col-span-2 row-start-2"}"
>
{#if lo < 0}
<span
@@ -133,8 +150,10 @@
{/each}
</div>
<span
class="num col-start-2 row-start-1 text-right text-cladd-fg @min-[30rem]:col-start-3"
>{s.row.note ?? (s.row.format ?? format)(total(s))}</span
class="num row-start-1 text-right text-cladd-fg {wide
? "col-start-3"
: "col-start-2"}"
>{valueText(s)}</span
>
</li>
{/each}
@@ -152,7 +152,11 @@
{:else if panels.length === 0}
<p class="py-10 text-center text-cladd-fg-soft">
No accounts to draw. Pick the accounts for this chart in
<a class="text-brand underline" href="/preferences">Preferences</a>.
<a
class="text-brand dark:text-(--ff-series-1) underline"
href="/preferences"
>Preferences</a
>.
</p>
{:else}
{#each panels as panel (panel.cur.code)}
@@ -178,7 +182,9 @@
{/if}
</div>
{#snippet footer()}
<a class="text-brand hover:underline" href="/accounts/asset"
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="/accounts/asset"
>View your asset accounts</a
>
{#if converted}
@@ -92,7 +92,11 @@
<p class="py-6 text-center text-cladd-fg-soft">
No budgets with a limit or spending in this period. Set an amount for a
budget on the
<a class="text-brand underline" href="/budgets">budgets page</a>.
<a
class="text-brand dark:text-(--ff-series-1) underline"
href="/budgets"
>budgets page</a
>.
</p>
{:else}
{#each visible as group (group.code)}
@@ -116,6 +120,10 @@
{/if}
</div>
{#snippet footer()}
<a class="text-brand hover:underline" href="/budgets">Go to your budgets</a>
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="/budgets"
>Go to your budgets</a
>
{/snippet}
</Box>
@@ -101,7 +101,9 @@
{/if}
</div>
{#snippet footer()}
<a class="text-brand hover:underline" href="/categories"
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="/categories"
>Go to your categories</a
>
{/snippet}
+3 -1
View File
@@ -95,7 +95,9 @@
{/each}
</div>
{#snippet footer()}
<a class="text-brand hover:underline" href="/accounts/liabilities"
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="/accounts/liabilities"
>See your liabilities</a
>
{/snippet}
@@ -164,7 +164,7 @@
: ""}"
>
<span
class="hidden size-10 shrink-0 place-items-center rounded-cladd-md text-white sm:grid"
class="hidden size-10 shrink-0 place-items-center rounded-cladd-md text-white sm:grid dark:text-black/75"
style:background={color}
>
<Icon class="size-5" />
@@ -137,7 +137,9 @@
{/if}
</div>
{#snippet footer()}
<a class="text-brand hover:underline" href="/accounts/{kind}"
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="/accounts/{kind}"
>See your {kind} accounts</a
>
{/snippet}
@@ -62,7 +62,9 @@
{/each}
</ul>
{#snippet footer()}
<a class="text-brand hover:underline" href="/piggy-banks"
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="/piggy-banks"
>Go to your piggy banks</a
>
{/snippet}
@@ -149,7 +149,9 @@
{/if}
</div>
{#snippet footer()}
<a class="text-brand hover:underline" href="/subscriptions"
<a
class="text-brand dark:text-(--ff-series-1) hover:underline"
href="/subscriptions"
>Go to your subscriptions</a
>
{/snippet}
+1 -1
View File
@@ -217,5 +217,5 @@ export const accountsQuery = (type: string, start: string, end: string) => ({
});
export function proportional(counts: number[]): string {
return counts.map((n) => `minmax(0, ${Math.max(2, n)}fr)`).join(" ");
return counts.map((n) => `minmax(8rem, ${Math.max(2, n)}fr)`).join(" ");
}