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