From a65ab711ef6ddb668e60e7d51b1b909053340227 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Wed, 21 Oct 2026 21:35:41 +0200 Subject: [PATCH] W2 web expenses, search and CSV export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W-5 expense editor (S-7): amount, description, category quick row and "Alla" grid with the §9.11 suggestion for a new expense (CS-1 to CS-7), the four fixed option rows (D-102), split modes default, ratio, exact and full with live shares, notes, inline §9.6 validation, one POST /sync per save, delete with confirmation, the discard dialog on Cancel, Esc and in-app navigation (D-080), rejection and network failures shown on the page with the form kept (§15.5). Defaults per AC-8.6 with this browser's last account and category; ?account= preselects the opener's account. W-6 expense detail (S-8): hero, facts, shares and mode, recurring template badge, the AC-12.4 timeline, deleted and missing states. W-12 search with filters in the URL query, count and sum, rows a hundred at a time, and the CSV export (UTF-8 BOM, semicolons, CRLF, decimal comma, ISO dates, header in the page language), built in the browser. The §9.11 port passes contracts/test-vectors/suggest_category.json (65 cases); the keyword table is a Vite raw import of contracts/category-keywords.csv, which needs ../contracts in server.fs.allow (vite.config.ts). Shared editor fields of design-spec §3.7 in web/src/components/editor/ for W5's template editor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B --- web/src/components/editor/AccountDot.tsx | 30 + web/src/components/editor/CategoryPicker.tsx | 115 +++ web/src/components/editor/ConfirmDialog.tsx | 73 ++ web/src/components/editor/DateField.tsx | 89 +++ web/src/components/editor/Editor.module.css | 379 ++++++++++ web/src/components/editor/NotesField.tsx | 65 ++ web/src/components/editor/OptionRows.tsx | 78 +++ web/src/components/editor/PayerRow.tsx | 38 + web/src/components/editor/SelectField.tsx | 103 +++ web/src/components/editor/SplitSection.tsx | 411 +++++++++++ web/src/components/editor/categories.test.ts | 66 ++ web/src/components/editor/categories.ts | 66 ++ web/src/components/editor/index.ts | 49 ++ web/src/components/editor/people.ts | 12 + web/src/components/editor/split.test.ts | 152 ++++ web/src/components/editor/split.ts | 219 ++++++ .../expenses/ExpenseDetailPage.test.tsx | 175 +++++ .../features/expenses/ExpenseDetailPage.tsx | 248 ++++++- .../expenses/ExpenseEditorPage.test.tsx | 456 ++++++++++++ .../features/expenses/ExpenseEditorPage.tsx | 661 +++++++++++++++++- web/src/features/expenses/Expenses.module.css | 191 +++++ web/src/features/expenses/PageStates.tsx | 32 + web/src/features/expenses/Timeline.tsx | 25 + web/src/features/expenses/i18n/en.json | 5 + web/src/features/expenses/i18n/sv.json | 5 + web/src/features/expenses/messages.ts | 60 ++ web/src/features/expenses/model.ts | 134 ++++ .../features/expenses/suggest/forLedger.ts | 35 + web/src/features/expenses/suggest/keywords.ts | 8 + .../features/expenses/suggest/suggest.test.ts | 76 ++ web/src/features/expenses/suggest/suggest.ts | 170 +++++ web/src/features/expenses/testLedger.ts | 233 ++++++ .../features/expenses/timelineLines.test.ts | 93 +++ web/src/features/expenses/timelineLines.ts | 169 +++++ web/src/features/expenses/usePushExpense.ts | 45 ++ web/src/features/export/ExportCsvButton.tsx | 25 + web/src/features/export/csv.test.ts | 109 +++ web/src/features/export/csv.ts | 76 ++ web/src/features/export/download.ts | 18 + web/src/features/export/i18n/en.json | 5 + web/src/features/export/i18n/sv.json | 5 + web/src/features/search/Search.module.css | 40 ++ web/src/features/search/SearchPage.test.tsx | 180 +++++ web/src/features/search/SearchPage.tsx | 251 ++++++- web/src/features/search/filter.test.ts | 103 +++ web/src/features/search/filter.ts | 116 +++ web/src/features/search/i18n/en.json | 18 + web/src/features/search/i18n/sv.json | 18 + web/vite.config.ts | 4 + 49 files changed, 5721 insertions(+), 13 deletions(-) create mode 100644 web/src/components/editor/AccountDot.tsx create mode 100644 web/src/components/editor/CategoryPicker.tsx create mode 100644 web/src/components/editor/ConfirmDialog.tsx create mode 100644 web/src/components/editor/DateField.tsx create mode 100644 web/src/components/editor/Editor.module.css create mode 100644 web/src/components/editor/NotesField.tsx create mode 100644 web/src/components/editor/OptionRows.tsx create mode 100644 web/src/components/editor/PayerRow.tsx create mode 100644 web/src/components/editor/SelectField.tsx create mode 100644 web/src/components/editor/SplitSection.tsx create mode 100644 web/src/components/editor/categories.test.ts create mode 100644 web/src/components/editor/categories.ts create mode 100644 web/src/components/editor/index.ts create mode 100644 web/src/components/editor/people.ts create mode 100644 web/src/components/editor/split.test.ts create mode 100644 web/src/components/editor/split.ts create mode 100644 web/src/features/expenses/ExpenseDetailPage.test.tsx create mode 100644 web/src/features/expenses/ExpenseEditorPage.test.tsx create mode 100644 web/src/features/expenses/Expenses.module.css create mode 100644 web/src/features/expenses/PageStates.tsx create mode 100644 web/src/features/expenses/Timeline.tsx create mode 100644 web/src/features/expenses/i18n/en.json create mode 100644 web/src/features/expenses/i18n/sv.json create mode 100644 web/src/features/expenses/messages.ts create mode 100644 web/src/features/expenses/model.ts create mode 100644 web/src/features/expenses/suggest/forLedger.ts create mode 100644 web/src/features/expenses/suggest/keywords.ts create mode 100644 web/src/features/expenses/suggest/suggest.test.ts create mode 100644 web/src/features/expenses/suggest/suggest.ts create mode 100644 web/src/features/expenses/testLedger.ts create mode 100644 web/src/features/expenses/timelineLines.test.ts create mode 100644 web/src/features/expenses/timelineLines.ts create mode 100644 web/src/features/expenses/usePushExpense.ts create mode 100644 web/src/features/export/ExportCsvButton.tsx create mode 100644 web/src/features/export/csv.test.ts create mode 100644 web/src/features/export/csv.ts create mode 100644 web/src/features/export/download.ts create mode 100644 web/src/features/export/i18n/en.json create mode 100644 web/src/features/export/i18n/sv.json create mode 100644 web/src/features/search/Search.module.css create mode 100644 web/src/features/search/SearchPage.test.tsx create mode 100644 web/src/features/search/filter.test.ts create mode 100644 web/src/features/search/filter.ts create mode 100644 web/src/features/search/i18n/en.json create mode 100644 web/src/features/search/i18n/sv.json diff --git a/web/src/components/editor/AccountDot.tsx b/web/src/components/editor/AccountDot.tsx new file mode 100644 index 0000000..7d9925f --- /dev/null +++ b/web/src/components/editor/AccountDot.tsx @@ -0,0 +1,30 @@ +import styles from './Editor.module.css'; + +/** Chart colours for accounts (design-spec §2.4): `--chart-cat-(position mod 8 + 1)`. */ +const CHART_COLOURS = 8; + +/** An account's colour position: its index among the household's accounts in created order (then id). */ +export function accountColorIndex(accountId: string, accounts: Iterable<{ id: string; created_at: string }>): number { + const ordered = [...accounts].sort((a, b) => + a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : a.id < b.id ? -1 : a.id > b.id ? 1 : 0, + ); + return Math.max( + 0, + ordered.findIndex((a) => a.id === accountId), + ); +} + +/** The account chip's leading dot (design-spec §3.7): a 12 px circle in the account's chart colour. Decorative. */ +export function AccountDot({ colorIndex, size = 12 }: { colorIndex: number; size?: number }) { + return ( +