feat(reports): live firefly reports with drill-down, money flow and spending calendar

This commit is contained in:
hh committed 2026-10-16 03:45:43 +00:00
1 parent 60caee10dc
commit 62cd17ecde
39 files changed
+6596

No files matched your search

@@ -0,0 +1,96 @@
<script lang="ts">
import { createQuery } from "@tanstack/svelte-query";
import type { LineSeries } from "#lib/components/charts/chart.js";
import LineChart from "#lib/components/charts/LineChart.svelte";
import { contains, today } from "#lib/dates.js";
import { formatDate } from "#lib/format.js";
import { reportQueries } from "#lib/reports/queries.js";
import { money, useReport } from "#lib/reports/report.svelte.js";
import { modeAxis } from "#lib/reports/view.js";
import ReportBox from "./ReportBox.svelte";
let { net = false }: { net?: boolean } = $props();
const report = useReport();
const p = $derived(report.params);
const ids = $derived(
report.selected.filter(
(id) => !net || report.byId().get(id)?.include_net_worth !== false
)
);
const period = $derived.by(() => {
if (!net) {
return "1D";
}
return report.template === "multi-year" ? "1M" : "1W";
});
const chart = createQuery(() => ({
...reportQueries.chart(p.start, p.end, period, ids),
enabled: report.accountsReady && ids.length > 0,
}));
const charts = $derived.by(() => {
const byCode = new Map<string, LineSeries[]>();
for (const set of chart.data ?? []) {
const code = set.currency_code ?? "";
const points = Object.entries(set.entries ?? {}).map(([x, y]) => ({
x: x.slice(0, 10),
y: Number(y),
}));
const list = byCode.get(code) ?? [];
if (net && list.length > 0) {
const [sum] = list;
sum.points = sum.points.map((point, i) => ({
x: point.x,
y: point.y + (points[i]?.y ?? 0),
}));
} else {
list.push({
format: (value: number) => money(code, value),
key: net ? code : (set.label ?? code),
label: net ? `Net worth in ${code}` : (set.label ?? code),
points,
});
}
byCode.set(code, list);
}
return [...byCode.entries()].sort(
(a, b) => report.codes.indexOf(a[0]) - report.codes.indexOf(b[0])
);
});
const marker = $derived(contains(p, today()) ? today() : undefined);
const xFormat = $derived(
report.template === "multi-year"
? (x: string) => formatDate(x, "month")
: (x: string) => formatDate(x, "short")
);
</script>
<ReportBox
body="p-3"
empty={charts.length === 0}
emptyText="No accounts to chart."
id={net ? "net-worth" : "balance-chart"}
loading={chart.isPending && ids.length > 0}
title={net ? "Net worth" : "Account balances"}
>
<div class="grid gap-x-6 gap-y-4 {charts.length > 1 ? "xl:grid-cols-2" : ""}">
{#each charts as [code, series] (code)}
<div class="min-w-0">
{#if charts.length > 1 || !net}
<h3 class="mb-1 font-semibold text-[12px] text-cladd-fg-soft">
{code}
</h3>
{/if}
<LineChart
axisFormat={modeAxis(code)}
height={charts.length > 1 ? 200 : 240}
label="{net ? "Net worth" : "Account balances"} in {code}"
{marker}
{series}
{xFormat}
/>
</div>
{/each}
</div>
</ReportBox>