189 lines
6.2 KiB
Svelte
189 lines
6.2 KiB
Svelte
<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>
|