Skip to content

Merge feat/currency into main - #1

Merged
Ermolz69 merged 5 commits into
mainfrom
feat/currency
Jun 24, 2026
Merged

Ermolz69 merged 5 commits into
mainfrom
feat/currency

Conversation

@Ermolz69

Copy link
Copy Markdown
Contributor

Summary

  • Adds currency-aware transaction handling across analysis, filtering, setup, and management flows.
  • Updates app settings, shared currency types, and UI selectors to work with explicit currency identifiers.
  • Keeps dashboard, settings, and offline transaction paths aligned with the new currency model.

Issues

No issues.

Key changes

  • Introduced shared currency types and default currency helpers.
  • Updated analytics, transaction filtering, and summary calculations to operate with currency-aware data.
  • Adjusted create transaction, manage currencies, and manage transactions services and UI components.
  • Updated spreadsheet setup, storage keys, and shared UI primitives where currency-related behavior changed.
  • Added and updated unit tests for mappers, services, filters, and spreadsheet template logic.

Validation

  • Unit and integration-level tests were updated for the affected currency, analytics, and transaction flows.
  • Frontend checks were not run separately; changes were validated through the affected component and service tests.
  • Manual UI testing was not run in this pass.

Notes

  • The main review focus should be currency propagation through transaction creation, analytics aggregation, and offline sync paths.
  • No additional follow-up items are known at this stage.

Ermolz69 added 5 commits June 24, 2026 17:35
## 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
@Ermolz69
Ermolz69 merged commit 3eca5bc into main Jun 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant