feat(accounts): the account page as a statement, all accounts instead of the audit report
Asset and liability pages open with opening plus income and money moved in, minus spending and money moved out, equals closing, then thirteen months of in and out next to the daily balance, and the statement with its balance column, day subtotals on ; and the last reconciled date. The audit report becomes one table of all accounts for the period.
This commit is contained in:
1 parent
b10a6bd21a
commit
f3a597d39c
11 files changed
+1118
-548
No files matched your search
@@ -1,246 +0,0 @@
|
||||
<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>
|
||||
@@ -1,73 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import Box from "#lib/components/Box.svelte";
|
||||
import {
|
||||
AUDIT_COLUMNS,
|
||||
type AuditColumn,
|
||||
DEFAULT_AUDIT,
|
||||
} from "#lib/reports/audit.js";
|
||||
import { reportQueries } from "#lib/reports/queries.js";
|
||||
import { navigate, useReport } from "#lib/reports/report.svelte.js";
|
||||
import { page } from "$app/state";
|
||||
import AuditAccount from "./AuditAccount.svelte";
|
||||
|
||||
const report = useReport();
|
||||
const p = $derived(report.params);
|
||||
const balances = createQuery(() => reportQueries.balances(p.start, p.end));
|
||||
|
||||
const columns = $derived.by(() => {
|
||||
const raw = page.url.searchParams.get("cols");
|
||||
if (raw === null) {
|
||||
return DEFAULT_AUDIT;
|
||||
}
|
||||
const keys = new Set(raw.split(","));
|
||||
return AUDIT_COLUMNS.map((c) => c.key).filter((key) => keys.has(key));
|
||||
});
|
||||
const accounts = $derived(
|
||||
report.selected
|
||||
.map((id) => ({ account: report.byId().get(id), id }))
|
||||
.filter((a) => a.account)
|
||||
.sort((a, b) =>
|
||||
(a.account?.name ?? "").localeCompare(b.account?.name ?? "")
|
||||
)
|
||||
);
|
||||
|
||||
function toggle(key: AuditColumn, on: boolean) {
|
||||
const next = AUDIT_COLUMNS.map((c) => c.key).filter((k) =>
|
||||
k === key ? on : columns.includes(k)
|
||||
);
|
||||
navigate({ cols: next.join(",") });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Box body="px-3 py-2" title="Options">
|
||||
<fieldset class="flex flex-wrap gap-x-4 gap-y-1">
|
||||
<legend class="sr-only">Visible columns</legend>
|
||||
{#each AUDIT_COLUMNS as column (column.key)}
|
||||
<label class="flex h-7 items-center gap-1.5 text-[13px]">
|
||||
<input
|
||||
checked={columns.includes(column.key)}
|
||||
class="size-3.5 accent-[var(--ff-brand)]"
|
||||
onchange={(event) =>
|
||||
toggle(column.key, event.currentTarget.checked)}
|
||||
type="checkbox"
|
||||
>
|
||||
{column.label}
|
||||
</label>
|
||||
{/each}
|
||||
</fieldset>
|
||||
</Box>
|
||||
{#if !report.accountsReady}
|
||||
<p class="text-cladd-fg-soft">Loading</p>
|
||||
{/if}
|
||||
{#each accounts as item (item.id)}
|
||||
<AuditAccount
|
||||
account={item.account}
|
||||
after={balances.data?.after.get(item.id)}
|
||||
before={balances.data?.before.get(item.id)}
|
||||
{columns}
|
||||
id={item.id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import AllAccounts from "#lib/components/accounts/AllAccounts.svelte";
|
||||
import PageHeader from "#lib/components/shell/PageHeader.svelte";
|
||||
import { formatRange } from "#lib/format.js";
|
||||
import type { ReportParams } from "#lib/reports/params.js";
|
||||
import { createReport, provideReport } from "#lib/reports/report.svelte.js";
|
||||
import AuditReport from "./AuditReport.svelte";
|
||||
import DefaultReport from "./DefaultReport.svelte";
|
||||
import DimensionReport from "./DimensionReport.svelte";
|
||||
import ParamBar from "./ParamBar.svelte";
|
||||
@@ -14,7 +14,7 @@
|
||||
provideReport(report);
|
||||
|
||||
const SUBTITLES: Record<string, string> = {
|
||||
audit: "Transaction history account overview",
|
||||
audit: "All accounts",
|
||||
budget: "Budget report",
|
||||
category: "Category report",
|
||||
default: "Default financial report",
|
||||
@@ -44,7 +44,11 @@
|
||||
{#if params.type === "default"}
|
||||
<DefaultReport />
|
||||
{:else if params.type === "audit"}
|
||||
<AuditReport />
|
||||
<AllAccounts
|
||||
accounts={report.selected}
|
||||
end={params.end}
|
||||
start={params.start}
|
||||
/>
|
||||
{:else}
|
||||
{#key params.type}
|
||||
<DimensionReport />
|
||||
|
||||
Reference in new issue
Block a user