feat: one page for a category, tag, budget or merchant
The page answers how much and whether that is normal: this month per currency with count, median and pace against a typical month, thirteen months as lanes per currency, and what is inside (merchants, cards, budgets, tags, subtags) on 1 to 4 with Enter narrowing the transactions below. Budgets keep spent, left and overspent with the limit line; expense and revenue accounts use the same page with visits and cadence. The twelve period cards and per-day charts are gone.
This commit is contained in:
1 parent
349a6a0b32
commit
722fdc2a20
19 files changed
+1667
-794
No files matched your search
@@ -0,0 +1,147 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { daysBetween } from "#lib/dates.js";
|
||||
import type { DimensionModel } from "#lib/dimension/model.svelte.js";
|
||||
import { counted, fmt, type Money } from "#lib/dimension/stats.js";
|
||||
import { formatDate, formatNumber } from "#lib/format.js";
|
||||
import { period } from "#lib/period.svelte.js";
|
||||
import { monthOf } from "#lib/reports/board.js";
|
||||
import { versusText } from "#lib/reports/typical.js";
|
||||
|
||||
const WEEK = 7;
|
||||
const SAME = 0.03;
|
||||
|
||||
let {
|
||||
model,
|
||||
noun,
|
||||
lead,
|
||||
}: { model: DimensionModel; noun: string; lead?: Snippet } = $props();
|
||||
|
||||
const facts = $derived(model.facts);
|
||||
const when = $derived.by(() => {
|
||||
if (!model.range) {
|
||||
return "all time";
|
||||
}
|
||||
if (model.focus === monthOf(model.now) && period.isCurrent) {
|
||||
return "this month";
|
||||
}
|
||||
return `in ${period.label}`;
|
||||
});
|
||||
const days = $derived.by(() => {
|
||||
const start = model.range?.start ?? model.history.first;
|
||||
const end =
|
||||
model.range && model.range.end < model.now ? model.range.end : model.now;
|
||||
return start ? daysBetween(start, end) + 1 : 0;
|
||||
});
|
||||
const perWeek = $derived(days >= WEEK ? (facts.count * WEEK) / days : null);
|
||||
const main = $derived(model.norms[0]);
|
||||
const typicals = $derived(model.norms.filter((n) => n.typical > 0));
|
||||
const active = $derived(
|
||||
model.typicalMonths.filter((month) =>
|
||||
model.main.some((flow) => monthOf(flow.date) === month)
|
||||
).length
|
||||
);
|
||||
const good = $derived(model.dir === 1);
|
||||
|
||||
function tone(ratio: number | null) {
|
||||
if (ratio === null || Math.abs(ratio) < SAME) {
|
||||
return "text-cladd-fg-soft";
|
||||
}
|
||||
return ratio > 0 === good ? "text-income" : "text-expense";
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet amounts(
|
||||
sums: Money[],
|
||||
big: boolean
|
||||
)}
|
||||
{#each sums as sum, i (sum.code)}
|
||||
{#if i > 0}
|
||||
<span class="text-cladd-fg-soft">+</span>
|
||||
{/if}
|
||||
<span class="num font-semibold text-cladd-fg {big ? "text-[22px]" : ""}"
|
||||
>{fmt(sum.code, sum.amount, 0)}</span
|
||||
>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
<div class="flex flex-col gap-1 pb-3">
|
||||
{#if lead}
|
||||
{@render lead()}
|
||||
{:else if !model.loading}
|
||||
<p
|
||||
class="flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5 text-[14px] text-cladd-fg-soft"
|
||||
>
|
||||
{#if facts.count > 0}
|
||||
{@render amounts(facts.sums, true)}
|
||||
<span>{when}</span>
|
||||
<span>· {counted(facts.count, noun)}</span>
|
||||
{#if facts.median}
|
||||
<span
|
||||
>· median
|
||||
<span class="num text-cladd-fg"
|
||||
>{fmt(facts.median.code, facts.median.amount, 0)}</span
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
{#if perWeek !== null}
|
||||
<span
|
||||
>·
|
||||
<span class="num text-cladd-fg"
|
||||
>{formatNumber(perWeek, perWeek < 10 ? 1 : 0)}</span
|
||||
>
|
||||
per week</span
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="font-semibold text-[22px] text-cladd-fg"
|
||||
>Nothing {when}</span
|
||||
>
|
||||
{#if model.history.last}
|
||||
<span>· last {formatDate(model.history.last)}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
{#if !model.loading && main}
|
||||
<p
|
||||
class="flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5 text-[13px] text-cladd-fg-soft"
|
||||
>
|
||||
{#if lead}
|
||||
{#if facts.count > 0}
|
||||
{@render amounts(facts.sums, false)}
|
||||
<span>{when} ·</span>
|
||||
{:else}
|
||||
<span>nothing {when} ·</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if typicals.length > 0}
|
||||
<span>typical month</span>
|
||||
{@render amounts(
|
||||
typicals.map((n) => ({ amount: n.typical, code: n.code })),
|
||||
false
|
||||
)}
|
||||
{#if main.projected !== null}
|
||||
<span
|
||||
>· on pace for ≈
|
||||
<span class="num font-semibold text-cladd-fg"
|
||||
>{fmt(main.code, main.projected, 0)}</span
|
||||
>,</span
|
||||
>
|
||||
{:else if main.ratio !== null}
|
||||
<span>·</span>
|
||||
{/if}
|
||||
{#if main.ratio !== null}
|
||||
<span class="font-semibold {tone(main.ratio)}"
|
||||
>{versusText(main.ratio)}</span
|
||||
>
|
||||
{/if}
|
||||
{:else if model.typicalMonths.length > 0}
|
||||
<span
|
||||
>in {active} of the last {model.typicalMonths.length}
|
||||
{model.typicalMonths.length === 1 ? "month" : "months"}</span
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in new issue
Block a user