Commit Graph

77 Commits

Author SHA1 Message Date
Víctor Falcón 50d0fccd17
fix(dashboard): render negative net worth as a downward bar (#722)
## Problem

On the dashboard **Net Worth Evolution** monthly bar chart, users with a
large negative net worth (lots of debt) saw an **empty chart** — only
the x-axis month labels rendered, no bars.

Root cause: the chart scales each asset segment so the stacked bar
height equals net worth. For negative net worth the scale factor was
clamped to zero:

```ts
const scaleFactor = hasLiabs && totalAssets > 0
    ? Math.max(0, netWorth / totalAssets) // negative net worth -> 0 -> every bar collapses
    : 1;
```

A stack of positive asset segments simply can't represent a negative
total, so everything collapsed to zero height.

## Fix

- Extract the scaling into a tested pure helper
`computeNetWorthBarScaling`.
- When net worth is **negative**, hide the (meaningless) scaled asset
segments and draw the deficit as a **single downward bar** from a **zero
baseline** (dashed reference line).
- **Positive** periods still stack assets upward. Both directions round
only at their far end and meet the zero line flush, so upward and
downward bars look symmetric.
- Extend the same handling to the **daily area chart**, which shared the
bug through the same scaled dataset.
- Share a single `NetWorthMode` type across the bar/area charts and
tooltip (was duplicated in three places).
- Translate the tooltip **Net Worth / Total** labels (were hardcoded
English; added `Net Worth` to `lang/es.json`).
- Render the chart for **liability-only** users (a debtor with just a
loan) instead of the "No account data available" empty state.

## Tests

- Unit tests for `computeNetWorthBarScaling`: no-liabilities, positive,
negative-with-assets, liabilities-only, net-worth-exactly-zero, and
no-assets cases.
- `bun run test`, `prettier --check`, `eslint` all green on the touched
files.

## QA

Verified in the running app (Playwright) with two seeded users:

- **Heavily indebted** (net worth −56.250,00 €): the chart now renders
red downward bars instead of an empty area.
- **Net worth crossing zero**: negative months draw downward, positive
months stack upward, sharing the zero baseline; tooltip shows
per-account balances + the (now translated) "Patrimonio Neto" total.

## Demo


https://github.com/user-attachments/assets/7a9b167f-c037-4c82-8b68-f32932be99fe

## Screenshots

**Before** — empty chart, only x-axis labels:

<!-- PLACEHOLDER: before screenshot -->
<img width="1440" height="900" alt="net-worth-before"
src="https://github.com/user-attachments/assets/3bf9b3e3-ae75-4230-bca7-e7246a9a35d9"
/>


**After** — negative net worth as downward bars:
<img width="1440" height="900" alt="net-worth-after-tooltip"
src="https://github.com/user-attachments/assets/85302f0a-e427-4cbf-baba-5b1414049e9f"
/>
<img width="1440" height="900" alt="net-worth-after-mixed"
src="https://github.com/user-attachments/assets/41f08331-f055-4245-beeb-8d071c0d5d6d"
/>
<img width="1440" height="900" alt="net-worth-after-negative"
src="https://github.com/user-attachments/assets/c8610b03-dcff-46de-8987-d47c477ddd85"
/>
2026-07-22 09:32:18 +00:00
Víctor Falcón 91ffeb917d
fix(amount-input): allow negative amounts via a sign toggle on iOS (#674)
## Problem

On the create/edit transaction form, the amount field uses
`inputMode="decimal"`. On iOS the numeric keypad that this triggers has
**no minus key**, so users cannot type the leading `-` the input relies
on to record a negative amount (e.g. an outgoing transfer).

## Fix

Add an opt-in `allowNegative` prop to `AmountInput` that renders a small
`+`/`−` sign-toggle button at the **start** of the field. It works with
any keyboard, so it does not depend on the OS keypad. It is enabled only
on the create-transaction amount input; budgets, balances and loan
fields keep their current UI.

- The button turns red with a `−` when the amount is negative, and shows
`+` otherwise (`aria-pressed` reflects the state).
- Toggle fires on `click` (works for both pointer and keyboard);
`onPointerDown` `preventDefault` keeps the input focused to avoid a
blur/reformat race.
- A leading `-` set via the toggle **before** typing is preserved on
focus, so the sign is not silently dropped.
- The currency symbol shifts right when the toggle is present so the two
never overlap; layout is unchanged when `allowNegative` is off.

## Refactor

Extracted the repeated `evaluateMathExpression(x) ?? parseInputValue(x)`
string→cents resolution (previously duplicated across blur, Enter, and
the new toggle) into a single `resolveCents()` helper.

## Tests

`resources/js/components/ui/amount-input.test.tsx` — the toggle is
hidden unless `allowNegative`, flips positive↔negative, and preserves
the sign when focusing after toggling an empty field.

## QA

Manually verified in the running app (create-transaction dialog): typing
an amount and toggling the sign both ways updates the value and the
button state correctly, on both prefix (USD `$`) and suffix (EUR `€`)
currency layouts.
2026-07-14 16:13:39 +00:00
Víctor Falcón 5b3400909e
fix(chart): stop tooltip render loop crashing the dashboard (PHP-LARAVEL-3B) (#657)
## Problem

Sentry **PHP-LARAVEL-3B** — React `Maximum update depth exceeded`
crashing `/dashboard`. Regressed, 5 occurrences / 4 users (mobile
Chrome/Android and Safari), last seen today.

## Root cause

`ChartTooltipPortal` in `resources/js/components/ui/chart.tsx`
positioned the portaled tooltip in a `useLayoutEffect` with **no
dependency array**, so it ran after every render and called `setPos`. An
equality guard was the only thing preventing a `render → effect → setPos
→ render` feedback loop, and it failed once the computed position
oscillated by a sub-pixel (fractional `getBoundingClientRect` / integer
`offsetWidth`). React then aborted after 50 nested updates, taking down
the dashboard.

## Fix

1. **`651c7d72`** — Depend on the primitive `coordinate.x/.y` (and
`offset`) so the effect runs only when the cursor moves, never as a
result of its own `setPos`. `coordinate` itself is excluded on purpose:
Recharts hands a fresh object every render, so depending on the object
would reopen the loop. Extract the flip/clamp math into a pure
`computeTooltipPosition` helper (`resources/js/lib/`) rounded to whole
pixels, and unit-test it.
2. **`1c1602db`** — Component regression test: asserts the positioning
effect does **not** re-run on a re-render with an unchanged coordinate,
and **does** on a coordinate change. Verified it fails against the
pre-fix dependency-less effect.
3. **`0354a031`** — Strengthen the helper test to assert sub-pixel
positions collapse to one pixel, and correct the doc note (the
dependency array removes the loop; rounding only narrows the
oscillation).

## Review

Two independent review agents (architecture/duplication/tests +
user-facing correctness) examined the diff:
- **Correctness**: no must-fix issues. First paint is correct (measured
at `-9999`/opacity 0 before paint, capped by a viewport-relative
`max-w`, so size is accurate and there is no flash). The loop cannot
recur through the `setHidden` effect or a repeated identical coordinate.
No mobile/edge regression — the flip/clamp logic is the extracted
original.
- **Architecture**: helper placement/naming/types match conventions; no
duplicate positioning logic elsewhere to fold in; both agents' top
request (regression-test the actual fix mechanism) is addressed by
commit 2. `ChartTooltipPortal` is exported for the test, matching how
`StackedBarShape` is exported for its own test.

## Known minor residual (acceptable)

If the tooltip's **content size** changes while the cursor stays on the
exact same point *and* it sits at a viewport edge, the flip/clamp won't
recompute until the next mouse move. In practice content size changes
together with the coordinate (Recharts hover). Worst case is a one-frame
cosmetic overflow that self-corrects — strictly better than the crash it
replaces.

## Testing

`chart-tooltip-position.test.ts` (5) + `chart-tooltip-portal.test.tsx`
(2) pass. Note: the infinite loop itself can't be reproduced in jsdom
(zero-size rects), so the component test guards the mechanism (effect
does not self-retrigger) rather than the throw.

Fixes PHP-LARAVEL-3B
2026-07-07 18:39:09 +00:00
Víctor Falcón 884038c13b
feat(transactions): highlight new transactions since last visit (#609)
## What

Between visits, highlight the transactions that arrived since the user
last opened the list.

- The newest `created_at` from the initial page load is stored in
`localStorage` (`transactions-last-visit`), frozen at mount.
- Every transaction whose `created_at` is newer than that marker gets a
subtle accent on the row (left border + faint tint).

## Why per-row instead of a single divider line

The first cut drew one "Last visit" divider with new rows above it. A
code review found this is structurally wrong: the list is sorted by
`transaction_date` (when the money moved), but "new" is defined by
`created_at` (when the row was inserted). The two don't correlate — a
bank sync that imports a batch dated across several days scatters the
new rows throughout the list, so a single boundary lands at the first
older row and **hides every new row below it**, the exact miss the
feature was meant to prevent.

Per-row marking is correct regardless of sort order: no new transaction
can be hidden.

## Notes / limitations

- The marker is written **once at mount** from the initial payload. Rows
that arrive mid-visit (load-more, a sync/refresh, AI categorization) do
not advance it, so they are never recorded as "seen" before the user has
seen them. The trade-off is they may re-appear as new next visit (errs
on showing, never hiding).
- Purely client-side (localStorage), so the marker is per-device.

## Tests

- Unit tests for `isNewSince` and `newestCreatedAt`.
- `LocalizationTest`, ESLint, Prettier all pass.
2026-06-29 16:08:14 +00:00
Víctor Falcón 9a458b1031
feat(ai): manage AI consent outside onboarding with live backfill (#591)
## Summary

Lets existing (non-onboarding) users discover and manage AI
categorization, gated behind a new Pennant feature flag. When a flagged
paid user grants consent, every uncategorized transaction is categorized
in the background with live, on-screen feedback.

Enable per user: `php artisan feature:enable AiConsentSettings
user@example.com`

## What's included

**Feature flag**
- New `AiConsentSettings` Pennant flag (off by default), exposed to the
frontend via the shared `features` prop.

**Billing settings (`/settings/billing`)**
- When the flag is active, a section to grant or revoke AI consent
(checkbox → `POST`/`DELETE /ai/consent`).

**Transactions page**
- A consent prompt rendered as the first row of the transactions table
(new `DataTable` `topRow` slot), styled with the shared gradient
`AiSparkleIcon`. Only shown to paid users with the flag who haven't
consented yet.
- An inline "Enable AI" button records consent without leaving the page.

**Backfill on consent**
- Granting consent dispatches a queued
`CategorizeUncategorizedTransactionsJob` that categorizes all of the
user's uncategorized transactions, **most recent first**, recording
progress in the cache.
- The transactions page polls a new status endpoint (`GET
/ai/categorization/{jobId}/status`): visible uncategorized rows show a
spinner + pulse while the backfill runs, categories stream in via
partial reloads, and a toast tracks `processed / total` until
completion.

## Notes / decisions

- **Polling, not websockets** — the project has no broadcasting
configured; this mirrors the existing `ReEvaluateTransactionRules` job +
status-endpoint pattern.
- The backfill job is kept separate from
`CategorizeOnboardingTransactionsJob` so the onboarding pass stays
progress-free (small, commented duplication).
- Reuses the existing `AiCategorizationGate` (config kill-switch + pro
plan + active consent) — free users and disabled-AI never dispatch the
job.
- Not included (YAGNI): cancelling an in-flight backfill on revoke;
persisting the spinner across full page reloads.

## Testing

- New Pest coverage: feature-flag exposure, billing/transactions consent
props, consent-triggered dispatch + gating, the status endpoint
(200/404), job progress recording, and recent-first ordering.
- `php artisan test --exclude-testsuite=Browser` passes (one unrelated,
pre-existing `DashboardTest` failure on `main`).
- Pint, Prettier and ESLint clean.
2026-06-25 10:50:35 +02:00
Víctor Falcón 7fb190683c
refactor(encryption): strip client-side transaction encryption (#514)
## Why

Transactions and account names used to be stored encrypted client-side.
They now live **unencrypted in the backend**, so the browser no longer
needs to encrypt, decrypt, or mask them. A handful of legacy accounts
may still have encrypted transactions, but the auto-run
decrypt-migration handles those — so this PR strips the now-dead
encryption machinery while keeping the migration path fully intact.

Net: **−1213 / +176** lines across 25 files.

## Removed (dead machinery)

- `setup-encryption` page, `POST /api/encryption/setup`,
`SetupEncryptionRequest`, and `encrypt()` / `generateSalt()` — new data
is never encrypted again
- `EncryptedText` and the decrypt-on-render description component →
replaced by a plaintext `TransactionDescription` that keeps the
privacy-mode fake labels
- All `isKeySet`-gated client decryption: transaction list (load /
search / sort), categorize flow, import dedup
- `isKeySet` masking / locking on amount display, app logo, import
button, add-transaction button, account page, and filters
- The unused `?encrypted=false` API filter and `isKeyPersistent()`

## Kept (legacy decrypt-migration)

- The two migration hooks, the unlock dialog, and the dashboard/login
unlock prompt
- `decrypt` / `importKey` + key derivation, key-storage,
`EncryptedMessage`, salt, `GET /api/encryption/message`,
`?encrypted=true`, and the middleware salt-cleanup
- `rule-engine` / edit / import-drawer account-name decryption —
plaintext-first (`if (!account.encrypted) return name`) legacy fallback
on the kept primitives; intentionally untouched

## Testing

- Pint  · ESLint  · Prettier 
- 184 vitest tests 
- 1425 PHP Feature/Unit tests  (Browser suite not run locally — needs
`bun run build`; no Browser test references the encryption UI)

## Follow-up (out of scope)

`DecryptedTransaction.decryptedDescription` / `decryptedNotes` are now
just plaintext aliases. Collapsing that type touches 13+ files (rule
engine, categorizer, edit dialog) and was left as a separate cleanup.
2026-06-20 16:13:26 +00:00
Víctor Falcón 7dde67c606
feat(ai): share AI sparkle icon and mark AI-generated rules (#538)
## What

- **Extract the AI sparkle into a shared component.** Moves the
Gemini-style gradient sparkle from
`components/transactions/ai-sparkle-icon` →
`components/ui/ai-sparkle-icon`. It's now the single icon to reuse
wherever a feature involves AI. Existing transaction-list usages
(`category-cell`, `transaction-filters`) point at the new path; no
visual change there.
- **Mark AI-generated automation rules.** In the Automation Rules table,
the rule **title** is now prefixed with the sparkle (tooltip +
`aria-label "Created by AI"`) when the rule's `origin` is `ai`.
Extracted into a small `AutomationRuleTitle` component.

The backend `origin` enum (`RuleOrigin`: user/ai) already shipped to
`main`; this PR is the frontend surface plus the `origin` field on the
TS `AutomationRule` type.

## Tests

- New `automation-rule-title.test.tsx`: sparkle shown for `origin:
'ai'`, absent for `'user'`.
- `bun run format`, `bun run lint` clean; vitest green.

## Note

Per review, the marker sits before the rule **name**, not next to the
category badge.
2026-06-15 18:22:11 +02:00
Víctor Falcón 58254fcede
feat(auth): show/hide toggle on password fields (#499)
## What

Add an eye icon to every password field that toggles between masked and
plaintext, so users can verify what they typed.

New reusable `PasswordInput` component
(`components/ui/password-input.tsx`) wrapping the base `Input` with an
`Eye`/`EyeOff` toggle.

## Where

- **Auth**: login, register (password + confirm), reset password
(password + confirm)
- **Settings**: password form & account form (current + new + confirm)
- **Dialogs**: delete-account confirmation, encryption unlock dialog

## Details

- Toggle is keyboard-skipped (`tabIndex={-1}`), mirrors the input's
`disabled` state.
- Accessible: `aria-pressed` + `aria-label` (`Show password` / `Hide
password`), translated and added to `lang/es.json`.
- Dark-mode aware via existing `text-muted-foreground` /
`hover:text-foreground` tokens.

## Tests

- New vitest suite for `PasswordInput` (mask default, toggle, ref
forwarding, disabled state) — 4 passing.
- `LocalizationTest` passing (new translation keys present).
- eslint + prettier clean.

## Not included

Left out (can add on request): `confirm-password.tsx`,
`setup-encryption.tsx`, and the open-banking API key/secret fields.
2026-06-06 11:42:20 +02:00
Víctor Falcón 53d051800b
feat: expand parent categories inline in breakdowns (#486)
## What

Expand/collapse child categories inline in three places:
- Dashboard → **Top spending categories**
- Cashflow → **Income sources**
- Cashflow → **Expense categories**

## How it looks

<img width="1225" height="928" alt="image"
src="https://github.com/user-attachments/assets/aadce904-bfdd-46eb-b919-8695577845d7"
/>

## Implementation

**Frontend**
- `useExpandableCategories` hook: tracks expanded set, lazy-fetches
children once, caches, resets on period change.
- `AnimatedCollapse` component for the height animation.
- Recursive rows in `BreakdownCard` and `TopCategoriesCard`.

**Backend**
- Extracted `rollUpByTree`/`displayNodeFor` out of
`CashflowAnalyticsController` into `CategoryTree::rollUp` (now shared by
cashflow + dashboard).
- Dashboard `top-categories` emits `has_children`/`is_direct` and
accepts `?parent` to drill into a category's children (children + a
direct "Parent" node), mirroring the existing cashflow breakdown drill.
- Added Spanish translations for the new toggle labels.

## Tests
- Backend: `has_children` true/false + parent-drill split (children +
direct node) for dashboard top categories.
- Component: chevron expands, lazily fetches `parent=…`, renders the
child, flips to "Hide subcategories".
- Full suite green; pint / eslint / prettier clean.

> Note: children fetch lazily on first expand (same pattern as the
Sankey inline expand).
2026-06-04 11:19:21 +02:00
Víctor Falcón 1cc10566a3
feat: parent/child category tree (#474)
## Summary

Adds nested categories (parent → child, up to **3 levels**) across the
app. Children inherit their parent's type and cashflow direction, and
every category selector now renders the hierarchy as an indented tree.
Gated behind the `CategoryTree` Pennant flag (off by default).

## Backend

- **Migration**: nullable self-referencing `parent_id`; uniqueness
scoped per-parent via a `parent_unique_marker` virtual column (root
names stay unique). New composite unique created before dropping the old
one so the `user_id` FK keeps a supporting index.
- **`CategoryTree` service**: descendant/ancestor resolution, depth &
cycle checks, type cascade, subtree deletion.
- **Validation**: depth limit, cycle prevention, inherited + locked
child type.
- **Delete strategies**: reparent (default), promote to root, or cascade
(uncategorizes affected transactions).
- **Transaction filter** expands a selected parent to its descendants.
- **Cashflow Sankey & breakdown** roll up to top-level parents with
click-to-drill (children + a parent "direct" node).
- **Budgets** tracking a parent also count their children's
transactions.
- **Unified** the frontend category query behind
`Category::forDisplay()` / `FRONTEND_COLUMNS` (8 call sites) so every
selector receives the full Category shape, including `parent_id`.

## Frontend

- New `category-tree.ts` helpers (build/flatten/descendants/path,
tree-aware selection toggle + tri-state).
- **Settings page**: indented tree, sortable by name/color/type
(siblings sorted, hierarchy preserved), parent picker, delete-strategy
dialog.
- **Combobox** (transaction table cell + edit/create modal + parent
picker): indented tree, search keeps matches with their ancestors.
- **Transaction filter**: indented tree, tri-state cascading selection
(parent ↔ children), themed checkboxes, selected branches float to top
on open, ancestor-aware search.
- **Categorizer palette** and **budget multi-select**: same indented
tree + ancestor-aware search.
- **Sankey**: click a parent node to drill into its children, with
breadcrumb.
- Pennant `CategoryTree` flag gates the parent UI.

## Tests

- Pest: model/validation, delete strategies, filter expansion, cashflow
rollup/drill, budget child inclusion.
- Vitest: tree-aware selection logic.
- All green; Pint / ESLint / Prettier clean.

## Rollout

Flag is off by default — enable per user with:
\`\`\`
php artisan feature:enable "App\\Features\\CategoryTree" you@example.com
\`\`\`
2026-06-03 19:30:12 +02:00
Víctor Falcón 71dd6e2b7f
feat(budgets): track multiple categories and labels per budget (#466)
## Summary

Budgets previously tracked a single category **or** label (mutually
exclusive). This lets a budget span **multiple** categories and labels
at once, all pooling spend against one allocated amount per period.

Scope decided with the requester:
- **Shared pool** — one allocated amount; any tracked category/label
counts against it.
- **Multi categories + multi labels** on a single budget.
- **Create-only** — tracking is chosen at creation and locked afterward
(edit dialog shows it read-only).

## Changes

**Backend**
- New `budget_category` + `budget_label` pivot tables; data migration
copies existing `category_id`/`label_id` into them, then drops those
columns.
- `Budget` model: `categories()` / `labels()` belongsToMany.
- `BudgetTransactionService` matches transactions across **all** tracked
categories OR labels (live assignment + historical backfill).
- `StoreBudgetRequest` accepts `category_ids` / `label_ids` arrays,
requires ≥1 across both, validates ownership. `update` no longer touches
tracking.

**Frontend**
- Reusable `MultiSelect` (popover + command + badge).
- Create dialog uses multi-selects; cards and show page render tracked
categories/labels as badges; edit dialog shows them read-only.
- Dexie bumped to v10 (drops unused per-category allocations table, adds
`budget_labels`).

## Testing
- Updated all budget/transaction/listener/browser tests for the pivot
model; added cases for multi-category matching, mixed category+label
pooling, and store validation (empty selection + foreign-ownership).
- Added the new `__()` strings to `lang/es.json`.
- Local `pint`, `lint`, `format` pass. Relying on CI for the full suite.
2026-06-01 12:32:23 +02:00
Víctor Falcón d5d262e9fd
fix(chart): mask stacked bar edges (#439)
## Summary
- mask stacked bar rounded edges with the card background
- clamp bar radius for tiny stacked segments
- add regression tests for shape stroke and radius

## Tests
- npx vitest run resources/js/components/ui/stacked-bar-chart.test.tsx
--reporter=verbose
- npx eslint resources/js/components/ui/stacked-bar-chart.tsx
resources/js/components/ui/stacked-bar-chart.test.tsx
2026-05-26 17:03:06 +02:00
Víctor Falcón 534a14790e
feat(accounts): add transaction action (#438)
## Summary
- add Add transaction action to disconnected transactional account
detail pages
- keep balance actions grouped as Update balance | Import balances |
menu
- center empty data-table messages vertically

## Tests
- php artisan test --compact tests/Feature/LocalizationTest.php
tests/Feature/AccountControllerTest.php
- npm test -- resources/js/pages/Accounts/Show.test.tsx
- npx eslint resources/js/pages/Accounts/Show.tsx
resources/js/pages/Accounts/Show.test.tsx
resources/js/components/ui/data-table.tsx

Note: npm run types still fails due existing unrelated TypeScript
errors.
2026-05-26 15:57:23 +02:00
Víctor Falcón 38e1976270
fix(chart): hide tooltip on scroll with opacity fade (#320)
## Summary

Follow-up to #317. Portaled tooltip uses `position: fixed`; on scroll it
stayed anchored to stale viewport coords.

- Listen to `scroll` (capture) and `resize`, flip a `hidden` flag to
hide the tooltip.
- Reset flag whenever Recharts reports a new `coordinate` (pointer back
on a bar).
- Replace `visibility` binary toggle with `opacity` + 120ms transition
so the whole tooltip (bg, border, text) fades as one layer instead of
revealing staggered unmount order.

## Test plan
- Hover a bar → tooltip fades in at cursor.
- Scroll the page while hovering → tooltip fades out smoothly, whole
element at once.
- Hover another bar after scrolling → tooltip reappears.
2026-04-24 08:28:14 +01:00
Víctor Falcón e4d2ade92f
fix(chart): tooltip escapes overflow, truncates long labels (#317)
## Summary

- Portal chart tooltip to `document.body` to escape ancestor
`overflow-hidden` (CardContent, scroll container). Positioned via
Recharts' `coordinate` prop + `.recharts-wrapper` viewport rect, with
viewport-edge flipping.
- Cap tooltip width and truncate long account/liability names with
ellipsis; values stay nowrap.
- Force `grid-cols-[minmax(0,1fr)]` on tooltip grid tracks so `truncate`
actually engages (grid items default to min-width auto).

## Test plan
- Hover net-worth chart: tooltip no longer clipped by container.
- Position follows cursor; flips near viewport edges.
- Long liability/account names show ellipsis within tooltip bounds.
2026-04-23 08:17:38 +01:00
Víctor Falcón ea9956f21d
fix: keep iOS popovers below the notch (#282)
## Summary
- add safe-area CSS variables for the viewport top and bottom insets
- apply safe-area-aware collision padding to the shared popover
primitive so iOS overlays stay below the notch
- add a focused regression test for the popover safe-area guard

## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/PopoverSafeAreaTest.php
2026-04-13 15:19:56 +01:00
Víctor Falcón 0735ee6d69
fix: preserve cents in chart amounts (#270)
## Summary
- keep two decimal places in net worth and account chart amounts instead
of rounding to whole units
- update shared chart tooltip, trend tooltip, and MoM tooltip currency
formatting so related chart values stay consistent
- add targeted formatter coverage for chart-style balances with decimals

## Testing
- npm test -- resources/js/utils/currency.test.ts
2026-04-10 15:41:24 +02:00
Víctor Falcón 755452d6ce
Fix net worth chart config UX and color scheme issues (#250)
## Summary

- **Clickable toggle rows**: The full row in the chart settings popover
now toggles the checkbox, not just the checkbox itself.
- **Chart stacking order**: When toggling "Include loans" or "Include
real estate", the chart now re-mounts so accounts appear in the correct
sorted position (biggest at bottom) instead of being appended on top.
- **Scheme-aware liability dot**: The red liability indicator dot in the
chart tooltip now respects the user's chart color scheme (neutral, blue,
pink) instead of always using the destructive/red color.
2026-03-31 11:19:35 +01:00
Víctor Falcón f140b5df7f
fix(dashboard): treat loans as debt in net worth (#238)
## Summary
- fix dashboard net worth math so loan balances reduce totals and trends
instead of showing as positive assets
- add a per-user toggle in the net worth chart settings to include or
exclude loans from the dashboard net worth card
- cover the new preference and liability handling with feature and
frontend tests

## Screenshots
<img width="1121" height="509" alt="image"
src="https://github.com/user-attachments/assets/ab6a7cde-1052-4dab-aa14-34f6d9528829"
/>
<img width="1122" height="506" alt="image"
src="https://github.com/user-attachments/assets/e48a0369-16c4-4294-ae00-4eba56480264"
/>
2026-03-20 09:55:53 +00:00
Víctor Falcón 956b661448
fix(amount-display): eliminate float round-trip causing missing thousands separator (#191)
## Summary

- Fixes intermittent missing thousands separator on the dashboard
account card balance display, reproducible with Binance accounts whose
crypto-derived balances land on specific cent values.
- Root cause: `AmountDisplay` was doing `amountInCents / 100` → state →
`amount * 100` back into `formatCurrency`. This IEEE 754 round-trip
introduces floating-point noise (e.g. `1234567 / 100 * 100 =
1234566.9999...`), causing `Intl.NumberFormat` to produce unexpected
output.
- Fix: pass `amountInCents` directly to `formatCurrency` (which already
handles the `/100` division internally). The encryption fake-amount
display is also simplified from `useState + useEffect` to a single
`useMemo`.

## Changes

- `resources/js/components/ui/amount-display.tsx` — remove intermediate
`amount` state, pass `amountInCents` directly to `formatCurrency`
- `resources/js/utils/currency.test.ts` — new test file covering correct
formatting, thousands separators, and the specific float round-trip edge
cases
2026-03-03 12:40:30 +00:00
Víctor Falcón 832fc6177e
feat(dashboard): make top spending categories clickable with transaction filter link (#189)
## Summary

- Makes each top spending category in the dashboard card a clickable
link to the transactions page, pre-filtered by that category and the
last 30 days
- Uses the same muted background hover effect (`hover:bg-muted`) as the
account title links on the dashboard for visual consistency
2026-03-03 11:24:50 +00:00
Víctor Falcón 2a1286e98a
chore(frontend): add orphan component detection and remove dead components (#181)
## 🚪 Why?

### Problem

Dead frontend components were accumulating in the codebase with no
automated way to detect them. Unused code increases maintenance burden,
confuses contributors, and inflates bundle analysis noise. Additionally,
several React hooks had incorrect dependency arrays causing stale
closures or unnecessary re-renders, and an unused variable was left in
the new test file.

## 🔑 What?

### Changes

- Add `resources/js/lib/orphan-components.test.ts` — a Vitest test that
scans every file under `resources/js/components/` and fails if any
component is never imported anywhere in the codebase (supports `@/`
alias imports, relative imports, and barrel `index.*` re-exports)
- Delete 14 orphan components identified by the new test:
-
`accounts/import-balances/import-balance-step-{account,mapping,preview,upload}`
  - `app-mobile-nav`
  - `appearance-dropdown`
  - `dashboard/stat-card`
  - `landing/encryption-video-player`
  - `sync-status-button`
  - `transactions/date-header`
  - `ui/{icon,input-group,navigation-menu,placeholder-pattern}`
- Restore `ui/avatar.tsx` which was incorrectly included in the deletion
(it is imported via relative path in `user-info.tsx`)
- Fix ESLint warnings across 5 files:
- `orphan-components.test.ts` — remove unused `componentDir` variable in
`importStrings()`
- `transaction-list.tsx` — remove `categories`, `accounts`, `banks`,
`automationRules` from `useCallback` deps (none referenced in callback
body)
- `amount-input.tsx` — add missing `locale` dep to `useEffect` (used in
`formatCurrency`)
- `use-dashboard-data.ts` — wrap `fetchData` in `useCallback([locale])`
and add to `useEffect` deps to prevent stale closure on locale
- `dashboard.tsx` — wrap `netWorthEvolution` fallback in
`useMemo([props.netWorthEvolution])` to stabilise the reference passed
to the downstream `useMemo`

##  Verification

### Tests

The new test runs as part of `bun run test` (Vitest), already executed
in the `linter` CI job. All 48 tests pass:

```
✓ resources/js/lib/chart-calculations.test.ts (32 tests)
✓ resources/js/lib/file-parser.test.ts (15 tests)
✓ resources/js/lib/orphan-components.test.ts (1 test)
```

`bun run lint` and `bun run build` now exit clean.

Going forward, any new component added to `resources/js/components/`
that is not imported anywhere will fail CI automatically.
2026-03-02 12:43:27 +01:00
Víctor Falcón 152b186c10
feat(privacy): enable privacy mode for all users and extend amount masking (#182)
## 🚪 Why?

### Problem
Privacy mode — which masks financial amounts so users can share their
screen without leaking account balances — was previously gated behind an
admin-only check. Regular users had no way to toggle it from the UI.
Additionally, several places in the app (chart tooltips, budget pages)
were rendering raw amounts via \`formatCurrency\` directly, bypassing
the privacy mode check entirely.

## 🔑 What?

### Changes
- Remove the \`isAdmin()\` guard from the privacy mode toggle in the
user menu, making it available to all users
- Mask digits with \`*\` in \`AmountDisplay\` when privacy mode is
enabled (e.g. \`$1,234.56\` → \`$*,***.**\`)
- Apply the same digit masking to the net worth chart tooltip totals in
\`chart.tsx\` (both single-currency and multi-currency paths)
- Replace raw \`formatCurrency\` calls with \`AmountDisplay\` in
\`budget-list-card.tsx\` (spent, allocated, remaining)
- Add a \`maskIfPrivate\` helper in \`budget-spending-chart.tsx\`
CustomTooltip to mask allocated, spent, last period, and available
amounts when privacy mode is active

##  Verification
<img width="1858" height="1193" alt="image"
src="https://github.com/user-attachments/assets/ec734841-c5cb-44e9-a9f5-ddee7a53a78c"
/>
<img width="1255" height="762" alt="image"
src="https://github.com/user-attachments/assets/b1b93615-41a0-4e8d-8dea-7b27e227181c"
/>
2026-03-02 10:48:45 +00:00
Víctor Falcón 4d14e4d2f0
feat(ui): add glowing effect to all card components (#170)
## Why

### Problem
Cards across the app lacked visual interactivity. Adding a
cursor-tracking glowing border effect improves the UI polish and makes
the dashboard and other card-heavy pages feel more dynamic.

## What

### Changes
- Install `motion` package (required for the `animate()` call in the
effect)
- Add `GlowingEffect` component at
`resources/js/components/ui/glowing-effect.tsx` — tracks pointer
position and renders an animated conic gradient border that follows the
cursor
- Update the base `Card` primitive to include `<GlowingEffect>` as the
first child with `spread=40`, `proximity=64`, `inactiveZone=0.01`,
`glow=true` — all consumers inherit the effect automatically
- Remove `overflow-hidden` from the net-worth chart's `<Card>` (it was
clipping the effect) and move it to `<CardContent>` where it's actually
needed to contain the chart

## Verification

### Tests
No new logic was introduced — this is a purely visual enhancement to an
existing UI primitive. Existing tests remain unaffected.

### Manual
Move the cursor near and over any card (dashboard stats, account
balances, net worth chart, cashflow, budgets) to see the gradient border
glow follow the cursor.
2026-03-01 10:56:59 +00:00
Víctor Falcón 2b9fd2384a
feat(i18n): localize Spanish translations and currency formatting (#160)
## Why

### Problem
The app had two categories of localization gaps:
1. Many UI strings were not translated — Spanish users saw raw English
text because keys were missing from `lang/es.json`.
2. All currency and number formatting was hardcoded to `'en-US'` (or
`undefined`), so amounts always formatted in English style regardless of
the user's locale setting.

## What

### Changes
- **`lang/es.json`** — Added 22 missing Spanish translation keys
covering onboarding, automation rules, balance history, dashboard, and
more.
- **`utils/currency.ts`** — Added `locale` parameter (default `'en-US'`)
to `formatCurrency`.
- **`utils/chart.tsx`** — Added `locale` parameter to
`formatCurrencyWithCode`; replaced `.toLocaleString()` calls with
locale-aware versions.
- **`hooks/use-dashboard-data.ts`** — Added `locale` param to
`formatMonth` and `deriveAccountMetrics`; wired `useLocale()` inside
`useDashboardData`.
- **`pages/dashboard.tsx`** — Added `useLocale()` and passed `locale` to
`deriveAccountMetrics`.
- **`components/charts/sankey-chart.tsx`** — Added `locale` param to
`formatAmount` and threaded it through `OtherCategoriesBreakdown`.
- **`components/charts/mom-chart.tsx`** — Replaced hardcoded `'en-US'`
in YAxis tickFormatter with `useLocale()`.
- **`components/budgets/budget-list-card.tsx`** — Passed `locale` to all
`formatCurrency` calls.
- **`components/budgets/budget-spending-chart.tsx`** — Passed `locale`
to all `formatCurrency` calls in `CustomTooltip`.
- **`components/ui/amount-display.tsx`** — Added `useLocale()` and
passed to `Intl.NumberFormat`.
- **`components/ui/amount-input.tsx`** — Added `useLocale()` and passed
to `Intl.NumberFormat` used for display formatting.
- **`components/accounts/balances-modal.tsx`** — Added `useLocale()` for
both the balance formatter and `formatDate`.
- **`components/transactions/edit-transaction-dialog.tsx`** — Replaced
hardcoded `'en-US'` with `useLocale()`.
-
**`components/accounts/import-balances/import-balance-step-mapping.tsx`**
— Replaced `'en-US'` with `useLocale()` in preview formatting.
- **`components/transactions/import-step-mapping.tsx`** — Replaced
`'en-US'` with `useLocale()` in preview formatting.
- **`components/onboarding/step-create-account.tsx`** — Fixed garbled
Spanish caused by 3-part JSX string concatenation; replaced with single
translation key.
- **`components/automation-rules/create-automation-rule-dialog.tsx`** —
Fixed multiline description key mismatch with `es.json`.

## Verification

### Tests
- TypeScript check (`npx tsc --noEmit`) passes with no new errors
introduced by these changes.
- `lang/es.json` validated as well-formed JSON.

### Manual
- With locale set to `es`, all previously untranslated strings now
render in Spanish.
- Currency amounts format according to the user's locale (e.g.,
`1.234,56 €` in `es` vs `$1,234.56` in `en-US`).
2026-02-28 18:46:18 +00:00
Víctor Falcón 1b7b147832
fix(ui): app icon visible on light wallpapers + country select overflow on mobile (#162)
## Why

### Problem
Two iOS-specific UX issues reported by users:

1. **App icon invisible on light wallpapers** — the home screen shortcut
icon was a black logo on a white background, making it nearly invisible
against light iOS wallpapers (appeared as a faint frosted outline).
2. **Country dropdown overflows viewport** — the country picker in the
Connect Account flow exceeded the mobile viewport height, making it
impossible to scroll through the full list on small screens.

### Root Cause
1. All icon PNGs were exported with a white background and a black/grey
logo. The PWA manifest `background_color` was also `#ffffff`. No dark
background was baked in.
2. `SelectContent` used a fixed `max-h-96` (384px) regardless of
available screen space. The `SelectPrimitive.Viewport` height was bound
to `--radix-select-trigger-height` (the trigger element's height)
instead of the actual available viewport space, preventing Radix's
internal scroll buttons from working correctly on mobile.

## What

### Changes
- Regenerated all icon PNGs (favicon, apple-touch-icon,
web-app-manifest, whispermoney_icon_x*) — white logo on `#1b1b18` dark
background via ImageMagick Screen composite
- Updated `site.webmanifest` `background_color` from `#ffffff` →
`#1b1b18`
- Fixed `SelectContent` max height: `max-h-96` →
`max-h-[min(24rem,var(--radix-select-content-available-height))]`
- Fixed `SelectPrimitive.Viewport` height:
`--radix-select-trigger-height` →
`--radix-select-content-available-height`

## Verification

### Tests
No automated tests cover static asset generation or CSS utility values —
these are visual/rendering fixes.

### Manual
- iOS: Remove and re-add the home screen shortcut from Safari to pick up
the new `apple-touch-icon.png`. Icon should show a white bird logo on a
dark background, visible on any wallpaper.
- Mobile (iOS/Android): Open Connect Account dialog and verify the
country dropdown fits within the viewport and is fully scrollable.
2026-02-28 13:41:41 +00:00
Víctor Falcón b1f01e4a8f
feat(automation-rules): simplify smart rules UI, fix re-evaluation, and localize amounts (#161)
## Why

**Problem:** Bulk re-evaluation of transactions against smart rules was
broken because the code required an encryption key that no longer exists
in the app. Additionally, the rule builder UI had unnecessary complexity
(priority field, labels/tags, date/category/notes conditions) that added
cognitive load without value. Currency amounts were also always
formatted with US conventions regardless of the user's locale.

## What

**Automation rules – re-evaluation fix:**
- Remove encryption key requirement from `evaluateRules`,
`evaluateRulesForTransactions`, `evaluateRulesForNewTransaction`, and
`prepareTransactionData` (accept `CryptoKey | null`)
- Drop `isKeySet` guards from single-transaction and bulk re-evaluate
handlers in `transaction-list.tsx` and `transaction-actions-menu.tsx`
- Simplify `use-re-evaluate-all-transactions.tsx` to pass `null` as key
directly

**Automation rules – UI cleanup:**
- Hide priority field from create/edit dialogs (still sends `priority:
0` / `rule.priority` to satisfy backend validation)
- Remove labels/tags from create/edit dialogs, actions column, and table
display
- Remove `date`, `category`, and `notes` from rule condition field
options in `rule-builder-utils.ts`
- Add `opacity-30` to disabled X button when only one condition/group
remains

**Amount localization:**
- Replace hardcoded `'en-US'` with `useLocale()` in `AmountDisplay` so
symbol position and number format follow the user's locale (e.g.
`89.705,00 €` in Spanish)

## Verification

- 38/38 `AutomationRuleTest` + `AutomationRuleEvaluationTest` tests pass
- Re-evaluation works without an encryption key set
- Spanish locale users see `89.705,00 €` instead of `€89,705.00`
2026-02-28 13:24:57 +00:00
Víctor Falcón b743cad803
feat: Add multi-currency conversion for net worth charts (#138)
## Why

### Problem

Users with accounts in multiple currencies see raw balances on the net
worth chart without any conversion. A user with a USD-denominated
profile who also holds EUR and GBP accounts sees misleading totals — the
chart simply sums cents across currencies as if they were the same unit.

## What

### Changes

- **`ExchangeRateService`** — New service that wraps
`CurrencyConversionService` with DB-level caching. Checks the
`exchange_rates` table first, falls back to the external CDN API on
cache miss, and stores the full rate blob for future lookups.
- **`exchange_rates` migration + model** — New table with
`(base_currency, date)` unique constraint storing JSON rate blobs.
Includes factory.
- **`CurrencyConversionService`** — Made `getRatesForCurrency()` public
so `ExchangeRateService` can delegate to it.
- **`DashboardAnalyticsController`** — `netWorthEvolution` and
`netWorthDailyEvolution` endpoints now convert every account balance
from `account.currency_code` to `user.currency_code`. Response includes
`currency_code` field and `_original` data for foreign-currency
accounts.
- **Future date capping** — `ExchangeRateService::getRates()` caps any
future date to today, since the currency API only serves historical
rates. Prevents 500 errors when the chart requests end-of-month rates
for the current (incomplete) month.
- **Frontend charts** — Simplified to single-currency display. Removed
multi-currency `TotalDisplay` component; the chart header and tooltip
now show amounts in the user's currency. Foreign-currency accounts show
their original amount inline in the tooltip (e.g., "€2,000.00 →
$2,222.22").
- **`valueFormatter` type fix** — Changed return type from `string` to
`React.ReactNode` in chart component interfaces to match actual usage
with `AmountDisplay`.

## Screenshot
<img width="1266" height="649" alt="image"
src="https://github.com/user-attachments/assets/6f661840-3f65-49fe-9948-d4cfd6c6ef0e"
/>
2026-02-20 10:45:23 +01:00
Víctor Falcón 900cf41e31
feat: Add daily granularity toggle with area visualization to net worth chart (#136)
## Summary

Adds daily granularity support to the dashboard Net Worth Evolution
chart, mirroring the daily balance feature added for individual account
pages in #135.

## Why

The net worth chart only supported monthly granularity, making it
impossible to see short-term net worth fluctuations. Users who track
daily balances need a way to visualize day-over-day net worth changes
across all accounts.

## Screenshots
<img width="1263" height="714" alt="image"
src="https://github.com/user-attachments/assets/646e7e21-5c1b-42dc-9a20-5e9b6d5d72b4"
/>
<img width="1261" height="715" alt="image"
src="https://github.com/user-attachments/assets/b9fe5080-55f3-485a-a566-1d25d83518c7"
/>
2026-02-19 15:35:22 +01:00
Víctor Falcón db7b6e4da7
feat: Integrate EnableBanking as open banking provider (#106)
## Summary

- Adds **EnableBanking** as the first open banking provider, allowing
users to connect real bank accounts and automatically sync transactions
and balances
- Uses a **`BankingProviderInterface`** contract so future providers
(Plaid, GoCardless, etc.) can be added by implementing the same
interface
- Feature-flagged behind the **`open-banking`** Pennant flag (default:
off)
- Connected accounts are **unencrypted** and transactions have `source =
'enablebanking'`

### What's included

**Backend:**
- `BankingProviderInterface` contract + `EnableBankingProvider`
implementation (JWT RS256 auth)
- `BankingConnection` model with full lifecycle (pending →
awaiting_mapping → active → expired/revoked/error)
- `TransactionSyncService` — pagination, deduplication by
`external_transaction_id`, amount/date mapping
- `BalanceSyncService` — preferred balance type selection (CLBD → ITAV
fallback)
- Authorization flow: start auth → bank redirect → callback → session
creation → account mapping → sync
- `SyncBankingConnectionJob` (unique per connection, 3 retries) +
scheduled every 6 hours
- `banking:sync` artisan command
- 5 migrations: `banking_connections` table, account fields, transaction
`external_transaction_id`, `pending_accounts_data`, `linked_at`

**Frontend:**
- Manual vs Connected account choice in the create account dialog
- Multi-step bank connection dialog (country → bank selection →
confirmation → redirect)
- Account mapping page — map discovered bank accounts to existing
accounts, create new ones, or skip
- Settings/Connections page with status badges, sync/disconnect actions
- "Connected" badge on linked accounts in settings

**Tests:**
- 49 tests covering feature flags, controllers, account mapping,
transaction sync, balance sync, deduplication, and pagination

### Feature Flags

This PR introduces **two Pennant feature flags**:

1. **`open-banking`** — Gates the entire open banking feature
(institutions endpoint, authorization flow, connections page). When
disabled, all open banking routes return 404.

2. **`account-mapping`** — Controls whether users see an intermediate
account mapping step after connecting a bank. When **enabled**, users
are redirected to a mapping page where they can choose to create new
accounts, link to existing ones, or skip each discovered bank account.
When **disabled**, all discovered accounts are automatically created
(original behavior). Linked accounts only sync transactions from their
last transaction date and only update the current balance from the
provider (no historical balance calculation or daily balance tracking).

Enable per-user:
```bash
php artisan feature:enable open-banking user@example.com
php artisan feature:enable account-mapping user@example.com
```

Enable for all users:
```bash
php artisan feature:enable open-banking all
php artisan feature:enable account-mapping all
```

## Test plan

- [x] Enable feature flag: `php artisan feature:enable open-banking`
- [x] Verify "Connected" option appears in create account dialog
- [x] Start authorization flow and verify redirect to bank
- [x] With `account-mapping` **disabled**: verify callback creates
accounts directly and dispatches sync
- [x] With `account-mapping` **enabled**: verify callback redirects to
mapping page
- [x] Test mapping page: create new, link to existing, and skip actions
- [x] Verify linked accounts sync only from last transaction date and
only update current balance
- [x] Verify connections page shows "Setup Required" badge for
awaiting_mapping status
- [x] Run `php artisan banking:sync` and verify transactions sync
- [x] Verify connections page shows status, sync, and disconnect actions
- [x] Run full test suite: `php artisan test --compact`
2026-02-12 09:09:28 +01:00
Víctor Falcón 48b4b7bd01 fix: Console log errors with Charts 2026-02-10 16:26:58 +01:00
Víctor Falcón a76826bd62 fix: console error 2026-02-10 14:47:46 +01:00
Víctor Falcón 70b603e901
feat: Spanish localization (#74)
## Pending
- [x] Translate landing page
- [x] Dashboard
- [x] Accounts list page
- [x] Account page
- [x] Cashflow
- [x] Budgets
- [x] Transactions
- [x] Settings

## Screenshots
<img width="1210" height="969" alt="image"
src="https://github.com/user-attachments/assets/c7935e5c-488d-4941-8f19-8834e5668257"
/>
<img width="1211" height="972" alt="image"
src="https://github.com/user-attachments/assets/e94e1daf-233a-4a49-aa65-5678c772d178"
/>
2026-02-08 11:58:08 +01:00
Víctor Falcón c32a7ab6ae
Fix chart hover background visibility in dark mode (#82)
## Summary

- Fixed hover background color in dark mode to improve visibility and
contrast with chart bars
- Changed from `fill-muted` (oklch(0.269 0 0)) to `fill-white/5` in dark
mode for a lighter, more transparent background
- Resolves issue where hover background was too similar to bar colors in
dark mode

## Changes

- Updated `ChartContainer` component to use `dark:fill-white/5` for the
recharts tooltip cursor in dark mode
- Maintains existing `fill-muted` behavior for light mode

Closes #66
2026-01-27 19:08:35 +01:00
Víctor Falcón 9b6c30775f
Add Budgeting Feature to Track and Manage Spending (#36)
## Overview

We're excited to introduce budgeting capabilities to Whisper Money! This
feature helps you take control of your finances by setting spending
limits and tracking your progress over time.

## Screenshots
<img width="1316" height="793" alt="image"
src="https://github.com/user-attachments/assets/ac394d36-cded-4ea4-9883-120785e260f1"
/>
<img width="1315" height="907" alt="image"
src="https://github.com/user-attachments/assets/7c682474-5aa7-4388-b626-29b56f5ebbef"
/>
<img width="1315" height="992" alt="image"
src="https://github.com/user-attachments/assets/21eace45-23c6-472d-9aa0-0feb6db3fba4"
/>

## What's New

### Create Flexible Budgets
- Set budgets for specific categories or labels
- Choose from monthly, weekly, bi-weekly, or custom periods
- Set your own budget start date for better alignment with your pay
schedule

### Track Your Spending
- Visual spending charts show how much you've spent vs. your budget
- See at a glance which budgets are on track and which need attention
- View all transactions that count toward each budget

### Smart Budget Management
- **Carry Over**: Unused budget amounts automatically roll into the next
period
- **Reset**: Unused amounts return to your available money pool
- Edit or delete budgets anytime as your needs change

### Easy Access
- New Budgets section in the main navigation
- Quick overview cards showing budget status
- Detailed budget pages with spending history and transaction lists

## How It Works

1. Create a budget by selecting a category or label and setting your
spending limit
2. Your transactions are automatically matched to relevant budgets
3. Track your progress with visual charts and spending summaries
4. Adjust your budgets as needed to stay on track with your financial
goals

This feature is now available behind a feature flag and can be enabled
for users who want to start budgeting their expenses.
2026-01-21 15:25:50 +01:00
Víctor Falcón f4ab9181e1
feat: Better, easier, and faster account balance update modal (#65)
https://github.com/user-attachments/assets/a80966de-b282-4ba0-9431-2cfbcfddc023

Every detail is important. Now, when you are about to update the balance
of an account, the current balance is pre-filled, and you can even do
simple math operations.

If you just receive 1,000 dollars, you can just type `+ 1000` and press
enter.

No need to do the maths by your self.
2026-01-19 19:43:20 +01:00
Víctor Falcón c1ddc1477d fix: scroll category combobox to top while searching 2026-01-09 10:02:25 +01:00
Víctor Falcón 9af941cb47 ui: add cursor pointer to the buttons 2026-01-07 11:12:03 +01:00
Víctor Falcón 2eccee09a2 ui: amount display when encryption key is not set 2025-12-30 14:25:19 +01:00
Víctor Falcón 5fe961a308 chore: update privacy mode to update transactions descriptions 2025-12-30 11:03:58 +01:00
Víctor Falcón 46454f193f
Update balance with manually created transactions (#45)
<img width="1195" height="874" alt="image"
src="https://github.com/user-attachments/assets/1d2f5e10-dad5-4136-8f65-5e8a3e8b30c5"
/>
2025-12-30 07:55:34 +01:00
Víctor Falcón a785a27554 ui(fix): add hasKeyboard prop to dialogs
This property move the modal to the top on mobile, so we left space
bellow for the keyboard. On desktop is vertically centered as it was
before.
2025-12-30 07:22:19 +01:00
Víctor Falcón f8bea791e8
Bulk actions/select all (#33) 2025-12-15 17:59:58 +01:00
Víctor Falcón bf0c9ae989
fix: Automated rules with labels (#32) 2025-12-15 13:42:12 +01:00
Víctor Falcón 4c26c7588d
Amount display format (#29) 2025-12-15 10:44:13 +01:00
Víctor Falcón ab61266955 Fix amount field formatting when pressing Enter
- Add onKeyDown handler to AmountInput component to format value when Enter is pressed
- Ensure amount is properly parsed before form submission regardless of submission method
- Add browser test to verify amount formatting on Enter key press
2025-12-15 08:47:11 +01:00
Víctor Falcón 8811afbad8
feat: Add privacy mode to hide monetary amounts (#28)
## Summary
This PR implements a global privacy mode feature that allows users to
hide all monetary amounts throughout the application.

## Changes
- **Created PrivacyModeContext**: New context provider with localStorage
persistence for privacy mode state
- **Created AmountDisplay component**: Centralized component for
displaying monetary amounts with blur effect when privacy mode is
enabled
- **Updated app logo logic**: Logo now shows birdhouse icon when privacy
mode is enabled or encryption key is not set
- **Added privacy mode toggle**: New toggle in user menu to
enable/disable privacy mode
- **Replaced inline amount formatting**: All monetary amounts now use
the AmountDisplay component
- **Updated trend indicators**: AmountTrendIndicator and
PercentageTrendIndicator now use AmountDisplay
- **Added animated transitions**: Smooth transitions when toggling
privacy mode

## Features
- Privacy mode state persists across sessions (localStorage)
- Amounts display as `$•••.••` with blur effect when privacy mode is
enabled
- Animated transitions (300ms) when toggling privacy mode
- App logo changes to birdhouse when privacy mode is on
- Toggle accessible from user menu at the top

## Testing
- Tested privacy mode toggle functionality
- Verified amounts are hidden/shown correctly across all pages
- Confirmed logo animation works properly
- Verified localStorage persistence

## Screenshots
Privacy mode can be toggled from the user menu, and all amounts
throughout the application will be hidden with a blur effect.
2025-12-14 16:03:02 +01:00
Víctor Falcón 4f380af8f8
Refactor: Transaction Table Layout and Styling Improvements (#26)
## Overview
This PR refactors the transaction table to improve layout, styling, and
user experience. The changes include better column organization,
improved date grouping, and enhanced visual consistency.

<img width="1302" height="595" alt="image"
src="https://github.com/user-attachments/assets/72dd38fa-bfa3-432c-a342-fbcc1a05414b"
/>

## Changes

### Layout & Structure
- Added date and account columns to the transaction table
- Consolidated description cell for better readability
- Improved date grouping functionality
- Enhanced transaction column definitions and organization

### Styling Improvements
- Improved column styling and cell padding throughout the table
- Updated bulk actions bar styling
- Adjusted account selector alignment
- Enhanced data-table component with better spacing and visual hierarchy

### Components Updated
- `transaction-columns.tsx` - Refactored column definitions
- `transaction-list.tsx` - Improved list rendering and layout
- `date-header.tsx` - New component for date grouping
- `data-table.tsx` - Enhanced table component styling
- `category-cell.tsx` - Improved category display
- `bulk-actions-bar.tsx` - Updated styling
- `transactions/index.tsx` - Updated page layout

## Files Changed
- 9 files changed
- 463 insertions(+), 182 deletions(-)

## Testing
- [x] Verify transaction table displays correctly
- [x] Check date grouping functionality
- [x] Test bulk actions bar alignment
- [x] Verify responsive behavior
2025-12-14 12:56:05 +01:00
Víctor Falcón 8e1b2c0969
Add sync now button to pending operations dialog (#27)
Adds a 'Sync now' button below the pending operations list in the
dialog, allowing users to manually trigger a sync directly from the
dialog.

## Changes
- Added new props to `PendingOperationsDialog`: `onSyncNow`,
`syncStatus`, and `isOnline`
- Added a button at the bottom of the dialog that triggers the sync
process
- Button shows loading state with spinning icon while syncing
- Button is disabled when offline or when sync is already in progress
2025-12-14 12:50:00 +01:00
Víctor Falcón 389e272318 Refactor transaction UI components for better responsive layout and consistency
- Remove explicit button sizes from transaction actions menu
- Improve action buttons layout with flex containers and responsive classes
- Add hideColumnTextOnMobile prop to DataTableViewOptions for better mobile control
- Fix Tailwind class ordering and indentation in categorize page
- Update transaction filters actions container for better mobile layout
2025-12-10 13:28:45 +01:00