Commit Graph

331 Commits

Author SHA1 Message Date
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 186ae57c2d
refactor(accounts): unify account type icons via accountIconByType helper (#180)
## Why

### Problem
Account type icons were defined in two separate places —
`step-account-types.tsx` (onboarding) and `account-type-icon.tsx`
(dashboard) — using different icon choices for the same account types.
For example, `checking` showed `Building2` on the dashboard but `Wallet`
in onboarding, and `retirement` showed `Umbrella` vs `TrendingUp`. This
created a visually inconsistent experience.

## What

### Changes
- Added `accountIconByType(type: AccountType): LucideIcon` helper to
`resources/js/types/account.ts` as the single source of truth for
account type icons
- Updated `AccountTypeIcon` component to delegate to the new helper
instead of maintaining its own map
- Updated `StepAccountTypes` to derive icons from the helper instead of
storing them inline per entry
- Dashboard account cards now use the same icon set as the onboarding
screen (Wallet, PiggyBank, CreditCard, LineChart, TrendingUp, Building2)

## Verification

### Tests
No behaviour change — purely a refactor consolidating icon mapping into
one place. Existing tests remain unaffected.
2026-03-02 08:47:44 +00:00
Víctor Falcón edcadace1a chore: Welcome header text UI 2026-03-02 08:33:06 +00:00
Víctor Falcón 6c5961da05 fix: Missing space between page sections and create button 2026-03-01 19:31:28 +00:00
Víctor Falcón efd86bc8d7
feat(nav): add icon+label mobile nav with active pill and full-width buttons (#178)
## Why

### Problem
The mobile bottom navigation bar showed only icons with no labels,
making it hard to identify each section at a glance — especially for new
users.

## What

### Changes
- Each nav item now renders its icon above a short text label
- Active item gets a subtle pill background (`bg-primary/15`) matching
the reference design
- Each button uses `flex-1` so items stretch to fill all available width
evenly
- Added `mobileTitle` to all nav items with short, space-conscious
labels: `Panel`, `Flujo`, `Cuentas`, `Movim.`, `Presup.`
- Removed the `__()` translation dependency from the mobile nav to avoid
a runtime crash that was causing icons to disappear entirely
2026-03-01 19:43:18 +01:00
Víctor Falcón 0388705c12
fix(transactions): fix toolbar overflow on mobile and shorten button label (#175)
## Why

### Problem
On iOS (and narrow mobile screens), the transactions toolbar overflows
the viewport — the Columns button is pushed off-screen because the
actions row does not wrap.

Additionally, the \"Add Transaction\" button label was too long for
small screens, contributing to the overflow.

## What

### Changes
<img width="388" height="374" alt="image"
src="https://github.com/user-attachments/assets/7962f21d-70b7-427b-b01b-a855b687452d"
/>
2026-03-01 17:57:59 +00:00
Víctor Falcón 7260525890
fix(i18n): localise missing strings in budget dialogs to Spanish (#177)
## Summary

- Wrap all hardcoded strings in `create-budget-dialog.tsx` and
`edit-budget-dialog.tsx` with `__()` so they pass through the i18n
system (submit button label, start day labels, helper text for
carry-over/reset, and the selection-required validation message)
- Update `getBudgetPeriodTypeLabel` and `getRolloverTypeLabel` in
`budget.ts` to use `__()` so period type options (Monthly, Weekly,
Bi-weekly, Custom) and rollover types (Carry Over, Reset/Pool) are
translated
- Add all missing Spanish translations to `lang/es.json`: period type
labels, rollover type labels, start-day helpers, carry-over/reset
descriptions, and validation messages
2026-03-01 17:56:02 +00:00
Víctor Falcón 7a8eda9d90
fix(i18n): localize billing settings page into Spanish (#176)
## Summary

- Wrap benefit card titles and descriptions in `__()` — they were
hardcoded strings never passed through the translation function
- Move the `benefits` array inside the component so `__()` is called at
render time (after translations are loaded)
- Add 7 missing Spanish translation keys in `lang/es.json`
- Fix the untranslated `"— $9/month"` entry that had the English string
as its own translation

## Missing keys added

| English | Spanish |
|---|---|
| Unlimited Everything | Sin Límites en Todo |
| No limits on bank accounts, transactions, or categories. | Sin límites
en cuentas bancarias, transacciones o categorías. |
| Your data is never shared with third parties. You are always the
owner. | Tus datos nunca se comparten con terceros. Tú eres siempre el
propietario. |
| Smart Automation | Automatización Inteligente |
| Automation rules to categorize transactions automatically. | Reglas de
automatización para categorizar transacciones automáticamente. |
| Priority Support | Soporte Prioritario |
| Get help when you need it with priority email support. | Obtén ayuda
cuando la necesites con soporte prioritario por correo electrónico. |
| — $9/month | — $9/mes |
2026-03-01 18:43:09 +01:00
Víctor Falcón b433f1f07e chore: Improve create account dialog 2026-03-01 12:50:42 +00:00
Víctor Falcón 9317238c49
feat(i18n): add localization test and fix missing Spanish translations (#174)
## 🚪 Why?

### Problem
Untranslated strings were silently reaching production. Several UI
components rendered raw English literals without going through \`__()\`,
and 15+ strings used via \`__()\` had no entry in \`lang/es.json\`,
causing Spanish-locale users to see untranslated text. There was also no
automated safeguard to catch new regressions.

## 🔑 What?

### Changes
- Add \`tests/Feature/LocalizationTest.php\` with two test groups that
run in CI:
1. **PHP translation files** — verifies every key in \`lang/en/*.php\`
exists in the matching \`lang/es/*.php\` file
2. **Spanish JSON translations** — scans all \`.ts\`/\`.tsx\` files for
static \`__()\` calls and asserts each key exists in \`lang/es.json\`
- Wrap previously unwrapped literals in \`__()\` in `header.tsx`
(Github, Discord, Dashboard, Log in, Register), \`appearance.tsx\`
(Chart color scheme heading and color scheme labels), and
\`rule-builder.tsx\` (field config labels and operator labels at render
site)
- Add 15 missing Spanish translations to \`lang/es.json\`: Discord,
Chart color scheme, Choose the color palette for your charts, Blue,
Colorful, Neutral, Pink, Account Name, Bank Name, contains, equals,
greater than, is empty, is not empty, less than

##  Verification

### Tests
- New test: `tests/Feature/LocalizationTest.php` — 2 tests, 4
assertions, all passing
- Runs automatically in CI via the existing `./vendor/bin/pest` job (no
CI config changes needed)

### Manual Verification
- `php artisan test --compact tests/Feature/LocalizationTest.php` →
`Tests: 2 passed (4 assertions)`
2026-03-01 12:32:39 +00:00
Víctor Falcón 0493b87562
feat(Budgets): add period navigation and unify period selector UI (#171)
## Why

### Problem

Users viewing a budget had no way to look back at historical periods —
the show page always displayed the current period with no navigation.
The cashflow page's period selector also used a different visual style
(loose buttons with gaps) compared to the new grouped button pattern
established for budgets.

## What

### Changes

- **Budget period navigation**: new `BudgetPeriodNavigation` component
renders a `ButtonGroup` with `[<] [date range] [>]` buttons. Clicking
the label returns to the current period; arrows navigate between
existing periods.
- **Backend**: `BudgetController::show` accepts an optional
`?period=<uuid>` query param to serve a specific period. Future periods
are blocked at the query level on both direct access and `nextPeriod`
resolution.
- **Dropdown**: removed the split `ButtonGroup` from the budget header;
"Edit budget" and "Delete budget" now live together in a single `˅`
dropdown.
- **Cashflow period selector**: updated `PeriodNavigation` to use
`ButtonGroup` + `size="icon"` buttons to match the same visual style.
- **Tests**: 4 new feature tests covering period navigation,
future-period blocking, cross-budget access (404), and `nextPeriod`
boundary behaviour.

## Verification
<img width="921" height="683" alt="image"
src="https://github.com/user-attachments/assets/ceb5f70b-a15a-4a36-ae49-5d84054a62f9"
/>
2026-03-01 12:25:01 +00:00
Víctor Falcón 717bf34103
fix(i18n): localize mobile bottom navigation labels into Spanish (#173)
## Why

### Problem
The mobile bottom tab bar navigation labels (Dashboard, Cashflow,
Accounts, Transactions, Budgets) were being rendered as raw English
strings regardless of the user's locale. Spanish-speaking users would
see English labels in the mobile navigation despite the rest of the app
being translated.

## What

### Changes
- Import the `__()` translation utility in `app-sidebar.tsx`
- Wrap `{item.title}` with `{__(item.title)}` in the mobile bottom tab
bar so labels go through the same translation lookup as the desktop
sidebar

All relevant Spanish translations (`"Dashboard"`, `"Cashflow"`,
`"Accounts"`, `"Transactions"`, `"Budgets"`) were already present in
`lang/es.json`.

## Verification
<img width="343" height="118" alt="image"
src="https://github.com/user-attachments/assets/c58acff7-fdbd-4dda-b2fc-4201865118c1"
/>
2026-03-01 12:04:44 +00:00
Víctor Falcón 9f5e62f736
feat(ui): add create buttons to accounts and budgets pages (#172)
## Summary

- Add a top-right **Create Budget** / **Create Account** button (using
the existing `CreateButton` component) to the budgets and accounts index
pages, matching the UI pattern already used in Settings > Bank accounts
- Add a card-style **Create Account** trigger at the end of the accounts
grid, mirroring the existing card-style **Create Budget** trigger in the
budgets grid
- Refactor `CreateBudgetDialog` and `CreateAccountDialog` to accept an
optional `trigger` prop, keeping existing usages as-is (no breaking
changes)

### Video

https://github.com/user-attachments/assets/37322387-720a-4b07-b3af-b43cd897ba5a
2026-03-01 11:37:29 +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 0d9fc5a2b9
feat(transactions): re-add select all matching filters to bulk actions bar (#169)
## 🚪 Why?

### Problem
The bulk actions bar lost the \"Select all matching filters\"
capability, limiting users to acting only on the currently
visible/selected page of transactions. Users had no way to apply bulk
operations (category, labels, re-evaluate rules) to all transactions
matching their active filters.

## 🔑 What?

### Changes
- Re-adds a \"Select all matching filters\" icon button (with tooltip)
to the bulk actions bar — clicking it switches into a \"selecting all\"
mode
- When selecting all: the count label is replaced by a `CheckCheck` icon
with tooltip \"All matching filters\"; the Delete action is hidden
(backend-only safety)
- Bulk category and label updates in \"select all\" mode send `filters`
to `PATCH /transactions/bulk` and show a loading → success toast with
the updated count
- Re-evaluate rules in \"select all\" mode sends `filters` to the
re-evaluate bulk endpoint instead of `transaction_ids`
- Extends `BulkReEvaluateRulesRequest` and
`ReEvaluateTransactionRulesJob` to accept and apply a `filters` param
- Adds Spanish translations for new strings
- Wraps transaction count text in `whitespace-nowrap` to prevent it
splitting across two lines

##  Verification

### Tests
- 2 new tests added in
`tests/Feature/ReEvaluateTransactionRulesTest.php`: verifies controller
passes filters to job, and job correctly scopes transactions by filters
- All 16 tests in `ReEvaluateTransactionRulesTest` pass
2026-03-01 10:27:43 +00:00
Víctor Falcón eda72d4304
feat(rules): move automation rule evaluation to the backend (#168)
## 🚪 Why?

### Problem

Automation rule evaluation was happening entirely on the frontend,
relying on the client to fetch rules, loop over transactions, and apply
category/label/note changes. This meant evaluation was tied to
browser-side decryption logic, couldn't be triggered server-side, and
had no reliable progress reporting for bulk operations.

## 🔑 What?

### Changes

- **Single re-evaluation**: `POST
/transactions/{transaction}/re-evaluate-rules` applies all matching
rules immediately and returns the updated transaction; the frontend
updates local state from the response.
- **Bulk re-evaluation**: `POST /transactions/re-evaluate-rules`
dispatches a queued job and returns a `job_id`; the frontend polls `GET
/transactions/re-evaluate-rules/status/{jobId}` ~every second, updates a
progress toast, and refreshes the list when done.
- **Note support**: `AutomationRuleService` now applies plain
(unencrypted) `action_note` actions; encrypted notes are skipped since
the backend cannot decrypt them.
- **Dirty-only saves**: `AutomationRuleService::applyActions()` now
tracks whether any field changed and only calls `saveQuietly()` when
needed, avoiding unnecessary writes.
- **Encrypted transactions skipped**: Transactions with `description_iv`
are excluded from bulk queries and silently skipped on single
re-evaluation (existing behavior preserved).
- **Frontend cleanup**: Removed `evaluateRules`,
`appendNoteIfNotPresent`, and related crypto imports from
`transaction-list.tsx`, `index.tsx`,
`use-re-evaluate-all-transactions.tsx`, and
`transaction-actions-menu.tsx`.

##  Verification

### Tests

- New: `tests/Feature/ReEvaluateTransactionRulesTest.php` — 14 passing
tests covering:
  - Single re-evaluation applies category, label, and note
  - Single re-evaluation skips encrypted transactions
  - Bulk job dispatched and returns 202 with job_id
  - Bulk status polling returns `processing` and `done`
- Partial bulk (specific transaction_ids) only processes selected
transactions
  - Encrypted transactions excluded from bulk processing
  - Unauthenticated requests are rejected (401)

### Manual Verification

- Row dropdown "Re-evaluate rules" calls backend and updates the row in
place
- Bulk "Re-evaluate All Expenses" dispatches job, shows progress toast,
refreshes list on completion
- Encrypted transactions are silently skipped in both flows
2026-03-01 10:37:12 +01:00
Víctor Falcón cd0da10014
fix(i18n): fix missing space after Tracking label and add account/accounts Spanish translations (#167)
## Why

### Problem

Two small locale bugs in the Spanish UI:

1. **Budgets — Tracking label**: The translated string `Seguimiento:`
was rendered immediately adjacent to the tracking label value (e.g.
`Seguimiento:Padel`) due to a missing whitespace between the two JSX
expressions.
2. **Settings > Connections — account count**: The lowercase keys
`account` and `accounts` used for the singular/plural account count
badge (`ES · 1 account`) had no Spanish translations, so the English
fallback was always shown instead of `1 cuenta` / `X cuentas`.

## What

- `resources/js/components/budgets/budget-list-card.tsx`: Added `{' '}`
between `{__('Tracking:')}` and `{trackingLabel}` to ensure a space is
always rendered.
- `lang/es.json`: Added missing lowercase translation keys `"account":
"cuenta"` and `"accounts": "cuentas"`.

## Verification

### Tests

No dedicated tests exist for rendered translation strings. Changes are
mechanical (whitespace and JSON key additions) with no logic involved.

### Manual

- Switch locale to Spanish (`es`).
- Navigate to Budgets — confirm the tracking label reads `Seguimiento:
Padel` (with space).
- Navigate to Settings > Connections — confirm the account count reads
`1 cuenta` / `X cuentas`.
2026-02-28 22:53:28 +01:00
Víctor Falcón 39a47ec23f
feat(cashflow): promote trend chart above money flow and increase height (#166)
## Why

The Cashflow Trend chart was buried at the bottom of the page, making it
easy to miss despite being a high-value overview of income, expenses,
and net cashflow over time.

## What

- Moved the **Cashflow Trend** chart to appear immediately after the
summary cards, before the **Money Flow** (Sankey) chart
- Increased the chart height from `h-[250px]` to `h-[350px]` (both the
rendered chart and loading skeleton) to give it more visual weight,
closer to other prominent charts in the app

## Verification

### Tests
No logic changes — purely layout and sizing adjustments to existing
components.

### Manual
- Cashflow page now shows: Summary Cards → Cashflow Trend → Money Flow →
Breakdown Cards
- Trend chart renders taller and is more immediately visible on page
load
2026-02-28 18:53:02 +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 9bb835e79b
fix(categorizer): fetch uncategorized transactions from backend instead of IndexedDB (#165)
## Problem

The categorizer was reading transactions exclusively from the browser's
IndexedDB (Dexie), which is only populated via an on-demand sync
triggered from the import drawer. When an account was deleted
(soft-deleting all its transactions), those transactions remained in the
local IndexedDB and kept appearing in the categorizer on subsequent
visits.

## Solution

- **`TransactionController::categorize()`** now queries uncategorized
transactions directly from the database (`WHERE category_id IS NULL`)
with their account and bank eager-loaded, and passes them as an Inertia
prop alongside the existing `categories`, `accounts`, `banks`, and
`labels`.
- **`categorize.tsx`** drops all Dexie/`useLiveQuery` dependencies. The
transaction list now comes from the Inertia prop; client-side decryption
runs once on mount (the encryption key remains client-side only — the
server never sees it). Category assignment
(`transactionSyncService.update()`) is unchanged.

## Tests

Added 5 new feature tests covering:
- Categorize page is accessible to authenticated users and includes the
`transactions` prop
- Only uncategorized transactions are returned
- Transactions from deleted accounts are excluded
- Transactions from other users are not visible
- Guests are redirected to login
2026-02-28 16:30:54 +00:00
Víctor Falcón e01d62ffd4
fix(accounts): widen bank column and truncate text on mobile (#163)
## Why

### Problem

On mobile, the accounts settings table bank column was too narrow,
causing bank names to overflow or wrap awkwardly and overlap adjacent
columns (e.g. the Type badge).

## What

### Changes

- Set `min-w-32` on the bank cell container to give the column a
guaranteed minimum width on small screens
- Added `shrink-0` to the bank logo so it doesn't get squished
- Added `truncate` to the bank name text so long names are clipped with
an ellipsis instead of overflowing

## Verification
<img width="384" height="394" alt="image"
src="https://github.com/user-attachments/assets/4a96642f-b601-41e5-a350-df43073672a7"
/>
2026-02-28 16:09:24 +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 0c5ba916bf
Add chart color scheme setting (#101)
## Summary

- Add a new **Chart color scheme** dropdown in Settings > Appearance
allowing users to choose between 4 color palettes: **Colorful** (new
default), **Neutral** (previous zinc grayscale), **Blue**, and **Pink**
- Persist the setting in a new `user_settings` table with instant
client-side feedback via localStorage + cookie
- All charts across dashboard, categories, budgets, and cashflow update
instantly when switching schemes
- Reduced colorful palette intensity (shifted from 500/600 to 300/400
range) for lower contrast
2026-02-28 12:58:21 +01:00
Víctor Falcón 79dd24b23e
fix(ux): improve status badge, hide balance update for connected accounts, localize delete confirm (#159)
## Why

### Problem

Three UX issues were identified:

1. **Connection status badge looked like a button** — the `<Badge>`
component has interactive-looking styles that made it visually ambiguous
as a status indicator rather than a purely informational element.
2. **"Update balance" was shown for bank-connected accounts** — accounts
synced via Open Banking have their balances managed automatically;
showing a manual update button was misleading and could create
confusion.
3. **Delete confirmation word was not localized** — the dialog required
typing `DELETE` even when the UI was in Spanish, where the placeholder
correctly said `ELIMINAR`. The check was hardcoded to `'DELETE'`.

## What

### Changes

<img width="756" height="407" alt="image"
src="https://github.com/user-attachments/assets/4866edf7-5329-486a-9f72-fcd69429f784"
/>
<img width="757" height="424" alt="image"
src="https://github.com/user-attachments/assets/bc9bda54-e0a4-402b-be32-a9fe1897cfd5"
/>
2026-02-27 22:37:16 +00:00
Copilot a4d2100459
fix: prevent gain/loss sign from wrapping off the amount (#158)
The `+`/`-` sign and the formatted amount are separate text nodes inside
the same `<span>`, allowing the browser to break between them when
horizontal space is tight — leaving the sign orphaned on a separate
line.

## Changes

- **`account-balance-chart.tsx`, `account-list-card.tsx`,
`account-balance-card.tsx`**: Added `whitespace-nowrap` to the gain/loss
amount `<span>` in all three components

```diff
- className={`text-right font-mono tabular-nums ${...}`}
+ className={`whitespace-nowrap text-right font-mono tabular-nums ${...}`}
```

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>The gain/lost amount is broken in two lines by the symbol
when there is no enough space</issue_title>
>
<issue_description>![ResizedImage_2026-02-25_18-12-15_5005.png](https://github.com/user-attachments/assets/70285291-ceca-4116-ad05-df8e97007e41)
> 
> </issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes whisper-money/whisper-money#157

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: victor-falcon <238766+victor-falcon@users.noreply.github.com>
2026-02-26 09:15:23 +01:00
Víctor Falcón e718f5df5c
fix: improve connection error message contrast in dark mode (#155)
## Summary

- Uses `text-destructive-foreground` in dark mode for the connection
error icon and message text, replacing `text-destructive` which was too
dim against the dark background.
2026-02-25 13:56:26 +01:00
Víctor Falcón 690be20f21
feat(open-banking): add update credentials flow for API-key connections (#154)
## Why

### Problem

When API tokens for Indexa Capital, Binance, or Bitpanda expire or are
revoked, syncs fail silently with 401/403 errors. Users have no way to
replace their credentials without disconnecting and recreating the
entire connection (losing account mappings and history).

## What

### Changes

- **Auth failure email notification**: on the final retry attempt (3rd
of 3), if a sync job fails with 401/403 for an API-key provider, an
email is sent to the user with a link to the connections settings page
- **Update credentials endpoint**: `PATCH
/settings/connections/{connection}/credentials` validates new
credentials against the provider API before saving, then triggers a sync
- **Update credentials dialog**: provider-specific form (API token for
Indexa/Bitpanda, API key + secret for Binance) shown via an "Update
Credentials" button in the error banner and dropdown menu
- **Mailable**: `BankingConnectionAuthFailedEmail` follows existing
patterns (queued, rate-limited, Markdown template)
- **Form request**: `UpdateConnectionCredentialsRequest` with dynamic
validation rules per provider and authorization check

### Files changed

| File | Change |
|------|--------|
| `app/Jobs/SyncBankingConnectionJob.php` | Send auth failed email on
final attempt for auth errors on API-key providers |
| `app/Mail/BankingConnectionAuthFailedEmail.php` | New queued mailable
|
| `resources/views/mail/banking-connection-auth-failed.blade.php` |
Email template |
| `app/Http/Controllers/OpenBanking/ConnectionController.php` |
`updateCredentials()` action with provider credential validation |
| `app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php`
| Dynamic validation per provider |
| `routes/settings.php` | PATCH route for credential updates |
| `resources/js/components/open-banking/update-credentials-dialog.tsx` |
Dialog with provider-specific fields |
| `resources/js/pages/settings/connections.tsx` | Update Credentials
button in error state and dropdown |

## Verification

### Tests

- **12 new tests** across 2 test files, all passing:
- `SyncBankingConnectionJobTest`: 5 tests covering email sent on final
retry (Indexa 401, Binance 403), not sent before final retry, not sent
for non-auth errors, not sent for EnableBanking
- `ConnectionControllerTest`: 7 tests covering valid credential update
for each provider, invalid credentials, EnableBanking rejection,
authorization, feature flag, required field validation
- Full OpenBanking test suite: **145 tests, 473 assertions** passing
2026-02-25 13:41:24 +01:00
Víctor Falcón 255033999d feat: Update facehash and enable blink 2026-02-24 15:54:49 +01:00
Víctor Falcón ae81e20a66
perf(dashboard): optimize query performance and eliminate redundant requests (#146)
## Why

### Problem

The dashboard page takes ~4 seconds to load for users with many balance
records. Profiling revealed 29 queries across the initial page load + 3
separate API calls, a 576 KB wasted payload from unused bank records,
and a critical 3.7s bottleneck in `BalanceLookup` correlated subqueries.

## What

### Changes

**Query optimizations:**
- Replace correlated `MAX()` subqueries in `BalanceLookup` with
derived-table `joinSub` pattern — **48x faster** (3,693ms → 77ms) on
accounts with thousands of balance records
- Replace `whereHas` (EXISTS subqueries) with JOINs in
`DashboardAnalyticsController` and `CashflowAnalyticsController` — ~3x
faster per query
- Cache encryption check results in `HandleInertiaRequests` middleware
to avoid 2 duplicate queries per request

**Payload & request reduction:**
- Remove dead `banks` query from `DashboardController` (2,365 records,
576 KB never used by frontend)
- Remove duplicate `categories` and `accounts` queries already provided
by middleware shared props
- Consolidate 3 separate `fetch()` API calls into Inertia v2
`Inertia::defer()` props grouped under `'dashboard'` (single follow-up
request with skeleton fallbacks)

**Frontend:**
- Replace `useDashboardData` fetch hook with `usePage()` props +
`<Deferred>` components
- Convert `CashflowSummaryCard` from internal fetch to prop-based
- Use `router.reload({ only: [...] })` for balance update refetch

### Performance summary

| Metric | Before | After |
|--------|--------|-------|
| Initial page queries | 16 | 12 |
| Follow-up HTTP requests | 3 separate API calls | 1 Inertia deferred |
| BalanceLookup time | 3,693ms | 77ms |
| Wasted payload | 576 KB | 0 |

## Verification

### Tests

All 633 feature tests pass, including 40 dashboard/cashflow-specific
tests.
2026-02-24 08:43:48 +01:00
Víctor Falcón ce9574aa14
perf(accounts): replace client-side API calls with Inertia deferred prop (#144)
## Why

The `/accounts` page was making two extra client-side API calls via the
`useDashboardData()` hook:
- `/api/dashboard/net-worth-evolution` (~14.5s in dev)
- `/api/dashboard/top-categories` (~119ms, completely unused on this
page)

The backend logic itself is fast (~34ms), but the full middleware stack
cost of extra HTTP roundtrips in the dev environment was adding
significant overhead.

## What

### Backend (`AccountController.php`)
- Added `ExchangeRateService` constructor injection
- Added `Inertia::defer()` prop `accountMetrics` to the `index()` action
- Added `getAccountMetrics()`, `formatMonth()`, `convertBalance()`
private methods
- Uses `BalanceLookup::forAccounts()` for efficient batch loading (2 SQL
queries regardless of account count)

### Frontend (`Accounts/Index.tsx`)
- Removed `useDashboardData()` hook import and usage
- Added `accountMetrics` as an optional deferred prop (undefined until
resolved)
- Loading state derived from `!accountMetrics`, which naturally drives
the existing `loading` prop on `AccountListCard` (skeleton UI already
existed)
- Added `handleBalanceUpdated` callback using `router.reload({ only:
['accountMetrics'] })` for targeted refresh

## Verification

### Tests
- 3 new Pest tests covering deferred prop behavior:
- `accounts index defers account metrics` — verifies metrics are missing
from initial response, present after `loadDeferredProps()`, with correct
balance values
- `accounts index deferred metrics includes invested amount for
investment accounts` — verifies invested amount for investment account
types
- `accounts index deferred metrics returns null invested amount for
non-investment accounts` — verifies non-investment accounts get null
invested amount
- All 15 tests in `AccountControllerTest.php` pass
2026-02-23 20:08:41 +01:00
Víctor Falcón 0a9ca5b606
feat: enable invested amount tracking for savings accounts (#142)
## Summary

- Adds `Savings` to the list of account types that support invested
amount tracking, alongside `Investment` and `Retirement`
- Decouples `supportsInvestedAmount()` from
`NON_TRANSACTIONAL_ACCOUNT_TYPES` in TypeScript, since savings accounts
are transactional but now also support invested amount tracking
- Updates unit tests to reflect savings accounts supporting invested
amounts

This enables remunerated savings accounts to track invested money vs.
current balance to visualize gains/losses — the same way investment and
pension accounts already do.
2026-02-23 17:19:33 +01:00
Víctor Falcón 299b8a56d8
feat: investment benefits — show gains/losses on investment accounts (#140)
## Why

Investment and retirement accounts show balance over time, but there's
no way to see how much money was actually put in versus how much is
current value. Users can't tell at a glance whether their investments
are up or down.

## What

Adds an "invested amount" tracking system across the full stack:

**Backend**
- New `invested_amount` column on `account_balances` (nullable
bigInteger, cents, per-date)
- Auto-sync from providers: Indexa Capital (instruments_cost +
cash_amount), Bitpanda (fiat deposit/withdrawal history), Binance
(90-day windowed deposit/withdrawal with crypto→fiat conversion)
- Manual input support via Update Balance dialog
- Historical invested amount data in all balance evolution APIs (net
worth, account detail)

**Frontend**
- Dashed line on sparkline charts (dashboard + accounts page) showing
per-point historical invested amount alongside balance
- Dashed line on account detail charts (daily AreaChart + monthly
ComposedChart)
- Tooltips with labeled rows: Balance, Invested, Gain/loss (color-coded)
- Invested amount column in balances history modal
- Invested amount field in balance import wizard (CSV mapping)
- Demo account seeder updated with invested amount data

## Screenshots
<img width="1301" height="750" alt="image"
src="https://github.com/user-attachments/assets/0f05ecd0-8b98-47b4-9fa4-027f0311e3bb"
/>
<img width="744" height="374" alt="image"
src="https://github.com/user-attachments/assets/c4daa816-dee0-4f94-957f-317a13bc80d5"
/>
<img width="1267" height="738" alt="image"
src="https://github.com/user-attachments/assets/21df350c-6954-4ff5-8b3c-b858df3a8b3a"
/>
<img width="1301" height="828" alt="image"
src="https://github.com/user-attachments/assets/16f5f021-a926-4e8e-a999-c4ca32d1ea3d"
/>
<img width="1274" height="845" alt="image"
src="https://github.com/user-attachments/assets/62f2dfc0-04f0-4bdb-b072-cf7cd1be77d3"
/>
2026-02-23 13:59:10 +01:00
Víctor Falcón faddd59537 fix: Pricing table on dark scheme 2026-02-22 17:29:42 +01:00
Víctor Falcón c03f576758
fix(cashflow): prevent breakdown cards overflow on mobile (#139)
## Why

### Problem

The cashflow breakdown cards (Income Sources / Expense Categories)
overflow the viewport width on mobile devices. The row items (icon,
category name, trend indicator, percentage, amount) don't fit within the
narrow mobile viewport because all non-name elements are `shrink-0`,
leaving insufficient space.

Additionally, a leftover `bg-red-500` debug class was present on the
breakdown cards grid container.

## What

### Changes

- Restructured breakdown card rows into two groups (label side / data
side) with `max-w-[60%]` on the label group so the category name
truncates with ellipsis before pushing data off-screen
- Added `overflow-hidden` to the row flex container to prevent content
spilling
- Hidden the percentage text label on mobile (`hidden sm:inline`) — the
progress bar still conveys this visually
- Removed leftover `bg-red-500` debug class from the cashflow page grid
- Switched breakdown cards grid to explicit `grid-cols-1` on mobile for
clarity

## Verification

### Manual

Tested visually on mobile viewport — breakdown cards now fit within the
screen width, matching the Dashboard's TopCategoriesCard behavior.
2026-02-20 11:22:05 +01: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 880b27675c
fix(charts): use settings popover for chart controls on mobile (#137)
## Why

### Problem

On mobile screens, both the Net Worth Evolution chart (Dashboard) and
the Balance Evolution chart (Account page) have the Daily/Monthly toggle
button plus 3 chart type icons competing for horizontal space in the
card header, causing overflow and a cramped layout.

## Screenshots
<img width="548" height="554" alt="image"
src="https://github.com/user-attachments/assets/7b1e7b9b-4cb9-47d1-8c69-0e140682d93d"
/>
<img width="1426" height="2932" alt="image"
src="https://github.com/user-attachments/assets/ed3c7163-4690-4f4e-8dce-445fbad53936"
/>
2026-02-19 16:53:00 +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 126f7f7e72
feat: Add daily balance chart with area visualization for account pages (#135)
## Summary

- Adds a **Monthly / Daily granularity toggle** to the account balance
evolution chart on account pages
- Daily view renders as an **area chart** (line with gradient fill)
showing the last 30 days, while monthly keeps the existing bar chart
- All 3 chart view modes (stacked, period-over-period,
period-over-period %) work in both granularities
- Chart view toggle **tooltips update dynamically** based on granularity
(e.g. "Month over month change" → "Day over day change")
- Backend fills gaps in sparse `account_balances` data by carrying
forward the last known balance day-by-day
- Includes 3 new Pest tests for the daily endpoint (data points,
gap-filling, forbidden access)

## Screenshot
<img width="1271" height="743" alt="image"
src="https://github.com/user-attachments/assets/cca7a7ad-bdcc-45d2-a5e1-47b1cb855d49"
/>
<img width="1274" height="755" alt="image"
src="https://github.com/user-attachments/assets/14e1f2d8-41cd-49cc-b0f4-307edb847af8"
/>
2026-02-19 14:55:05 +01:00
Víctor Falcón a0d19aef81
feat: Add 'Today' marker on budget spending chart (#134)
## Summary

- Adds a vertical dashed reference line on the budget spending chart
indicating the current day within the period
- The marker only appears when today falls within the budget period date
range
- Adapts to both display modes: uses the day index (`Day N`) when a
previous period is shown, or the ISO date when showing a single period

## Screenshot

<!-- Add screenshot here -->
2026-02-19 11:48:18 +01:00
Víctor Falcón a2b1e91b49
fix: Prevent account card content overflow on long names (#133)
## Summary

- Adds proper flex overflow constraints (`min-w-0`, `shrink-0`) to the
account list card so long account names truncate with ellipsis instead
of overflowing the container.
- Adds `whitespace-nowrap` to the trend indicator component to keep "vs
last month" always on a single line.

## Changes

### `account-list-card.tsx`
- Added `min-w-0` to the left-side flex containers (account name area)
so the flexbox allows shrinking below content width
- Added `shrink-0` to the BankLogo and the right-side balance container
to prevent them from being compressed

### `amount-trend-indicator.tsx`
- Added `whitespace-nowrap` to the trend indicator wrapper so "vs last
month" always stays on a single line
2026-02-19 10:22:51 +01:00
Víctor Falcón fe76c2e43d
feat: Add Bitpanda exchange integration (#132)
## Summary

- Add Bitpanda as a new exchange provider using a single API key
(`X-Api-Key` header) to sync crypto and fiat wallet balances
- Follow the same architecture as Binance: provider string on
`banking_connections`, dedicated API client, balance sync service,
controller, form request, job wiring, factory state, route, and frontend
dialog
- Convert crypto wallet balances to the user's target currency via
`CurrencyConversionService`; fiat wallets are added directly or
converted if in a different currency
- Bitpanda appears in all countries in the connect dialog (same as
Binance)
- 18 new tests covering controller validation, balance sync scenarios,
sync job delegation, expiry handling, and email suppression
2026-02-19 09:26:31 +01:00
Víctor Falcón df9fc38562
feat: Add Binance integration (#131)
## Summary

- Adds Binance as a third banking provider alongside EnableBanking and
Indexa Capital
- Binance appears in the institution list for **all countries**, with
the full list sorted alphabetically
- Creates a single "Crypto Portfolio" investment account with total
portfolio value converted to the user's preferred currency
- Supports direct fiat pairs (e.g. BTCEUR), USD stablecoin 1:1 mapping,
and USDT fallback conversion

## Changes

- **Migration**: adds encrypted `api_secret` column to
`banking_connections`
- **BinanceClient**: HMAC-SHA256 authenticated API client for account
data and ticker prices
- **BinanceBalanceSyncService**: converts all non-zero balances to fiat
via direct pairs or USDT fallback
- **BinanceController + ConnectBinanceRequest**: validates credentials,
creates connection and single account
- **SyncBankingConnectionJob**: new `syncBinance()` branch
- **AccountMappingController**: Binance uses Investment account type
- **Frontend**: Binance institution for all countries, API Key + Secret
form fields, alphabetically sorted list
- **Factory**: `binance()` state on `BankingConnectionFactory`

## Test plan

- [x] `BinanceControllerTest` — 6 tests (valid connection, invalid
credentials, account-mapping flag, feature flag, validation, user
currency)
- [x] `BinanceBalanceSyncTest` — 7 tests (direct EUR pair, USDT
fallback, USD stablecoins, locked balances, same-date update, empty
balances, missing external ID)
- [x] Full test suite passes (545 tests)
- [x] Manual: open connection dialog → select any country → Binance
appears alphabetically → select Binance → API Key + Secret form →
connect
2026-02-18 15:23:46 +01:00
Víctor Falcón d7f0084338 fix: Discord link 2026-02-18 13:00:30 +01:00
Víctor Falcón 3f541ca4d6
feat: Add Indexa Capital integration (#130)
## Summary

- Allow users to connect their Indexa Capital (Spanish robo-advisor)
account via API token
- Adds "Indexa Capital" option when selecting Spain in the bank
connection dialog
- Token-based auth flow: user enters API token (instead of OAuth
redirect), validated against Indexa API, stored encrypted
- Syncs portfolio balance only (no transactions) from the
`/accounts/{id}/performance` endpoint
- Accounts created as `Investment` type
- Reuses existing account mapping flow when the `account-mapping`
feature flag is active
- Disconnect flow skips session revocation for Indexa (no OAuth session
to revoke)

## New files
- `IndexaCapitalClient` — HTTP client for Indexa Capital API
(`X-AUTH-TOKEN`)
- `IndexaCapitalBalanceSyncService` — Syncs portfolio value into
`account_balances`
- `IndexaCapitalController` — Token validation + connection creation
- `ConnectIndexaCapitalRequest` — Form request validation
- Migration adding encrypted `api_token` column to `banking_connections`

## Test plan
- [x] 6 controller tests (connect, invalid token, mapping, feature flag,
validation, multi-account)
- [x] 4 balance sync tests (sync, update, skip, missing field)
- [x] 3 sync job tests (balance-only, no expire, no email for Indexa)
- [x] 1 disconnect test (no revokeSession for Indexa)
- [x] All 530 existing tests still pass
2026-02-18 10:42:13 +01:00
Víctor Falcón 202835f76e fix: Accounts name on settings/account 2026-02-18 10:09:04 +01:00
Víctor Falcón f8e8fc4db1
Fix horizontal scroll on transactions table (#127)
## Summary
- Make the description column fill all remaining space on md+ screens by
replacing fixed `max-w` breakpoints with `md:w-full md:min-w-0
md:overflow-hidden`
- Eliminates horizontal scrolling on md, lg, and xl viewports without
modifying other columns

## Test plan
- [x] Verify no horizontal scroll on md (768px), lg (1024px), and xl
(1280px) viewports
- [x] Verify description text truncates correctly with ellipsis
- [x] Verify mobile/sm behavior is unchanged (max-w-[200px] /
sm:max-w-[400px])
- [x] Verify labels and notes still display within the description cell
2026-02-17 08:45:07 +01:00
Víctor Falcón d1efc03fc5
Move transaction filtering from client-side to server-side (#126)
## Summary

- **Server-side filtering/sorting/search**: With encryption removed, all
transaction filtering, sorting, and full-text search (LIKE on
description + notes) now happens via SQL queries instead of client-side
IndexedDB with decryption
- **Cursor pagination**: Transactions are returned with cursor-based
pagination (default 50 per page, "Load More" pattern) instead of loading
all into IndexedDB
- **Removed IndexedDB dependency**: The main transactions page no longer
depends on Dexie/IndexedDB for data or sync — other pages (categorize,
accounts/show, budgets/show) are unaffected and will be migrated
separately

## Changes

| File | Change |
|------|--------|
| `app/Models/Transaction.php` | Added `scopeApplyFilters` query scope |
| `app/Http/Requests/IndexTransactionRequest.php` | New form request for
filter validation |
| `app/Http/Controllers/TransactionController.php` | Server-filtered
cursor-paginated index, refactored bulkUpdate to reuse scope |
| `resources/js/pages/transactions/index.tsx` | Major rewrite —
server-driven filtering, sorting, pagination via `router.visit()` |
| `resources/js/components/transactions/transaction-filters.tsx` |
Search always enabled (removed encryption key dependency) |
| `resources/js/types/transaction.ts` | Added `ServerTransaction` type |
| `resources/js/contexts/sync-context.tsx` | Simplified — removed
transaction sync service |
| `tests/Feature/TransactionFilterTest.php` | 20 new Pest tests for all
filter scenarios |

## Test plan

- [x] Run `php artisan test` — all 523+ tests pass
- [x] Navigate to `/transactions` — paginated results load from server
- [x] Apply each filter type (date, amount, category, account, label,
search) and verify server round-trip
- [x] Test "uncategorized" category filter
- [x] Test combined filters
- [x] Test sorting by date, amount, description (ascending/descending)
- [x] Test "Load More" button for cursor pagination
- [x] Test bulk operations (select some → update, select all filtered →
update)
- [x] Verify URL persistence (refresh page with filters in URL)
- [x] Test search on both description and notes fields
2026-02-16 12:49:04 +01:00
Víctor Falcón 6abec95d0e
feat: Decrypt encrypted transactions on key unlock (#123)
## Summary

- Add `GET /api/transactions?encrypted=true` endpoint for paginated
listing of encrypted transactions
- Add `PATCH /api/transactions/bulk` endpoint for batch-updating
decrypted transaction data (max 50 per request, bypasses model events)
- Add `useDecryptTransactions` hook that mirrors the existing account
name decryption flow: fetches encrypted transactions page-by-page,
decrypts `description`/`notes` client-side via Web Crypto API, sends
plaintext back and clears IVs
- Wire the hook into `AppSidebarLayout` so decryption runs automatically
when the user unlocks their encryption key
- Once all transactions (and accounts) are decrypted, the encryption key
button in the header disappears automatically

## Test plan

- [x] 11 Pest tests covering encrypted/plaintext filtering, pagination,
user scoping, bulk update, authorization, validation, nullable notes,
guest access, and no-model-events behavior
- [x] Manual: create encrypted transactions, unlock encryption key,
verify transactions get decrypted and the key button disappears
2026-02-16 10:37:43 +01:00