feat(dashboard,accounts,charts): v1 dashboard redrawn, account lists and pages, svg chart kit
This commit is contained in:
1 parent
62cd17ecde
commit
9e89829bae
36 files changed
+5678
-16
No files matched your search
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { api } from "#lib/api/client.js";
|
||||
import Box from "#lib/components/Box.svelte";
|
||||
import { tones } from "#lib/components/charts/chart.js";
|
||||
import {
|
||||
type ChartSet,
|
||||
currency,
|
||||
money,
|
||||
} from "#lib/components/dashboard/data.js";
|
||||
import {
|
||||
isDynamic,
|
||||
periodOf,
|
||||
type Range,
|
||||
shiftPeriod,
|
||||
type ViewRange,
|
||||
} from "#lib/dates.js";
|
||||
import { toNumber } from "#lib/format.js";
|
||||
import { period, periodLabel } from "#lib/period.svelte.js";
|
||||
|
||||
const COUNT = 12;
|
||||
const STEPS = new Set(["1D", "1W", "1M", "3M", "6M", "1Y"]);
|
||||
|
||||
let { accountId, code }: { accountId: string; code: string } = $props();
|
||||
|
||||
const view = $derived<ViewRange>(
|
||||
isDynamic(period.view) || !STEPS.has(period.view) ? "1M" : period.view
|
||||
);
|
||||
const blocks = $derived.by<Range[]>(() => {
|
||||
const list = [periodOf(period.end, view)];
|
||||
for (let i = 1; i < COUNT; i += 1) {
|
||||
list.push(shiftPeriod(list[i - 1], view, -1));
|
||||
}
|
||||
return list;
|
||||
});
|
||||
const oldest = $derived(blocks.at(-1)?.start ?? period.start);
|
||||
const newest = $derived(blocks[0]?.end ?? period.end);
|
||||
|
||||
const flows = createQuery(() => ({
|
||||
queryFn: () =>
|
||||
api<ChartSet[]>("/v1/chart/balance/balance", {
|
||||
query: {
|
||||
"accounts[]": accountId,
|
||||
end: newest,
|
||||
period: view,
|
||||
start: oldest,
|
||||
},
|
||||
}),
|
||||
queryKey: ["account-flows", accountId, oldest, newest, view],
|
||||
}));
|
||||
|
||||
interface Flow {
|
||||
code: string;
|
||||
earned: number;
|
||||
spent: number;
|
||||
}
|
||||
|
||||
const byStart = $derived.by(() => {
|
||||
const map = new Map<string, Flow[]>();
|
||||
for (const set of flows.data ?? []) {
|
||||
const kind = set.label === "earned" ? "earned" : "spent";
|
||||
for (const [date, value] of Object.entries(set.entries)) {
|
||||
const key = date.slice(0, 10);
|
||||
const list = map.get(key) ?? [];
|
||||
let flow = list.find((f) => f.code === set.currency_code);
|
||||
if (!flow) {
|
||||
flow = { code: set.currency_code, earned: 0, spent: 0 };
|
||||
list.push(flow);
|
||||
map.set(key, list);
|
||||
}
|
||||
flow[kind] += Math.abs(toNumber(value));
|
||||
}
|
||||
}
|
||||
for (const list of map.values()) {
|
||||
list.sort((a, b) => Number(b.code === code) - Number(a.code === code));
|
||||
}
|
||||
return map;
|
||||
});
|
||||
const scale = $derived(
|
||||
Math.max(
|
||||
1,
|
||||
...[...byStart.values()].flatMap((list) =>
|
||||
list
|
||||
.filter((f) => f.code === code)
|
||||
.map((f) => Math.max(f.earned, f.spent))
|
||||
)
|
||||
)
|
||||
);
|
||||
const isCurrent = (block: Range) =>
|
||||
block.start === period.start && block.end === period.end;
|
||||
</script>
|
||||
|
||||
<Box body="p-0 @container" title="Periods">
|
||||
<ul
|
||||
class="-mr-px grid grid-cols-1 overflow-hidden @xs:grid-cols-2 @lg:grid-cols-3 @2xl:grid-cols-4 @4xl:grid-cols-6"
|
||||
>
|
||||
{#each blocks as block (block.start)}
|
||||
{@const list = (byStart.get(block.start) ?? []).filter(
|
||||
(f) => f.earned || f.spent
|
||||
)}
|
||||
<li class="border-cladd-outline/60 border-r border-b">
|
||||
<a
|
||||
class="flex flex-col gap-1 px-3 py-2 hover:bg-row-hover {isCurrent(
|
||||
block
|
||||
)
|
||||
? "bg-row-cursor shadow-[inset_2px_0_0_var(--ff-brand)]"
|
||||
: ""}"
|
||||
href="/accounts/show/{accountId}/{block.start}/{block.end}"
|
||||
>
|
||||
<span class="font-semibold text-[12px] text-cladd-fg"
|
||||
>{periodLabel(block, view)}</span
|
||||
>
|
||||
{#if list.length === 0}
|
||||
<span class="text-[12px] text-cladd-fg-softer"
|
||||
>No transactions</span
|
||||
>
|
||||
{/if}
|
||||
{#each list as flow (flow.code)}
|
||||
{@const fmt = money(currency(flow.code))}
|
||||
<span
|
||||
class="num flex flex-wrap justify-between gap-x-2 text-[12px]"
|
||||
>
|
||||
{#if flow.earned}
|
||||
<span class="text-income">+{fmt(flow.earned)}</span>
|
||||
{/if}
|
||||
{#if flow.spent}
|
||||
<span class="ml-auto text-expense">−{fmt(flow.spent)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if flow.code === code}
|
||||
<span aria-hidden="true" class="flex h-1 gap-0.5">
|
||||
<span class="flex flex-1 justify-end">
|
||||
<span
|
||||
class="h-full rounded-l-full"
|
||||
style:background={tones.income}
|
||||
style:width="{(flow.earned / scale) * 100}%"
|
||||
></span>
|
||||
</span>
|
||||
<span class="flex-1">
|
||||
<span
|
||||
class="block h-full rounded-r-full"
|
||||
style:background={tones.expense}
|
||||
style:width="{(flow.spent / scale) * 100}%"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Box>
|
||||
Reference in new issue
Block a user