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()}
|
{#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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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(" ");
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user