feat(reports): live firefly reports with drill-down, money flow and spending calendar
This commit is contained in:
1 parent
60caee10dc
commit
62cd17ecde
39 files changed
+6596
No files matched your search
@@ -0,0 +1,246 @@
|
||||
<script lang="ts">
|
||||
import ArrowLeft from "@lucide/svelte/icons/arrow-left";
|
||||
import ArrowLeftRight from "@lucide/svelte/icons/arrow-left-right";
|
||||
import ArrowRight from "@lucide/svelte/icons/arrow-right";
|
||||
import Calculator from "@lucide/svelte/icons/calculator";
|
||||
import Star from "@lucide/svelte/icons/star";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import type { Account } from "#lib/api/types.js";
|
||||
import Box from "#lib/components/Box.svelte";
|
||||
import { addDays } from "#lib/dates.js";
|
||||
import { formatDate, formatDateTime } from "#lib/format.js";
|
||||
import { type AuditColumn, ledger } from "#lib/reports/audit.js";
|
||||
import { reportQueries } from "#lib/reports/queries.js";
|
||||
import { money, useReport } from "#lib/reports/report.svelte.js";
|
||||
|
||||
let {
|
||||
id,
|
||||
account,
|
||||
before,
|
||||
after,
|
||||
columns,
|
||||
}: {
|
||||
id: string;
|
||||
account: Account | undefined;
|
||||
before: number | undefined;
|
||||
after: number | undefined;
|
||||
columns: AuditColumn[];
|
||||
} = $props();
|
||||
|
||||
const report = useReport();
|
||||
const p = $derived(report.params);
|
||||
const code = $derived(account?.currency_code ?? "");
|
||||
const query = createQuery(() => reportQueries.ledger(id, p.start, p.end));
|
||||
const rows = $derived(
|
||||
query.data && before !== undefined
|
||||
? ledger(query.data, id, code, before)
|
||||
: undefined
|
||||
);
|
||||
const show = (key: AuditColumn) => columns.includes(key);
|
||||
const ICONS = {
|
||||
deposit: ArrowRight,
|
||||
"opening balance": Star,
|
||||
reconciliation: Calculator,
|
||||
transfer: ArrowLeftRight,
|
||||
withdrawal: ArrowLeft,
|
||||
} as const;
|
||||
const META_DATES = [
|
||||
"interest_date",
|
||||
"book_date",
|
||||
"process_date",
|
||||
"due_date",
|
||||
"payment_date",
|
||||
"invoice_date",
|
||||
] as const;
|
||||
const dateColumns = $derived(META_DATES.filter((key) => show(key)));
|
||||
</script>
|
||||
|
||||
<Box body="p-0" href="/accounts/show/{id}" title={account?.name ?? `#${id}`}>
|
||||
{#if !rows}
|
||||
<p class="px-3 py-3 text-cladd-fg-soft">Loading</p>
|
||||
{:else if rows.length === 0}
|
||||
<p class="px-3 py-3">
|
||||
No activity was recorded on account
|
||||
<a
|
||||
class="text-brand dark:text-[var(--ff-series-1)] hover:underline"
|
||||
href="/accounts/show/{id}"
|
||||
>{account?.name}</a
|
||||
>
|
||||
between {formatDate(p.start, "long")} and {formatDate(p.end, "long")}.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="px-3 py-2 text-[13px]">
|
||||
Account balance of <strong>{account?.name}</strong> at the end of
|
||||
{formatDate(addDays(p.start, -1), "long")}
|
||||
was:
|
||||
<span class="num font-semibold">{money(code, before ?? 0)}</span>
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse text-[13px]">
|
||||
<thead>
|
||||
<tr class="text-left text-[12px] text-cladd-fg-soft">
|
||||
{#if show("icon")}
|
||||
<th class="h-7 w-8 px-2"><span class="sr-only">Type</span></th>
|
||||
{/if}
|
||||
{#if show("description")}
|
||||
<th class="h-7 px-3 font-medium">Description</th>
|
||||
{/if}
|
||||
{#if show("balance_before")}
|
||||
<th class="h-7 px-3 text-right font-medium">Balance before</th>
|
||||
{/if}
|
||||
{#if show("amount")}
|
||||
<th class="h-7 px-3 text-right font-medium">Amount</th>
|
||||
{/if}
|
||||
{#if show("balance_after")}
|
||||
<th class="h-7 px-3 text-right font-medium">Balance after</th>
|
||||
{/if}
|
||||
{#if show("date")}
|
||||
<th class="h-7 px-3 font-medium">Date</th>
|
||||
{/if}
|
||||
{#if show("from")}
|
||||
<th class="h-7 px-3 font-medium">From</th>
|
||||
{/if}
|
||||
{#if show("to")}
|
||||
<th class="h-7 px-3 font-medium">To</th>
|
||||
{/if}
|
||||
{#if show("budget")}
|
||||
<th class="h-7 px-3 font-medium">Budget</th>
|
||||
{/if}
|
||||
{#if show("category")}
|
||||
<th class="h-7 px-3 font-medium">Category</th>
|
||||
{/if}
|
||||
{#if show("bill")}
|
||||
<th class="h-7 px-3 font-medium">Subscription</th>
|
||||
{/if}
|
||||
{#if show("created_at")}
|
||||
<th class="h-7 px-3 font-medium">Created at</th>
|
||||
{/if}
|
||||
{#if show("updated_at")}
|
||||
<th class="h-7 px-3 font-medium">Updated at</th>
|
||||
{/if}
|
||||
{#if show("notes")}
|
||||
<th class="h-7 px-3 font-medium">Notes</th>
|
||||
{/if}
|
||||
{#each dateColumns as key (key)}
|
||||
<th class="h-7 px-3 font-medium">
|
||||
{key.replace("_date", "").replace("process", "processing")}
|
||||
date
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as row (row.split.transaction_journal_id)}
|
||||
{@const s = row.split}
|
||||
{@const Icon =
|
||||
ICONS[s.type as keyof typeof ICONS] ?? ArrowLeftRight}
|
||||
<tr
|
||||
class="h-7 border-cladd-outline border-t align-top hover:bg-row-hover"
|
||||
>
|
||||
{#if show("icon")}
|
||||
<td class="px-2 py-1.5 text-cladd-fg-soft">
|
||||
<Icon aria-label={s.type} class="size-3.5" />
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("description")}
|
||||
<td class="max-w-[22rem] px-3 py-1">
|
||||
<a
|
||||
class="hover:underline"
|
||||
href="/transactions/show/{row.group}"
|
||||
>
|
||||
{#if row.title}
|
||||
{row.title}
|
||||
({s.description})
|
||||
{:else}
|
||||
{s.description}
|
||||
{/if}
|
||||
</a>
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("balance_before")}
|
||||
<td
|
||||
class="num whitespace-nowrap px-3 py-1 text-right text-cladd-fg-soft"
|
||||
>
|
||||
{money(code, row.before)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("amount")}
|
||||
<td
|
||||
class="num whitespace-nowrap px-3 py-1 text-right {row.amount <
|
||||
0
|
||||
? "text-expense"
|
||||
: "text-income"}"
|
||||
>
|
||||
{money(code, row.amount)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("balance_after")}
|
||||
<td class="num whitespace-nowrap px-3 py-1 text-right">
|
||||
{money(code, row.after)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("date")}
|
||||
<td class="num whitespace-nowrap px-3 py-1">
|
||||
{formatDate(s.date, "medium")}
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("from")}
|
||||
<td class="max-w-[12rem] truncate px-3 py-1">
|
||||
<a class="hover:underline" href="/accounts/show/{s.source_id}"
|
||||
>{s.source_name}</a
|
||||
>
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("to")}
|
||||
<td class="max-w-[12rem] truncate px-3 py-1">
|
||||
<a
|
||||
class="hover:underline"
|
||||
href="/accounts/show/{s.destination_id}"
|
||||
>{s.destination_name}</a
|
||||
>
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("budget")}
|
||||
<td class="px-3 py-1">{s.budget_name ?? ""}</td>
|
||||
{/if}
|
||||
{#if show("category")}
|
||||
<td class="px-3 py-1">{s.category_name ?? ""}</td>
|
||||
{/if}
|
||||
{#if show("bill")}
|
||||
<td class="px-3 py-1">{s.bill_name ?? ""}</td>
|
||||
{/if}
|
||||
{#if show("created_at")}
|
||||
<td class="num whitespace-nowrap px-3 py-1 text-cladd-fg-soft">
|
||||
{formatDateTime(row.created)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("updated_at")}
|
||||
<td class="num whitespace-nowrap px-3 py-1 text-cladd-fg-soft">
|
||||
{formatDateTime(row.updated)}
|
||||
</td>
|
||||
{/if}
|
||||
{#if show("notes")}
|
||||
<td
|
||||
class="max-w-[24rem] whitespace-pre-line px-3 py-1 text-[12px] text-cladd-fg-soft"
|
||||
>
|
||||
{s.notes ?? ""}
|
||||
</td>
|
||||
{/if}
|
||||
{#each dateColumns as key (key)}
|
||||
<td class="num whitespace-nowrap px-3 py-1">
|
||||
{formatDate(s[key] ?? null, "medium")}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="border-cladd-outline border-t px-3 py-2 text-[13px]">
|
||||
Account balance of <strong>{account?.name}</strong> at the end of
|
||||
{formatDate(p.end, "long")}
|
||||
was:
|
||||
<span class="num font-semibold">{money(code, after ?? 0)}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</Box>
|
||||
Reference in new issue
Block a user