@hearthlog/module-finance 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +235 -0
- package/README.md +11 -2
- package/hearthlog.module.json +90 -0
- package/package.json +44 -3
- package/src/attention.ts +41 -0
- package/src/components/finance/AddAssetForm.svelte +58 -0
- package/src/components/finance/AddTransactionForm.svelte +127 -0
- package/src/components/finance/AssetDrawer.svelte +133 -0
- package/src/components/finance/AssetItem.svelte +96 -0
- package/src/components/finance/AssetView.svelte +165 -0
- package/src/components/finance/AssetsSheet.svelte +122 -0
- package/src/components/finance/BudgetItemActionsCell.svelte +18 -0
- package/src/components/finance/BudgetItemAmountCell.svelte +5 -0
- package/src/components/finance/BudgetItemCategoryCell.svelte +7 -0
- package/src/components/finance/BudgetItemDrawer.svelte +116 -0
- package/src/components/finance/BudgetItemNameCell.svelte +5 -0
- package/src/components/finance/BudgetItemSelectCell.svelte +25 -0
- package/src/components/finance/BudgetItemsTable.svelte +206 -0
- package/src/components/finance/BudgetPlanDrawer.svelte +89 -0
- package/src/components/finance/BudgetPlans.svelte +419 -0
- package/src/components/finance/EndlessTimeline.svelte +174 -0
- package/src/components/finance/FinanceReadout.svelte +229 -0
- package/src/components/finance/MonthSection.svelte +255 -0
- package/src/components/finance/RiverSlice.svelte +125 -0
- package/src/components/finance/SortUncategorizedSheet.svelte +198 -0
- package/src/components/finance/TimelineRowItem.svelte +319 -0
- package/src/components/finance/TransactionDrawer.svelte +480 -0
- package/src/components/finance/TransactionSelectionBar.svelte +200 -0
- package/src/components/finance/timeline-months.svelte.ts +37 -0
- package/src/components/finance/transaction-draft.ts +121 -0
- package/src/data/assets.ts +48 -0
- package/src/data/budgets.ts +123 -0
- package/src/data/definitions.ts +135 -0
- package/src/data/finance-timeline.ts +305 -0
- package/src/data/forecast.ts +256 -0
- package/src/data/planned-days.ts +57 -0
- package/src/data/repositories.ts +20 -0
- package/src/data/transactions.ts +452 -0
- package/src/finance/categories.ts +29 -0
- package/src/finance/category-pick.ts +8 -0
- package/src/finance/category-suggestions.ts +65 -0
- package/src/finance/floor.ts +29 -0
- package/src/finance/phone-balance.ts +22 -0
- package/src/finance/timeline.ts +113 -0
- package/src/pages/index.svelte +19 -0
- package/src/pages/index.ts +7 -0
- package/src/pages/layout.svelte +13 -0
- package/src/pages/layout.ts +18 -0
- package/src/pages/month.svelte +909 -0
- package/src/pages/month.ts +99 -0
- package/src/server/category-pick.ts +44 -0
- package/src/server/index.ts +11 -0
- package/src/styles.css +12 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { endOfMonth, format, parseISO } from 'date-fns';
|
|
2
|
+
import { formatNumber, MINUS } from '@hearthlog/sdk/format';
|
|
3
|
+
import { monthKeyOf } from '@hearthlog/sdk/axis';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure helpers for the Finance timeline: which filter the URL asks for, which
|
|
7
|
+
* months the endless list holds, and the geometry the balance line draws.
|
|
8
|
+
* Balances and totals always come from SQLite.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A plan is dated today or later; a row is done once marked done or once its day has passed. */
|
|
12
|
+
export type TimelineKind = 'planned' | 'done';
|
|
13
|
+
/** All lists done history above Today and plans below it; the others list one kind. */
|
|
14
|
+
export type TimelineShow = 'all' | TimelineKind;
|
|
15
|
+
/** 0 above the Today line (done up to today), 1 below it. */
|
|
16
|
+
export type TimelineSection = 0 | 1;
|
|
17
|
+
|
|
18
|
+
const DAY = 'yyyy-MM-dd';
|
|
19
|
+
|
|
20
|
+
/** All unless one kind is asked for; `?view=archive` links from the old Archived tab open done. */
|
|
21
|
+
export function parseShow(params: URLSearchParams): TimelineShow {
|
|
22
|
+
const show = params.get('show');
|
|
23
|
+
if (show === 'planned' || show === 'done') return show;
|
|
24
|
+
return params.get('view') === 'archive' ? 'done' : 'all';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether a filter lists rows of a kind. */
|
|
28
|
+
export function showsKind(show: TimelineShow, kind: TimelineKind): boolean {
|
|
29
|
+
return show === 'all' || show === kind;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DateWindow {
|
|
33
|
+
start: string;
|
|
34
|
+
end: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------- endless ranges
|
|
38
|
+
|
|
39
|
+
/** Months listed before and after today's month; a hundred years each way feels endless. */
|
|
40
|
+
export const MONTH_SPAN = 1200;
|
|
41
|
+
|
|
42
|
+
/** First and last month a filter lists; null runs on endlessly. */
|
|
43
|
+
export interface MonthBounds {
|
|
44
|
+
first: string | null;
|
|
45
|
+
last: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* All runs both ways without end. Plans start at today's month and run on
|
|
50
|
+
* ahead. Done history runs back without end and stops at today's month, or at
|
|
51
|
+
* a later month holding a row marked done ahead of its date.
|
|
52
|
+
*/
|
|
53
|
+
export function monthBounds(show: TimelineShow, today: string, lastDone: string | null): MonthBounds {
|
|
54
|
+
const current = monthKeyOf(today);
|
|
55
|
+
if (show === 'all') return { first: null, last: null };
|
|
56
|
+
if (show === 'planned') return { first: current, last: null };
|
|
57
|
+
const done = lastDone ? monthKeyOf(lastDone) : current;
|
|
58
|
+
return { first: null, last: done > current ? done : current };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The nearest month a filter lists. */
|
|
62
|
+
export function clampMonth(key: string, bounds: MonthBounds): string {
|
|
63
|
+
if (bounds.first && key < bounds.first) return bounds.first;
|
|
64
|
+
if (bounds.last && key > bounds.last) return bounds.last;
|
|
65
|
+
return key;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** First and last day of a `yyyy-MM` month. */
|
|
69
|
+
export function monthWindow(key: string): DateWindow {
|
|
70
|
+
const first = parseISO(`${key}-01`);
|
|
71
|
+
return { start: format(first, DAY), end: format(endOfMonth(first), DAY) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------- geometry
|
|
75
|
+
|
|
76
|
+
/** A little air around the reached values, always including the floor. */
|
|
77
|
+
export function paddedDomain(min: number, max: number, floor: number, pad = 0.08): [number, number] {
|
|
78
|
+
const lo = Math.min(min, max, floor);
|
|
79
|
+
const hi = Math.max(min, max, floor);
|
|
80
|
+
const span = hi - lo || Math.max(Math.abs(hi), 1);
|
|
81
|
+
return [lo - span * pad, hi + span * pad];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The scale for balance charts: the reached values, plus the floor only when
|
|
86
|
+
* it is near them, so a distant floor (often 0) never flattens the line.
|
|
87
|
+
*/
|
|
88
|
+
export function outlookDomain(min: number, max: number, floor: number): [number, number] {
|
|
89
|
+
const span = max - min || Math.max(Math.abs(max), 1);
|
|
90
|
+
return paddedDomain(min, max, floor >= min - span * 0.25 ? floor : min);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Round tick values inside a domain: steps of 1, 2 or 5 times a power of ten. */
|
|
94
|
+
export function niceTicks(lo: number, hi: number, count: number): number[] {
|
|
95
|
+
const span = hi - lo;
|
|
96
|
+
if (!(span > 0) || count < 1) return [];
|
|
97
|
+
const raw = span / count;
|
|
98
|
+
const magnitude = 10 ** Math.floor(Math.log10(raw));
|
|
99
|
+
const normalized = raw / magnitude;
|
|
100
|
+
const step = (normalized < 1.5 ? 1 : normalized < 3 ? 2 : normalized < 7 ? 5 : 10) * magnitude;
|
|
101
|
+
const ticks: number[] = [];
|
|
102
|
+
for (let value = Math.ceil(lo / step) * step; value <= hi + step * 1e-9; value += step) {
|
|
103
|
+
ticks.push(Math.round(value / step) * step);
|
|
104
|
+
}
|
|
105
|
+
return ticks;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `184k`, `−12k`, `950` — compact axis labels in the app locale. */
|
|
109
|
+
export function axisLabel(value: number): string {
|
|
110
|
+
const magnitude = Math.abs(value);
|
|
111
|
+
const text = magnitude >= 1000 ? `${formatNumber(Math.round(magnitude / 1000))}k` : formatNumber(Math.round(magnitude));
|
|
112
|
+
return value < 0 ? `${MINUS}${text}` : text;
|
|
113
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
import { appClock } from '@hearthlog/sdk/runtime';
|
|
5
|
+
import { DataPlaceholder } from "@hearthlog/sdk/ui";
|
|
6
|
+
let { data } = $props();
|
|
7
|
+
$effect(() => {
|
|
8
|
+
let active = true;
|
|
9
|
+
const search = page.url.search;
|
|
10
|
+
void data.workspace.ready.then(() => {
|
|
11
|
+
if (!active) return;
|
|
12
|
+
const now = appClock.now();
|
|
13
|
+
return goto(`/finance/${now.getFullYear()}/${now.getMonth() + 1}${search}`, { replace: true });
|
|
14
|
+
}).catch(() => undefined);
|
|
15
|
+
return () => { active = false; };
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
<svelte:head><title>Finance</title></svelte:head>
|
|
19
|
+
<div class="p-4"><h1 class="text-2xl font-semibold">Finance</h1><DataPlaceholder label="Opening the selected month" /></div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { redirect } from '@sveltejs/kit';
|
|
2
|
+
import type { ModuleLoadEvent } from '@hearthlog/sdk/runtime';
|
|
3
|
+
|
|
4
|
+
export const load = ({ url }: ModuleLoadEvent) => {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
redirect(307, `/finance/${now.getFullYear()}/${now.getMonth() + 1}${url.search}`);
|
|
7
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ModuleLoadEvent } from '@hearthlog/sdk/runtime';
|
|
2
|
+
import { queryDatabase } from '@hearthlog/sdk/data';
|
|
3
|
+
|
|
4
|
+
export const load = ({ depends, parent }: ModuleLoadEvent) => {
|
|
5
|
+
depends('app:assets');
|
|
6
|
+
const assetsData = parent().then(async ({ workspace }) => {
|
|
7
|
+
await workspace.ready;
|
|
8
|
+
return queryDatabase(async (repositories) => {
|
|
9
|
+
const [assets, assetsSum] = await Promise.all([
|
|
10
|
+
repositories.assets.getAssetsByType('asset'),
|
|
11
|
+
repositories.assets.getSumOfAssetsByType('asset'),
|
|
12
|
+
]);
|
|
13
|
+
return { assets, assetsSum };
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
void assetsData.catch(() => undefined);
|
|
17
|
+
return { assetsData };
|
|
18
|
+
};
|