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,188 @@
|
||||
<script lang="ts">
|
||||
import ChevronDown from "@lucide/svelte/icons/chevron-down";
|
||||
import { createQuery, keepPreviousData } from "@tanstack/svelte-query";
|
||||
import { DropdownMenu } from "bits-ui";
|
||||
import { api } from "#lib/api/client.js";
|
||||
import type {
|
||||
Account,
|
||||
Item,
|
||||
Page,
|
||||
TransactionGroup,
|
||||
} from "#lib/api/types.js";
|
||||
import Amount from "#lib/components/Amount.svelte";
|
||||
import {
|
||||
canReconcile,
|
||||
createUrl,
|
||||
editUrl,
|
||||
go,
|
||||
openOld,
|
||||
reconcileUrl,
|
||||
showUrl,
|
||||
} from "#lib/components/accounts/links.js";
|
||||
import Box from "#lib/components/Box.svelte";
|
||||
import Sparkline from "#lib/components/charts/Sparkline.svelte";
|
||||
import { span } from "#lib/components/dashboard/data.js";
|
||||
import DropdownMenuContent from "#lib/components/ui/dropdown-menu/dropdown-menu-content.svelte";
|
||||
import DropdownMenuItem from "#lib/components/ui/dropdown-menu/dropdown-menu-item.svelte";
|
||||
import { contains } from "#lib/dates.js";
|
||||
import { formatDate, formatMoney, toNumber } from "#lib/format.js";
|
||||
import { period } from "#lib/period.svelte.js";
|
||||
import {
|
||||
groupTitle,
|
||||
signedAmount,
|
||||
signedForeign,
|
||||
} from "#lib/transactions/split.js";
|
||||
|
||||
const LIMIT = 10;
|
||||
const TRIGGER =
|
||||
"cladd-surface cladd-gradient cladd-outline cladd-hoverable inline-flex h-cladd-sm items-center gap-1 rounded-cladd-sm px-2.5 font-semibold text-[12px]";
|
||||
|
||||
let {
|
||||
account,
|
||||
trend = [],
|
||||
marker = -1,
|
||||
}: { account: Item<Account>; trend?: number[]; marker?: number } = $props();
|
||||
|
||||
const a = $derived(account.attributes);
|
||||
const cur = $derived({
|
||||
code: a.currency_code,
|
||||
decimals: a.currency_decimal_places,
|
||||
symbol: a.currency_symbol,
|
||||
});
|
||||
|
||||
const list = createQuery(() => ({
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: () =>
|
||||
api<Page<TransactionGroup>>(`/v1/accounts/${account.id}/transactions`, {
|
||||
query: { ...span(period.start, period.end), limit: LIMIT },
|
||||
}),
|
||||
queryKey: ["account-tx", account.id, period.start, period.end, LIMIT],
|
||||
}));
|
||||
|
||||
const rows = $derived(
|
||||
(list.data?.data ?? [])
|
||||
.flatMap((group) =>
|
||||
group.attributes.transactions
|
||||
.filter(
|
||||
(s) =>
|
||||
(s.source_id === account.id || s.destination_id === account.id) &&
|
||||
contains(period.range, s.date)
|
||||
)
|
||||
.map((split, i) => ({
|
||||
foreign: signedForeign(split, account.id),
|
||||
group,
|
||||
key: `${group.id}-${i}`,
|
||||
money: signedAmount(split, account.id),
|
||||
split,
|
||||
title:
|
||||
group.attributes.transactions.length > 1
|
||||
? `${groupTitle(group)}: ${split.description}`
|
||||
: split.description,
|
||||
}))
|
||||
)
|
||||
.slice(0, LIMIT)
|
||||
);
|
||||
const change = $derived(toNumber(a.balance_difference));
|
||||
const trendColor = $derived(
|
||||
change < 0 ? "var(--ff-expense)" : "var(--ff-income)"
|
||||
);
|
||||
</script>
|
||||
|
||||
<Box body="py-1" href={showUrl(account.id)} title={a.name}>
|
||||
{#snippet tools()}
|
||||
{#if trend.length > 1}
|
||||
<Sparkline color={trendColor} {marker} values={trend} width={72} />
|
||||
{/if}
|
||||
{#if change !== 0}
|
||||
<Amount amount={change} signed {...cur} class="text-[12px]" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
<div class="transition-opacity {list.isPlaceholderData ? "opacity-60" : ""}">
|
||||
{#if list.isPending}
|
||||
<div class="h-14"></div>
|
||||
{:else if list.isError}
|
||||
<p class="px-3 py-2 text-expense">{list.error.message}</p>
|
||||
{:else if rows.length === 0}
|
||||
<p class="px-3 py-3 text-cladd-fg-soft">
|
||||
No transactions in this period.
|
||||
</p>
|
||||
{:else}
|
||||
<ul class="flex flex-col">
|
||||
{#each rows as row (row.key)}
|
||||
<li>
|
||||
<a
|
||||
class="flex h-10 items-center gap-2 px-3 text-[12.5px] hover:bg-row-hover sm:h-7"
|
||||
href="/transactions/show/{row.group.id}"
|
||||
>
|
||||
<span class="num w-12 shrink-0 text-cladd-fg-soft"
|
||||
>{formatDate(row.split.date.slice(0, 10), "short")}</span
|
||||
>
|
||||
<span
|
||||
class="min-w-0 flex-1 truncate text-cladd-fg"
|
||||
title={row.title}
|
||||
>{row.title}</span
|
||||
>
|
||||
{#if row.foreign}
|
||||
<span class="num shrink-0 text-[11px] text-cladd-fg-soft"
|
||||
>({formatMoney(row.foreign)})</span
|
||||
>
|
||||
{/if}
|
||||
<Amount
|
||||
amount={row.money.amount}
|
||||
code={row.money.code}
|
||||
decimals={row.money.decimals}
|
||||
symbol={row.money.symbol}
|
||||
tone={row.money.tone}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{#snippet footer()}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="{TRIGGER} cladd-color-green text-cladd-primary"
|
||||
>
|
||||
Create <ChevronDown class="size-3.5" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenuContent align="start" class="w-48">
|
||||
<DropdownMenuItem
|
||||
onSelect={() => go(createUrl("withdrawal", account.id))}
|
||||
>New withdrawal</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem onSelect={() => go(createUrl("deposit", account.id))}
|
||||
>New deposit</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem onSelect={() => go(createUrl("transfer", account.id))}
|
||||
>New transfer</DropdownMenuItem
|
||||
>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu.Root>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class="{TRIGGER} ml-auto text-cladd-fg">
|
||||
<span class="num"
|
||||
>{formatMoney({ amount: a.current_balance, ...cur })}</span
|
||||
>
|
||||
<ChevronDown class="size-3.5" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenuContent align="end" class="w-48">
|
||||
<DropdownMenuItem onSelect={() => go(showUrl(account.id))}
|
||||
>Show</DropdownMenuItem
|
||||
>
|
||||
{#if canReconcile(account)}
|
||||
<DropdownMenuItem onSelect={() => go(reconcileUrl(account.id))}
|
||||
>Reconcile</DropdownMenuItem
|
||||
>
|
||||
{/if}
|
||||
<DropdownMenuItem onSelect={() => go(editUrl(account.id))}
|
||||
>Edit</DropdownMenuItem
|
||||
>
|
||||
<DropdownMenuItem onSelect={() => openOld(showUrl(account.id))}
|
||||
>Open in old UI</DropdownMenuItem
|
||||
>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu.Root>
|
||||
{/snippet}
|
||||
</Box>
|
||||
Reference in new issue
Block a user