feat(dashboard): morning page, compact accounts, what the bot entered, next 7 days

The right column drops the box per account for one table per currency
with sparklines and the last five transactions on l, a list of what was
entered since the last look (x marks reviewed), bills and piggy bank
dates in the next seven days, and debts with friends net per currency.
The left column opens with spending so far against a typical month, and
$ shows totals in UAH.
This commit is contained in:
hh committed 2026-10-16 15:14:03 +00:00
1 parent cea51281cd
commit 8c4e72666b
11 files changed
+1452 -312

No files matched your search

+59 -60
View File
@@ -1,93 +1,92 @@
<script lang="ts">
import { createQuery, keepPreviousData } from "@tanstack/svelte-query";
import AccountBox from "#lib/components/dashboard/AccountBox.svelte";
import { createQuery } from "@tanstack/svelte-query";
import AccountsBlock from "#lib/components/dashboard/AccountsBlock.svelte";
import AccountsChart from "#lib/components/dashboard/AccountsChart.svelte";
import BudgetsChart from "#lib/components/dashboard/BudgetsChart.svelte";
import CategoriesChart from "#lib/components/dashboard/CategoriesChart.svelte";
import DebtsBox from "#lib/components/dashboard/DebtsBox.svelte";
import {
accountsChartQuery,
accountsQuery,
seriesPoints,
} from "#lib/components/dashboard/data.js";
import DueBox from "#lib/components/dashboard/DueBox.svelte";
import { accountsQuery } from "#lib/components/dashboard/data.js";
import InboxBox from "#lib/components/dashboard/InboxBox.svelte";
import InfoBoxes from "#lib/components/dashboard/InfoBoxes.svelte";
import InsightBox from "#lib/components/dashboard/InsightBox.svelte";
import PaceBox from "#lib/components/dashboard/PaceBox.svelte";
import PiggyBanksBox from "#lib/components/dashboard/PiggyBanksBox.svelte";
import SubscriptionsBox from "#lib/components/dashboard/SubscriptionsBox.svelte";
import PageHeader from "#lib/components/shell/PageHeader.svelte";
import { today } from "#lib/dates.js";
import Checkbox from "#lib/components/ui/checkbox/checkbox.svelte";
import { provideColumn } from "#lib/dashboard/column.svelte.js";
import { inPrimary, ratesQuery } from "#lib/dashboard/primary.svelte.js";
import { useKeys } from "#lib/keys.svelte.js";
import { period } from "#lib/period.svelte.js";
import { session } from "#lib/session.svelte.js";
const assets = createQuery(() => ({
...accountsQuery("asset", period.start, period.end),
placeholderData: keepPreviousData,
}));
const chart = createQuery(() => ({
...accountsChartQuery(period.start, period.end),
placeholderData: keepPreviousData,
}));
provideColumn();
const frontpage = $derived.by(() => {
const list = (assets.data ?? [])
.filter((a) => a.attributes.active !== false)
.sort((a, b) => (a.attributes.order ?? 0) - (b.attributes.order ?? 0));
if (!("frontpageAccounts" in session.prefs)) {
return list;
}
const ids = new Set(session.frontpageAccounts);
return list.filter((a) => ids.has(a.id));
});
const rates = createQuery(() => ratesQuery());
const primary = $derived(session.primary?.code ?? "");
const ownRates = $derived(
inPrimary.on && Object.values(rates.data ?? {}).some((r) => r.own)
);
const trends = $derived(
new Map(
(chart.data ?? []).map((set) => {
const points = seriesPoints(set, false);
return [
set.label,
{
marker: points.findIndex((p) => p.x === today()),
values: points.map((p) => p.y),
},
];
})
)
useKeys(() => [
{
group: "Dashboard",
keys: "$",
label: `Show totals in ${primary}`,
run: () => inPrimary.toggle(),
},
]);
const assets = createQuery(() =>
accountsQuery("asset", period.start, period.end)
);
$effect(() => {
if (
assets.isSuccess &&
!assets.isPlaceholderData &&
assets.data.length === 0
) {
if (assets.isSuccess && assets.data.length === 0) {
location.assign("/new-user");
}
});
</script>
<PageHeader subtitle="What's playing?" title="Dashboard" />
<PageHeader subtitle="What's playing?" title="Dashboard">
{#snippet actions()}
<div class="flex items-center gap-1.5 text-[12px] text-cladd-fg-soft">
<Checkbox
checked={inPrimary.on}
id="in-primary"
onCheckedChange={() => inPrimary.toggle()}
/>
<label for="in-primary">Show in {primary}</label>
<span class="hidden sm:inline"><span class="kbd">$</span></span>
{#if ownRates}
<span>≈ from your own exchanges</span>
{/if}
</div>
{/snippet}
</PageHeader>
<InfoBoxes />
<div class="mt-3 grid gap-3 lg:grid-cols-12">
<div class="flex min-w-0 flex-col gap-3 lg:col-span-8">
<div
class="mt-3 grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(25rem,36%)] xl:grid-rows-[auto_1fr]"
>
<PaceBox class="xl:col-start-1" />
<div
class="flex min-w-0 flex-col gap-3 xl:col-start-2 xl:row-span-2 xl:row-start-1"
>
<AccountsBlock />
<InboxBox />
<DueBox />
<DebtsBox />
<SubscriptionsBox />
<PiggyBanksBox />
</div>
<div class="flex min-w-0 flex-col gap-3 self-start xl:col-start-1">
<AccountsChart />
<BudgetsChart />
<CategoriesChart />
</div>
<div class="flex min-w-0 flex-col gap-3 lg:col-span-4">
{#each frontpage as account (account.id)}
{@const trend = trends.get(account.attributes.name)}
<AccountBox
{account}
marker={trend?.marker ?? -1}
trend={trend?.values ?? []}
/>
{/each}
<SubscriptionsBox />
<PiggyBanksBox />
<DebtsBox />
</div>
</div>
<div class="mt-3 flex flex-col gap-3">