Merge feat/currency into main - #1
Merged
Merged
Conversation
## Entity: @entities/currency
Introduced a first-class `Currency` type with fields `id`, `code`,
`name`, `symbol`, `decimalDigits`, `isDefault`, `isEnabled`. Added
`getDefaultCurrencies()` — a seed list of 10 currencies (UAH, USD,
EUR, RUB, PLN, GBP, CHF, CAD, TRY, JPY) used only for initial sheet
population and as an offline fallback. The sheet remains the source of
truth; adding a new currency there requires no code change.
## Feature: @features/manage-currencies
New feature following the same pattern as manage-categories / manage-containers:
- `currency-row.mapper.ts` — `mapCurrencyToRow` / `mapRowToCurrency`
with tolerant parsing: blank `decimalDigits` defaults to 2, booleans
are case-insensitive (`true` / `TRUE` / `1`), missing name/symbol
fall back to the code itself.
- `manage-currencies.service.ts` — `readCurrencies()` reads the
Currencies sheet when Google is connected; falls back to defaults
when the sheet is missing or the user is offline.
- `useCurrencies()` — React hook that seeds state with defaults
immediately so the UI is never empty on first render.
- `CurrencySelect` — replaces every free-text currency `<Input>` with
a `<select>` populated from the sheet. Only `isEnabled` currencies
appear. If an older transaction references a currency that is no
longer enabled, it is kept as an option so editing never silently
rewrites the value.
## sheet.constants.ts + spreadsheet template (templateVersion 2 → 3)
- Added `sheetNames.currencies = 'Currencies'` and corresponding
`currencyHeaders`, `sheetRanges.currencies*` entries.
- Added `'Currencies'` to `requiredSheetNames`.
- `buildTemplateValueRanges()` signature changed from a boolean flag
to an options object `{ includeDefaultCategories, includeDefaultCurrencies }`
so both seeds are controlled independently.
- Setup service runs two `readRange` checks in parallel; seeds the
Currencies sheet only when it is empty, so re-running setup never
clobbers manually added currencies.
- `spreadsheet-template-formatting.ts` — added tab color, header
style, column widths, and integer number format for the new sheet.
- `templateVersion` bumped to `'3'` so existing users' cached
"template ready" state invalidates and they re-run setup to gain
the Currencies sheet.
## CurrencyCode type (TASK-4)
`CurrencyCode` widened from `'UAH' | 'USD' | 'EUR'` to `string`.
All consumers (`Container`, `generateContainerId`, `createContainer`,
`findContainerDuplicate`, `useContainers`) continue to compile without
changes. Removed now-pointless `as CurrencyCode` casts in
`CreateTransactionModal` and `container-row.mapper`. Replaced the
free-text currency `<Input>` in `ManageContainersModal` with
`<CurrencySelect>` for consistency.
## calculateTransactionSummary (TASK-5)
Rewrote to return `CurrencySummary[]` grouped by currency code instead
of a single `TransactionSummary` object. Uses a `Map<string, CurrencySummary>`
so amounts of different currencies are never added together.
100 USD + 100 UAH now correctly produces two separate entries.
Insertion order of first appearance is preserved.
## DashboardPage (TASK-6)
Removed `activeTransactions[0]?.currency` fallback that labelled a
mixed-currency sum with the first transaction's currency. The three
summary cards (Income / Expense / Balance) now render a `<ul>` with
one `<li>` per currency. When no transactions exist, shows `—` instead
of a potentially misleading zero with a guessed currency.
## Transaction currency filter (TASK-7)
- Added `currency: string` to `TransactionFilters` (default `''` = all).
- `filterTransactions()` skips on `transaction.currency !== filters.currency`
when the filter is set.
- Added `getTransactionCurrencyOptions()` — deduped, sorted list of
currency codes present in the transaction list.
- `TransactionsHistory` exposes a "Currency" `FilterSelect` in the
advanced filters panel; options are derived from actual transactions,
with "All currencies" as the default. The `hasActiveFilters` check
includes the new field so the "Clear" button activates correctly.
…solation tests TASK-8: Fix analytics charts to respect currency - Added active currency picker to DashboardAnalytics; tabs appear only when multiple currencies exist - Filtered transactions per active currency before computing all chart data - Added forceLocal flag to readAnalyticsStats / useAnalyticsStats to bypass Google Sheets pre-aggregated data when multi-currency is active (pre-aggregated rows lack currency) TASK-9: Update Google Sheets formulas for currency columns - Added currency column to MonthlyStats (month, currency, income, expense, balance) - Added currency column to CategoryStats (category, kind, currency, total, count) - Added summaryByCurrencyFormula at Summary!A8 - QUERY groups totals per currency - Updated analytics row mapper to read 5-column rows - Updated MonthlyStats and CategoryStats types to include currency field - Bumped templateVersion to 4 to force re-setup for existing users TASK-10: Scope offline cache keys to spreadsheetId - Replaced offlineTransactionsStorage singleton with createOfflineTransactionsStorage(spreadsheetId) factory - IDB keys now use spreadsheet:<id>:transactionsCache / pendingTransactionsQueue / failedSyncQueue / lastSuccessfulSyncAt - Updated createLocalQueueDataSource to accept spreadsheetId and build scoped storage - Updated useTransactions, useOfflineSyncStatus, and SettingsPage to use scoped instances - Spreadsheet A cache can never bleed into Spreadsheet B TASK-11: Clear UI state and warn on spreadsheet switch - Added prevSpreadsheetId ref in useTransactions; detects when spreadsheetId changes - On switch: immediately clears transactions state and resets errors - Checks old spreadsheet pending queue; sets syncWarning if unsynced changes remain TASK-12: Currency correctness tests - calculate-monthly-stats: 100 UAH + 50 UAH = 150 UAH; UAH and USD stay separate - calculate-category-stats: same grouping invariants per category + currency - transaction-summary: groups totals by currency, never mixes them - analytics-stats.service: local analytics emits separate monthlyStats and categoryStats per currency - transaction-filters: currency filter shows only matching transactions, composes with other filters - transaction-row.mapper: legacy rows without source/syncStatus still parse; non-UAH currencies read correctly TASK-13: Cache isolation tests - local-queue-data-source: transactions created in sheet-a are absent from sheet-b - local-queue-data-source: pending queue of sheet-a is not synced when using sheet-b data source - local-queue-data-source: switching to a new spreadsheet sees empty cache even when old one has data
…icker ## Bug fix: undefined valueRange.values crash Google Sheets API omits the values field entirely when a range is empty, causing a crash (Cannot read properties of undefined reading map) in every service that called .map() directly on the response. Fixed with a ?? [] fallback in all affected services: - manage-transactions.service.ts (3 call sites: getTransactions, findTransactionRowIndex, softDeleteTransaction) - manage-categories.service.ts - manage-containers.service.ts - manage-currencies.service.ts - analytics-stats.service.ts (monthlyStats + categoryStats) ## Empty state: added Refresh button When the transactions list is empty, EmptyState now shows two buttons: Create transaction (primary) and Refresh (secondary). EmptyState component extended with optional secondActionLabel / onSecondAction props. ## New shared DatePicker component Replaced native <input type="date"> (browser-rendered, unstyled) with a fully custom DatePicker in shared/ui/date-picker/: - Trigger button styled consistently with Input (h-10, brand focus ring) - Calendar icon (SVG) instead of the raw glyph - SVG chevrons for month navigation - Today highlighted with a brand-color border (no fill) - Selected day has brand-color background - Closes on Escape key and outside pointer click - Supports label, error, hint props matching the Input API Replaced usages: - CreateTransactionModal: native Input type=date replaced with DatePicker - TransactionsHistory: local DateFilterField component removed entirely, both filter date fields now use the shared DatePicker
Rewrote shared/ui/select/Select.tsx from a native <select> wrapper to a fully custom dropdown that matches the design system: - Trigger button styled consistently with Input and DatePicker (h-10, border, brand focus ring, hover:border-zinc-300) - SVG chevron that rotates 180 deg when the dropdown is open - Dropdown popup: rounded-xl, shadow-xl, p-1, max-h-64 with scroll - Each option row: rounded-lg, 12px padding, hover:bg-zinc-50 - Selected option: bg-indigo-50, text-brand, font-medium + checkmark icon - Unselected option has an invisible checkmark placeholder so text stays aligned - Closes on Escape and outside pointer click - API changed from children (<option>) to options prop for clean composition - Supports label, hint, error, disabled props Updated all usages: - CreateTransactionModal: Type select (expense/income) migrated to options prop - SettingsPage: Language and Theme selects migrated to options prop - CurrencySelect: now passes options array instead of rendering <option> children - TransactionsHistory: local FilterSelect component removed entirely (5 instances), all filter dropdowns (Type, Category, Currency, Container, Sync status) now use the shared Select component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Issues
No issues.
Key changes
Validation
Notes