feat(dashboard,accounts): reconcile, debts box, per-currency charts, validated series palette
This commit is contained in:
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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(" ");
|
||||
}
|
||||
Reference in new issue
Block a user