Commit Graph

504 Commits

Author SHA1 Message Date
Víctor Falcón fc2ae4fa3e feat(periods): gate custom month start day behind feature flag
- add CustomMonthStartDay Pennant feature (off by default)
- hide the Start of month setting in account settings unless enabled
- only validate month_start_day when the feature is active so other
  profile updates keep working while the option is hidden
- share the flag to the frontend and cover both enabled/disabled paths
2026-06-11 20:32:01 +01:00
Víctor Falcón 4557ef5065 fix(periods): resolve current period in user timezone and align period end
- compute current/trend now() against user timezone so period
  boundaries are correct near midnight and month edges
- make end_inclusive endOfDay to match frontend and stay correct
  if compared against datetime columns
- add user-periods.ts unit tests covering custom-start-day month,
  quarter, and year ranges, shifting, and boundary equality
2026-06-11 20:31:11 +01:00
Víctor Falcón 2762acb240 feat(periods): add custom month start day
Allow users to configure the day their monthly reporting period starts so dashboard and cashflow ranges match their budget cycle.
2026-06-11 20:31:11 +01:00
Víctor Falcón 9c3c4d573e
feat(analysis): per-category 12-month spending drawer (#519)
## Summary

Adds a **category-analysis drawer** so you can see how much you spend on
a single category, month by month, over the last 12 months — answering
"am I spending more or less on food delivery?".

Reachable via an **Analyze** button on three cards:
- Dashboard → Top spending categories
- Cashflow → Income Sources
- Cashflow → Expense Categories

Each button opens a shared drawer with a full-width category chooser
(all categories) and a 12-month stacked bar chart.

## Chart semantics
- X = last 12 rolling months (gaps filled with zero). Y = user display
currency.
- Stacked segments = the chosen category's immediate children
(grandchildren rolled in) + a **Direct** segment (spend on the parent
itself) + an **Other** segment (children beyond the top 6, ranked by
total).
- A leaf category renders a single series named after the category.
- Amounts convert to the user currency and **net per month**, oriented
by category type so the expected direction reads as a positive bar; an
against-grain month (e.g. refunds > spend) dips below zero.
- Colors use the theme-aware chart palette (same as the net-worth
chart).

## Summary stats
Two glowing cards above the chart: **Monthly average** and **Trend**
(recent 6-month average vs earlier 6-month average; null when there's no
earlier baseline).

## Persistence
Each widget remembers its own chosen category in `localStorage`
(category only), prefilling the first category of its list otherwise; a
remembered-but-deleted category falls back gracefully.

## Tests
- 11 Pest feature tests (rollup, Direct/Other, net-with-refunds,
12-month window, currency conversion, income orientation, summary
average + trend).
- 5 vitest tests for the prefill/persistence precedence.
- es.json keys added.
2026-06-11 09:52:53 +02:00
Víctor Falcón 49acc8a884
fix(analysis): truncate long breakdown names so amounts stay in widget (#518)
## What

Long category/label names in the shared bar-list breakdowns (cash-flow
top expenses/income, dashboard top categories, analysis drawer
categories/tags/accounts) spilled the amount out past the widget's right
edge.

## Why

The name span already had `truncate min-w-0 flex-1`, but its `grow` row
wrapper (the `<Link>`/`<div>`) had no `min-w-0`. Flex items default to
`min-width: auto`, so the wrapper refused to shrink below its content —
the inner `truncate` never engaged.

## Change

Add `min-w-0` to both `grow` wrappers so the row can shrink and the
existing `truncate` clamps the name with an ellipsis, keeping the amount
inside the widget.

Visual-only CSS class change.
2026-06-10 21:58:36 +02:00
Víctor Falcón bcd025f1b1
feat(analysis): shared bar-list breakdowns in transaction drawer (#517)
## What

Unifies the "top categories"-style bar lists behind one reusable
component and applies it to the transaction **analysis drawer**.

### Shared component
New `resources/js/components/shared/category-breakdown-list.tsx` —
generic `CategoryBreakdownRow<T>` + adapter (leading marker, truncated
name, optional trend + %, amount, proportional bar, expand/collapse
recursion). Now used by:
- Dashboard **Top spending categories** (was a local `CategoryRow`)
- Cash-flow **Income Sources / Expense Categories** (was a local
`BreakdownRow`)
- Analysis drawer breakdowns

### Analysis drawer
- **Top categories** — pie+list → bar list, now with **expand/collapse**
of subcategories (children already in the payload, served through
`useExpandableCategories` with no extra fetch).
- **Top tags** — recharts bar → bar list, colour-dot leading marker.
- **Top accounts** — plain list → bar list, with the **bank/account
logo** as the leading marker instead of a category icon.

### Largest expenses table
Long category names pushed the amount onto two lines (minus sign split
from the digits). Category + description columns are now capped to the
same width, the chip truncates, and the amount cell is
`whitespace-nowrap`.

### Backend
`icon` added to the category breakdown payload (parent, child,
Uncategorized) so the drawer can render the icon circle.

## Tests
- Drawer: category expand/collapse + tag/account render tests.
- Backend: asserts `color`/`icon` on the category breakdown.
- Full JS suite (191) + analysis feature tests green; pint / eslint /
prettier clean.

## Note
Dashboard card has no test file — refactor preserves its public
behaviour; worth a visual check.
2026-06-10 12:36:20 +02:00
Víctor Falcón 21f8f3b277
fix(analysis): align summary card amounts to a common baseline (#515)
## What

In the transaction **analysis drawer**, the summary cards' amounts
weren't lining up horizontally: the **Avg / day** card's label row holds
the day-editor button (a 24px icon button), making that row taller than
a plain text label, so its amount sat lower than **Total spent**.

## Fix

Give every summary card's label row a fixed `h-6` (matching the
icon-button height), so all label rows are the same height and the
amounts share a common baseline. Amounts were already the same size
(`text-lg`); this just equalizes the vertical space above them.

Applies to all summary tiles — expense mode (Total spent / Avg per day)
and income mode (Income / Expenses / Net / Margin).

## Before / After
- Before: Avg/day amount offset below Total spent.
- After: both amounts aligned on the same row.

Frontend tests (9) pass; eslint + prettier clean.
2026-06-09 16:09:07 +02:00
Víctor Falcón 7cc49a5368
feat(analysis): project-aware transaction analysis (#513)
## What

Reworks the filtered **transaction analysis drawer** to behave like a
*project view* — a coherent set of related transactions (a trip, a
rental, a renovation, a side hustle) — and surfaces data that was
already computable but never shown.

### Two shapes, auto-detected
The drawer adapts to whether the set is **expense-only** or **income +
expense**, detected from the income share (`income >= 15% of expense`).
A stray refund won't flip a trip into a P&L view; a rental's rent will.

- **Expense mode:** total spent, daily average, cumulative spend line.
- **Income mode:** net result + margin %, income vs expense bars,
cumulative **net** line.

The mode can be overridden per filter (Auto / Expenses only / Income &
expenses), mirroring the existing daily-average override exactly:
remembered in the browser per filter fingerprint and synced to a
matching saved filter via a new `analysis_mode` column + PATCH endpoint.

### New panels (all from existing data)
- **Largest expenses** — top 5, expandable to 10, biggest spends first.
- **Spending by payee** — horizontal bars, gated to ≥2 named payees.
- **Spending by account** — list, gated to ≥2 accounts.

### Smarter table
The largest-expenses table hides any column the filter or the rows have
pinned to a single value (e.g. filtering by one label drops the Labels
column; a one-category result drops the Category column).

## Tests
- **Backend:** new Pest coverage for `largest_expenses`, payee/account
breakdowns, `cumulative_net`, and the `analysis_mode` endpoint
(`TransactionAnalysisTest`, `SavedFilterTest`) — all green.
- **Frontend:** 9 vitest cases covering mode detection/override, the day
override, and column hiding.
- New `__()` keys added to `lang/es.json`.

## Notes
- Adds migration `add_analysis_mode_to_saved_filters_table`.
2026-06-09 15:32:07 +02:00
Víctor Falcón 1530544b8b
fix(header): keep mobile logo on one line, compact auth buttons (#512)
## What

Two mobile-header fixes:

- **Logo no longer wraps/clips.** The brand row had no `shrink-0`, so
flexbox squeezed it and "Whisper Money" wrapped to two lines or got cut
off by the buttons. Now pinned with `shrink-0` + `whitespace-nowrap`,
and the bird icon is bumped `size-4` → `size-5`.
- **Auth buttons fit when logged out.** Log in + Register both as text
buttons overflowed the narrow pill. Log in is now an icon-only ghost
button (`LogIn` icon, `aria-label` for a11y), so Register keeps its full
primary-CTA pill.

Desktop header is untouched.

## Test plan

- [ ] Logged-out mobile: logo on one line, login icon + Register both
visible, no overflow
- [ ] Logged-in mobile: Dashboard button unaffected
- [ ] Desktop unchanged
2026-06-09 14:44:35 +02:00
Víctor Falcón 6486908716
fix(transactions): tidy filter bar into two balanced rows (#510)
## What

Tidies the transactions filter bar so it lays out as **two balanced
rows** — a control on the left and on the right of each — instead of
wrapping awkwardly once **Transaction analysis** is enabled (the
saved-filters button was crowding the row).

- **Row 1:** `Filters` (left) · search input (grows, middle) ·
saved-filters bookmark (right)
- **Row 2:** `Analysis / Categorize / + Transaction / ▾` (left) ·
`Columns` (right)

Actions row dropped `flex-wrap` + `sm:justify-end` in favour of a clean
`justify-between`; the saved-filters/clear group is now grouped to the
right with `ml-auto`.

## Screenshots

**Desktop**


![Desktop](https://github.com/whisper-money/whisper-money/blob/assets/transaction-filters-screenshots/screenshots/transactions-desktop.png?raw=true)

**Mobile**


![Mobile](https://github.com/whisper-money/whisper-money/blob/assets/transaction-filters-screenshots/screenshots/transactions-mobile.png?raw=true)

## Notes

- Screenshots are hosted on a throwaway
`assets/transaction-filters-screenshots` branch so they render here
without landing in `main`. Safe to delete after merge.
2026-06-09 11:04:32 +02:00
Víctor Falcón c944a2c37e
feat(analysis): break down spending by sub-category (#508)
## What

Clicking **Analysis** on the transactions page opens a chart of spending
by category. Two improvements:

### Sub-category breakdown
Previously every expense rolled up to its top-level category. Now each
parent shows its total with the sub-categories that carry spending
nested beneath it.

- New `CategoryTree::spendingBreakdown()` builds a two-level tree: each
root with its rolled-up total + immediate sub-categories. Grand-children
fold into their level-2 ancestor. Spend booked directly on a parent that
is *also* split across sub-categories surfaces as a **Direct** child, so
the children always sum to the parent total.
- `by_category` slices now carry a `children[]` array.
- The legend renders each parent row, then indented sub-category rows
(name, % of parent, amount). The donut stays at parent level.

### Clearer colors
Monochrome schemes (blue, pink, neutral) run darkest→lightest, so
adjacent slices were nearly identical. A new `alternateContrast()` zips
the two palette halves (`chart-1,5,2,6,3,7,4,8`) so neighbouring slices
alternate between dark and light ends. Sub-category dots reuse the
parent color at reduced opacity.

## Tests
Added coverage for sub-category nesting, the Direct child, grand-child
folding, and the flat (no-children) case. All analysis + CategoryTree
tests pass.
2026-06-08 14:53:36 +02:00
Víctor Falcón 8375fd490e
feat(transactions): filtered analysis dashboard (#507)
## Summary

Adds an **Analysis** button to the transactions toolbar (left of
Categorize) that opens a bottom-sheet dashboard summarizing the
transactions matching the current filters. Built around the Miami-trip
use case: tag a trip, filter to it, and see where the money went.

Everything is behind the existing `TransactionAnalysis` feature flag
(button hidden + endpoint returns 403 when off).

## Behavior

- **Button**: hidden unless `features.transactionAnalysis`. Disabled
until at least one filter is applied — desktop shows a hover tooltip,
mobile a tap dialog, both reading *"Apply a filter to enable this
button."*
- **Drawer** (vaul bottom-sheet, like the importer): fetches
`/api/transactions/analysis` with the current filters on open. Skeleton
+ empty/error states.
- **Charts**: KPI cards · spending over time (income/expense bars +
cumulative line, auto daily/monthly bucketing) · category donut + ranked
list (only when >1 category) · per-tag bars (only when >1 label).
- **Manual day override**: the *Avg / day* card has an editor to
override the date span used for the daily average (tickets bought months
ahead skew the span). Resolution precedence: matching saved filter's
`analysis_days` → `localStorage[fingerprint]` → auto span. Edits persist
to localStorage always, and sync to the saved filter when the current
filters match one.

## Backend

- `Api/TransactionAnalysisController@summary` — reuses
`Transaction::applyFilters()` + `IndexTransactionRequest`, converts to
the user's base currency via `ExchangeRateService` (rates preloaded),
rolls categories up through `CategoryTree`.
- `GET /api/transactions/analysis`.
- `analysis_days` (nullable) on `saved_filters` + `PATCH
/api/saved-filters/{id}/analysis-days`.

## Tests

- Pest: analysis endpoint (flag gating, totals, category/tag breakdown,
day/month bucketing, cumulative) + saved-filter day override
(set/clear/forbidden/invalid).
- Vitest: button gating (hidden/disabled/opens) + day-override
resolution (auto / saved-precedence / localStorage fallback / persists
to saved filter).
- New `lang/es.json` keys for all added strings.

## Notes

- Data is always sourced from the backend.
- The flag is still off by default — enable `TransactionAnalysis`
per-user to try it.
2026-06-08 14:04:00 +02:00
Víctor Falcón c53c40cf0b
feat(landing): add go now button to redirect modal (#506)
## Summary

On the landing page, logged-in users see a modal with a three-second
progress bar that redirects them to the dashboard. This adds a primary
**Go now** button so users can skip the wait.

- Move **Cancel** button to the left, add primary **Go now** button on
the right (`sm:justify-between`)
- `Go now` calls `router.visit(dashboard())` immediately
- Added `Go now` Spanish translation (CI enforces `es.json`)

## Testing

- Added test: redirects immediately when clicking go now
- All 4 dialog tests pass
2026-06-08 09:51:35 +02:00
Víctor Falcón 346e21ad4e
feat(cashflow): enlarge and color-code net cashflow and saved cards (#505)
## What

Enlarge and color-code the headline numbers in the first two cash-flow
cards so users can spot a weak savings net or low allocation at a
glance.

- **Net Cashflow**: headline number bumped to `4xl`; green when net ≥ 0,
red when negative.
- **Saved & Invested**: headline number bumped to `4xl`; colored by
allocation rate — green ≥50%, yellow ≥25%, orange ≥0%, red below.

## Why

The numbers were uniformly black, making it hard to tell at a glance
when net cashflow is negative or when only a little is being set aside.

## Testing

- `vitest run` on `net-cashflow-card.test.tsx` 
- `bun run format` / `bun run lint` clean
2026-06-08 09:23:57 +02:00
Víctor Falcón 899ea6a939
feat(currency): add NZD (New Zealand Dollar) (#504)
## What
Add New Zealand Dollar (NZD) as a supported currency.

## Compatibility
Conversion provider `@fawazahmed0/currency-api` covers NZD (standard ISO
4217). Conversion service lowercases codes and fetches `nzd.min.json` —
same path AUD uses. `exchange_rates` table stores rates as JSON, so any
3-letter code works. Validation rules and Inertia currency props
auto-derive from config via `CurrencyOptions`.

## Changes
- `config/currencies.php` — NZD entry (`allows_primary` +
`allows_account`)
- `resources/js/utils/currency.ts` — `NZ$` symbol
- `lang/es.json` — Spanish translation (name passes through `__()`)

## Tests
Currency + translation suites pass.
2026-06-08 09:18:17 +02:00
Víctor Falcón f4bbbfd767
fix(auth): prevent FormData crash on successful login (#503)
## What

Removes `resetOnSuccess={['password']}` from the login form.

## Sentry

- Fixes PHP-LARAVEL-2Y — `TypeError: Failed to construct 'FormData':
parameter 1 is not of type 'HTMLFormElement'.` (6 users, on `/login`)

## Root cause

On a successful login the server redirects to the dashboard, which
**unmounts the login form**. `resetOnSuccess` then fires a form reset,
and Inertia's reset handler constructs `new FormData(ref)` from the form
ref — now stale/null — which throws.

The reset serves no purpose on login (the user navigates away
regardless), so removing it eliminates the crash.

## Tests

- Added `resources/js/pages/auth/login.test.tsx`: renders the login page
and asserts the `<Form>` is not configured to reset on success.
2026-06-08 09:03:15 +02:00
Víctor Falcón a7dde5fbc5
feat(open-banking): finalize bank connection without a session via state token (#498)
## Why

iOS PWAs hand the bank redirect back to the system browser (Safari),
where the app session does not exist. The old `/open-banking/callback`
sat behind `auth` + `verified` middleware, so Safari hit it with no
session → bounced to `/login`, the callback body never ran, and the
connection stayed `pending` forever (4 stale rows observed in prod).

## What

Make the bank connection flow session-independent end to end.

### 1. State-token callback (session-independent)
- `store()` generates `Str::random(40)`, persists it on the connection,
and passes it to `startAuthorization` as `state`.
- `/open-banking/callback` is now **public**. It resolves the connection
from `state_token` (`resolveConnectionFromState`) and derives the owner
from the connection, not the session.
- Connection is finalized server-side (`session_id`, status,
`state_token = NULL`) before any redirect.
- New migration adds the nullable, unique `state_token` column; the
column is hidden on the model.

### 2. Completion page instead of login bounce
When the callback runs without a session (the Safari case), the
connection is already finalized server-side. Instead of bouncing the
user to `/login`, render a standalone "go back to the app" page — their
session lives in the PWA, not here.

- New page `resources/js/pages/open-banking/connection-complete.tsx`
(success / error states, dark mode).
- `finishRedirect()` renders the page when `!Auth::check()`; logged-in
users still get the normal redirect.

### 3. Onboarding: land on the connections step + poll cross-browser
- A logged-in user finishing the flow lands straight on the onboarding
connections step (`?step=create-account`), now resolved server-side via
a validated `initialStep` prop so the step is deterministic.
- While on that step the page polls every 4s (`usePoll`, `only:
['accounts']`), so a connection finalized in another browser (PWA →
Safari) shows up without a manual refresh.

## Flow

```
POST /authorize → stateToken = Str::random(40)
  → startAuthorization(state = stateToken)
  → create BankingConnection(pending, state_token)
EnableBanking → callback?code&state=stateToken  (PUBLIC, no session needed)
  → find connection WHERE state_token = state
  → createSession(code) → finalize (session_id, status, state_token = NULL)
  → session present? redirect to connections step / mapping
                   : render "go back to the app" completion page
PWA (logged in) → onboarding?step=create-account → polls accounts every 4s
```

## Tests

36 passing. Covers:
- state-token persistence and session-less finalization
- owner resolved from the connection regardless of who is authenticated
- completion page rendered (success + error) when no session; login
fallback when no connection resolves
- logged-in user redirected directly to the onboarding connections step
- `?step=create-account` lands on that step; unknown step falls back to
default
2026-06-06 12:12:07 +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 8df44c2ef4
feat(transactions): save and reuse transaction filters (#496)
## What

Adds **saved filters** on the transactions page so users can name a set
of filters and reuse them later. The whole feature is gated behind a new
\`transaction-analysis\` Pennant feature flag (off by default).

## Why

Reusing the same filter combinations (e.g. "Japan trip", "Utilities") is
currently manual every time. This ports the saved-filters work from the
\`analysis-page\` branch, scoped down to **only** the transactions page
— the analysis screen and unrelated query changes from that branch are
intentionally left out.

## Changes

**Feature flag**
- \`App\Features\TransactionAnalysis\` — class-based flag, resolves
\`false\` by default.
- Shared to the frontend as \`features.transactionAnalysis\` and added
to the \`Features\` type.

**Backend (saved filters)**
- \`SavedFilter\` model (UUID, \`filters\` json cast, user belongsTo) +
migration + factory.
- \`Api\SavedFilterController\` — user-scoped index/store/update/destroy
under \`/api/saved-filters\`, ownership enforced with \`abort_unless\`.
- \`StoreSavedFilterRequest\` / \`UpdateSavedFilterRequest\` — name
unique per user, snake_case filter rules.

**Frontend**
- \`transaction-filter-serialization.ts\` —
serialize/deserialize/fingerprint between UI filter state and the
persisted snake_case shape.
- \`SavedFilters\` dropdown — load/save/update/delete with active +
dirty indicators.
- Integrated into the filter bar via an opt-in \`enableSavedFilters\`
prop, so it renders **only** on the transactions page (budgets/accounts
reuse the same component and stay unchanged). Render is also gated by
the feature flag.

**Filter bar UX**
- Split search + filters + saved searches onto their own row, separate
from the actions row (Categorize / Add transactions / columns), which
were getting cramped.
- On mobile, the text search moves into the filters popover.

## Reviewer notes
- Backend endpoints are **not** flag-gated; only the UI is, per the
request.
- Two-layer scoping: page opt-in prop **and** feature flag — flag off
means no UI anywhere.
- Translation keys fall back to the key (English); \`es.json\` entries
not added, matching the source branch.

## Testing
- \`tests/Feature/SavedFilterTest.php\` — 9 passing (auth, per-user
listing/uniqueness, ownership on update/delete).
- Pint, Prettier, ESLint clean. No new TypeScript errors introduced.
2026-06-05 18:00:14 +02:00
Víctor Falcón 744d874464
fix(import): honor selected date format for CSV imports (#494)
## Problem

Importing a CSV with `DD/MM/YYYY` dates (e.g. `02/06/2026`) parsed them
as `MM/DD/YYYY`, and the **Date Format** selector either didn't appear
or had no effect — the preview showed the same wrong date regardless of
the chosen format.

## Root cause

The `xlsx` parser coerced CSV date-like strings into Excel **serial
numbers** at read time, guessing the format itself. `parseDate` then hit
its numeric short-circuit and **ignored the selected format entirely**,
so the Date Format radio never changed the preview.

On top of that, ambiguous dates (valid as both DD/MM and MM/DD) were
resolved silently by browser locale, and a saved import config force-hid
the selector — so a wrong guess couldn't be corrected.

## Changes

- **`parseFile` reads with `raw: true`** — CSV cells stay as their
original strings; `parseDate` applies the chosen format. Native
`.xls/.xlsx` dates still arrive as numbers and use the serial path.
- **`detectDateFormat()`** now returns `{ format, ambiguous }`. When
multiple formats parse the sample equally, the importer keeps the Date
Format selector visible (defaulting to the locale/saved best guess) —
even when a saved config exists — so a previously-saved wrong format can
be fixed. `autoDetectDateFormat()` kept as a thin wrapper.
- Applied to both the transactions and account-balances import drawers.
- **UI:** moved the Date Format selector above the preview and switched
the mapping form from broken Tailwind v4 `space-y`/`space-x` to
`flex`/`gap`.

## Tests

- New `parseFile` regression test: `02/06/2026` stays a string and
parses to June (DD-MM) vs February (MM-DD).
- New `detectDateFormat` tests for the `ambiguous` flag.
- All 39 `file-parser` tests pass; lint/format clean.
2026-06-05 15:10:48 +02:00
Víctor Falcón 300188f135
feat(landing): real testimonials with gravatar/facehash avatars (#493)
## Summary
- Replaces the placeholder landing testimonials with **11 real ones**
sourced from welcome-email replies and the Discord community.
- All testimonials shown in **English** with **Spanish translations**
added to `lang/es.json`.
- Adds a testimonial from Víctor Falcón (co-owner).

## Avatars
- Uses **Gravatar** with `d=404`; on miss, Radix `Avatar` falls back to
**`<Facehash>`** (same config as `user-info.tsx`).
- Privacy: raw emails are **not** stored in the repo — only precomputed
MD5 hashes.

## Layout
- Testimonials render in **two marquee rows**, same direction but with
different speeds + a phase offset so cards don't line up into a grid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-05 14:37:49 +02:00
Víctor Falcón 45e25e018d
feat: optionally update manual account balance on transaction delete (#491)
## What

When deleting a transaction that belongs to a **manual**
(non-bank-connected) account, the user can now opt to update the
account's **current balance** for today, so it stays accurate without
re-syncing.

- Deleting an **expense** (amount < 0) → balance **increases** by the
amount.
- Deleting **income** (amount > 0) → balance **decreases** by the
amount.
- Formula: `new_balance = current_balance − transaction.amount`. If
today has no balance row, it's seeded from the latest known balance.
- Available for **single delete** and **bulk delete** (a checkbox in the
confirmation dialog).
- Connected accounts are never touched — their balances come from bank
sync.
- On the account page, the balance display refreshes automatically after
the delete (no page reload).

## How

- New `ManualBalanceAdjuster` service; `TransactionController@destroy`
calls it when the request carries `update_balance` and the account is
manual.
- Frontend `transactionSyncService.delete/deleteMany` accept an
`updateBalance` option; both delete dialogs show the checkbox only when
a manual account is affected.
- `TransactionList` gains an `onBalanceUpdated` callback;
`Accounts/Show` uses it to bump the balance chart's refresh key.
- Added `banking_connection_id` to the accounts payload (transactions,
budgets, and the shared Inertia props feeding accounts/show) so the
frontend can identify manual accounts.

<img width="896" height="400" alt="image"
src="https://github.com/user-attachments/assets/e7f35ccb-13a1-40a1-bf04-615752bddf1a"
/>
2026-06-05 11:30:31 +02:00
Víctor Falcón f8cc8816a8
feat: enable category tree for all users, remove flag (#488)
## Summary
Removes the `CategoryTree` Pennant feature flag so the parent/child
category tree UI is active for all existing and new users.

## Changes
- Delete `app/Features/CategoryTree.php` (flag resolved `false`).
- `CategoryController` — drop `Feature`/`CategoryTreeFeature` imports
and the `categoryTreeEnabled` Inertia prop.
- `parent-category-field.tsx` — remove the `usePage` flag read and `if
(!enabled) return null` gate; the parent field now always renders.

## Testing
- `vendor/bin/pint --dirty` — pass
- `bun run lint` — clean (1 pre-existing unrelated warning in
`chart.tsx`)
- `php artisan test tests/Feature/Settings/CategoryTreeTest.php` — 11
passed
- `php artisan test tests/Feature/Settings/CategoryTest.php` — 32 passed
2026-06-04 13:44:57 +02:00
Víctor Falcón 721cbef024
feat: single-open Sankey expand, fit overflowing children (#487)
## What

- Expanding a category on the Sankey chart now **collapses any other
open category**, so only one subcategory column is shown at a time.
- Grow the SVG \`viewBox\` to the real content bounds so tall child
stacks no longer get **clipped above or below** the canvas.

## Why

Multiple simultaneously-expanded categories cluttered the chart, and a
parent with 3+ children produced a stack taller than the canvas — the
top entries were cut off (e.g. "Traveling €2,011" rendered half-hidden).

## Tests

- Added a test asserting opening a second parent collapses the first.
- Existing expand/collapse tests still pass (5/5).
2026-06-04 11:35:26 +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 679c8d7bff
feat: expand Sankey subcategories inline (#485)
## What

Clicking a parent category in the cashflow **Money Flow** Sankey now
expands its children inline as an extra column branching off the parent
— expenses grow to the right, income to the left — instead of swapping
the whole chart for a drilled-in view.

<img width="782" height="392" alt="image"
src="https://github.com/user-attachments/assets/6ce7d925-19d2-4744-a5da-6884a9583a4d"
/>

## Behaviour

- Toggle: click a parent to expand, click again to collapse. Multiple
parents can be open at once.
- One level deep (a child with its own children is not further
expandable).
- The parent stays as an intermediate node and its flow splits into the
children + a node for transactions booked directly on the parent
(labelled **Parent**).
- Changing the period resets expansions.

## Implementation

- `sankey-chart.tsx`: dynamic column layout (was fixed 3-column); lazily
fetches children via the existing `/api/cashflow/sankey?parent=`
endpoint and caches them; label + amount now render in a `foreignObject`
so flexbox handles spacing, vertical centering, and CSS-ellipsis
truncation (full name kept in a `title`); expand affordance is a
`ChevronsRight` icon.
- Child nodes use the same `MIN_NODE_HEIGHT` and gap as top-level nodes,
centered on the parent so links fan out cleanly.
- Removed the old drill-replace + breadcrumb flow (and the now-unused
`sankeyParent` hook option).
- Backend: the direct-transactions node is renamed from `"<name>
(direct)"` to `"Parent"` (+ `es` translation).
2026-06-04 10:31:40 +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 faccbc3c5a
refactor(js): use InputError for inline form errors (#483)
## What

15 hand-rolled `{errors.x && <p className="text-sm
text-red-500">...</p>}` blocks across 6 components (label/category
create+edit dialogs, automation-rule form, rule builder) replaced with
the existing `InputError` component.

Bonus: `InputError` uses `text-red-600 dark:text-red-400` — the inline
copies had **no dark-mode variant** (CLAUDE.md requires dark-mode
support).

## Stats

- **-63 / +21 lines**

## Checks

- `bun run test` — 154 passed
- `bun run lint` / `format` — clean (1 pre-existing warning)
- `tsc --noEmit` — 59 pre-existing errors, none new

Part of duplication-removal series (#475–#482).
2026-06-03 19:01:21 +02:00
Víctor Falcón 4028e5dfa3
refactor(js): use shared formatMonthFromYearMonth in dashboard (#482)
## What

`use-dashboard-data.ts` had a private `formatMonth()` duplicating
`utils/date.ts` `formatMonthFromYearMonth()`.

Removed the local copy; dashboard now uses the shared util.

**Tiny visual change:** non-current-year labels on dashboard net-worth
chart render `Jan '26` instead of `Jan 26` — now consistent with
cashflow-trend and account-balance charts which already use the shared
util.

## Stats

- **-14 / +2 lines**

## Checks

- `bun run test` — 154 passed
- `bun run lint` — clean (1 pre-existing warning)

Part of duplication-removal series (#475–#481).
2026-06-03 19:00:46 +02:00
Víctor Falcón aa3b811027
refactor(js): extract shared getCsrfToken util (#475)
## What

Same XSRF-TOKEN cookie-parsing snippet was duplicated inline in **12
files** (some as local `getCsrfToken()` copies, some inline
`decodeURIComponent(...)` blocks).

Extracted to single util: `resources/js/lib/csrf.ts`, with vitest
coverage.

## Why

Part of duplication-removal series — PRs that mostly delete code.

## Stats

- App code: **-89 / +23 lines**
- 12 files now import one shared util
- New: `lib/csrf.ts` (8 lines) + `lib/csrf.test.ts`

## Checks

- `bun run test` — 154 pass (3 new)
- `bun run lint` — clean (1 pre-existing warning in chart.tsx)
- `bun run format` — applied
- `tsc --noEmit` — 59 pre-existing errors on main, same 59 on branch
(none introduced)
2026-06-03 17:26:09 +02:00
Víctor Falcón e178f1b1bd
feat(settings): let users disable bank transactions email (#472)
## Summary

Adds a **Notifications** section to the account settings page
(`/settings/account`, between Profile information and Update password)
where users can opt out of the daily "new transactions synced" email.

- New per-user preference `notify_on_bank_transactions_synced` on
`user_settings` (defaults to `true`, opt-out).
- `SendDailyBankTransactionsSyncedEmailJob` skips users who disabled it.
- Single generic `PATCH /settings/notifications` endpoint updates any
notification type via a key→column allowlist in
`NotificationPreferenceController::PREFERENCES`. Future notifications
only need a new entry there — no new route/controller.
- Email footer now links back to the settings section so users can
manage preferences.

## Testing

- `tests/Feature/Settings/NotificationPreferenceTest.php` — update,
unknown-key rejection, invalid value, auth, create-when-missing,
default-true, inertia prop.
- `tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php` — email
not sent when disabled.
2026-06-02 12:24:39 +02:00
Víctor Falcón e5b493329a
feat(currencies): add Colombian and Dominican peso (#471)
## Summary

Adds **COP** (Colombian Peso) and **DOP** (Dominican Peso) to the
supported currency list for both accounts and user primary currency.

## Compatibility

Both are supported by the conversion API (fawazahmed0 currency-api):
- 1 USD = 3686.83 COP
- 1 USD = 58.74 DOP

No migration needed — `currency_code` columns are plain strings and
validation reads the whitelist from `config/currencies.php`.

## Changes

- `config/currencies.php` — add COP + DOP (primary + account)
- `resources/js/utils/currency.ts` — add `RD$` symbol for DOP
- `tests/Feature/Settings/AccountTest.php` — acceptance tests for both

## Testing

`php artisan test --filter="Colombian|Dominican"` → passing
2026-06-01 18:14:16 +02:00
Víctor Falcón f9d1303d98
feat(importer): support YYYYMMDD date format (#470)
## Summary
Adds a compact `YYYYMMDD` (no separators) date format to the
transactions and balance importers.

- New `DateFormat.YearMonthDayCompact = 'YYYYMMDD'` enum value
- `parseDate` parses the 8-digit form (`20241231` → `2024-12-31`)
- `autoDetectDateFormat` recognizes it (unambiguous, no separators)
- Both mapping UIs (transactions + balances) expose it as a selectable
option

## Testing
- `bunx vitest run resources/js/lib/file-parser.test.ts` — 34 passed
- Added tests: compact auto-detection + compact conversion
- `bun run format`, `bun run lint` clean
2026-06-01 16:44:46 +02:00
Víctor Falcón d68fee6c2d
fix(sentry): only report errors in production (#467)
## What

Gate Sentry error reporting on the production environment so nothing is
sent from local, staging, or testing.

- **Backend** (`config/sentry.php`): DSN resolves to `null` unless
`APP_ENV=production`. A null DSN disables the Laravel SDK entirely.
- **Frontend** (`resources/js/app.tsx`): `enabled` now requires
`import.meta.env.MODE === 'production'` (plus a DSN).
2026-06-01 12:41:31 +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 5ce439f463
feat(cashflow): rework summary cards into net + saved/invested (#465)
## What

Reworks the two top summary cards on the **Cashflow** page so they no
longer both show net flow.

### Net Cashflow (card 1)
- Shows the net amount **and** its share of income (%).
- Compares **both** the amount and the percentage with the previous
period.

### Saved & Invested (card 2, replaces "Savings Rate")
- Headline = money moved to savings + investment categories (amount).
- Percentage = that total as a share of **net cashflow** (income −
expenses).
- Compares amount and percentage vs. the previous period.
- Keeps the per-category Saved / Invested breakdown at the bottom.
- Adds a **click-triggered help popover** (shadcn `Popover`, works on
touch + desktop, dismisses on outside click / Escape) explaining the
numbers come from transactions using a **"saving"** or **"investment"**
category type.

### i18n
- The card's "Saved" label uses a dedicated key so it renders
**"Ahorrado"** on the cashflow card while form buttons keep
**"Guardado"**. A minimal `lang/en.json` keeps the English label as
"Saved".
- Spanish strings added for the new card + popover copy.

## Testing
- `vitest` — net-cashflow + saved-invested card specs (amount/percentage
comparisons, click-to-reveal popover)
- Full `pest` suite (excl. Browser) — green; localization key-coverage
test passes
- `pint --test`, `prettier --check`, `eslint` — clean
- production `bun run build` — succeeds
2026-06-01 11:37:28 +02:00
Víctor Falcón 448bb2e64a
feat(posthog): route analytics through reverse proxy (#463)
## What

Route PostHog through the managed reverse proxy at `t.whisper.money` to
dodge ad blockers and keep analytics first-party.

We use the **JS library method** (`posthog-js` in
`resources/js/lib/posthog.ts`), so only that init needed changes — no
snippet.

## Changes
- `api_host` default → `https://t.whisper.money` (proxy domain)
- Add `ui_host` (default `https://eu.posthog.com`) so dashboard links
resolve back to PostHog correctly
- Both stay env-overridable: `VITE_POSTHOG_HOST`, new
`VITE_POSTHOG_UI_HOST`
- Update `.env.example`

## Test
- `posthog.test.ts` passes
- format + lint clean (one pre-existing unrelated warning)
2026-06-01 09:29:30 +02:00
Víctor Falcón 96ee311299
fix(register): don't block signup on unrecognized browser timezone (#462)
## Problem

Some users reported they couldn't register: they filled the form
correctly, pressed the button, saw a loading spinner, then got bounced
back to the registration page with **no error message**. Meanwhile 20-30
users register fine every day, and the issue wasn't reproducible by the
team.

## Root cause

The register form sends a **hidden, browser-auto-detected `timezone`**
field (`Intl.DateTimeFormat().resolvedOptions().timeZone`). The backend
validated it with Laravel's `timezone` rule:

```php
'timezone' => ['nullable', 'string', 'timezone'],
```

That rule rejects **legacy IANA aliases** that many browsers/OSes still
emit — e.g. `Asia/Calcutta`, `Asia/Saigon`, `Asia/Rangoon`,
`US/Pacific`. Proof on PHP 8.4:

```
in_array("Asia/Calcutta", DateTimeZone::listIdentifiers())  →  false
```

When validation failed, Fortify redirected back with a `timezone` error
— but the form had **no error display** for timezone (every other field
has one). Result: spinner → silent bounce → no message → and the user
can't fix a hidden field anyway. The team couldn't reproduce because
their browsers send canonical zones that pass. Production tzdata may
reject even more zones.

## Fix

- Stop validating the auto-detected hidden field against the strict
timezone list.
- Add `normalizeTimezone()`: keep recognized zones (including BC aliases
via `DateTimeZone::ALL_WITH_BC`), drop anything unrecognized to `null`.
Registration can never be blocked by it again.
- Surface `errors.timezone` in the form as a safety net so a
hidden-field failure can never be silent in the future.

## Tests

- Legacy alias `Asia/Calcutta` → registers, stored as-is.
- Unrecognized zone → registers, stored `null`.
- All existing registration tests pass (13 passed).
2026-06-01 09:03:15 +02:00
Víctor Falcón 85ea3cc714
feat(accounts): show 50 transactions per page and link to full list (#459)
## What

- Bump the account page transaction list page size from **10 → 50**
(`Accounts/Show.tsx`).
- Add a **View in Transactions** button next to *Load more* (only on the
account page) that opens the Transactions page pre-filtered to the
current account via `account_ids`.
- Add Spanish translation `View in Transactions` → `Ver en
Transacciones`.

## Notes

- The account list is synced client-side (IndexedDB); `pageSize`
controls how many rows render before *Load more*, so no backend change
needed.
- The Transactions index already defaults to `per_page = 50` and reads
`account_ids` from the query string.

## Testing

- `vitest` Accounts/Show tests pass.
- `bun run format`, `bun run lint`, `bun run build` pass.
2026-05-30 18:48:21 +02:00
Víctor Falcón 144d919c0b
fix(import): correct balance for same-day, zero and negative values (#456)
## Problem

CSV imports only carry a date (no timestamp). Rows are newest-on-top and
we import top-to-bottom. Three balance bugs:

1. **Same-day balance wrong.** The store loop did `Map.set(date,
balance)` per row, so when a day had 2+ transactions the **last**
(bottom = oldest) row overwrote the newest. The correct daily balance is
the **first** (newest) row.
2. **Zero balance dropped.** The parser guard `row[mapping.balance]`
treated numeric `0` as falsy, so a real `0` balance became `null`. `0`
is valid.
3. **Negative balance lost its sign.** `parseAmount` stripped `-` via
`replace(/[^\d]/g,'')`, so negative balances (and amounts) came out
positive.

## Fix

- `import-transactions-drawer.tsx`: keep the **first** occurrence per
date (`!balancesToImport.has(date)`).
- `file-parser.ts`: guard balance on null/undefined/empty-string only,
so `0` is kept.
- `file-parser.ts`: `parseAmount` now detects a leading `-` (or `(...)`)
and applies the sign — fixes negative balances and negative amounts.

## Tests

Added 4 cases to `file-parser.test.ts`: zero (number), zero (string),
negative, empty→null. Full suite: **144 passed**.

> Note: the same-day "keep first" logic lives in the React component and
relies on `newTransactions` staying in CSV order (top = newest), which
holds in current code.
2026-05-30 16:19:27 +02:00
Víctor Falcón 65175e184a
fix(accounts): translate update button in edit account modal (#455)
## Problem
Update button in edit account modal showed hardcoded English (`Update` /
`Updating...`) instead of translated text.

## Fix
Wrap button label in `__()` so it resolves the existing `es.json` keys
(`Actualizar` / `Actualizando...`).

## Test
`edit-account-dialog.test.tsx` passes.
2026-05-30 16:09:30 +02:00
Víctor Falcón 05ee8dc442
feat(categorize): show debtor and creditor names (#454)
## What
Show the debtor and creditor names (when present) on the categorization
page (`/transactions/categorize`).

## Changes
- `TransactionController@categorize`: select `creditor_name` and
`debtor_name` — they were never sent to the frontend.
- `categorizer-card.tsx`: render Creditor/Debtor rows below the account
when present, using the same i18n labels as the transaction columns.
- `TransactionTest`: assert both names are exposed on the categorize
page.

## Testing
- `php artisan test --filter="debtor and creditor"` — passes.
- pint, format, lint clean.
2026-05-30 12:27:59 +02:00
Víctor Falcón cbc2f4f85f
chore: update Discord invite link (#452)
Swap Discord invite link `discord.gg/2WZmDW9QZ8` →
`discord.gg/m8hUhx6D9D` across all files (README, header, user menu,
connections page, welcome email, test).
2026-05-29 15:56:48 +02:00
Víctor Falcón 5119528149
fix(categories): expose cashflow setting on create (#448)
## Summary
- Always show a simplified Cashflow and charts explanation in the
category form
- Explain how each category type affects cashflow, income/spending
charts, top spending categories, savings, and investments
- For transfer categories, keep a cashflow chart visibility selector so
users can choose hidden/inflow/outflow
- Show savings and investment categories as outflows in the cashflow
chart and store default/new categories as outflow
- Add a new migration to update existing production savings/investment
categories to outflow
- Avoid user-facing Sankey terminology and update Spanish copy to use
dinero/saldo instead of efectivo
- Add missing Spanish translations and browser/feature coverage

## Tests
- php artisan test --compact tests/Feature/LocalizationTest.php
- php artisan test --compact
tests/Feature/Console/ResetUserCategoriesCommandTest.php
- php artisan test --compact tests/Feature/Settings/CategoryTest.php
--filter="migration updates existing default saving and investment
categories|migration sets saving and investment categories to cashflow
outflow|users can create savings and investment categories|default
categories are created when user registers"
- php artisan test --compact tests/Feature/CashflowAnalyticsTest.php
--filter="sankey includes savings and investment categories on the
expense side|sankey includes outflow transfer categories on the expense
side"
- php artisan test --compact tests/Browser/CategoriesTest.php
--filter="explains savings and investment category cashflow impact in
the create dialog|can create a new transfer category with a cashflow
analytics direction"
- php artisan test --compact tests/Browser/CategoriesTest.php
- ./vendor/bin/pest --testsuite=Performance --filter="account show page
does not exceed query threshold"
- vendor/bin/pint --dirty --format agent
- npx prettier --write
resources/js/components/categories/category-cashflow-direction-fields.tsx
- npm run build (assets emitted; Sentry sourcemap upload reports local
SSL error)

## Video
- screenshots/category-create-cashflow-setting.webm

## Notes
- npm run types still fails on existing unrelated TypeScript errors.
2026-05-29 11:05:04 +02:00
Víctor Falcón 0b9406714e
fix: filter Safari cashback extension errors (#447)
## Sentry
- Issue: PHP-LARAVEL-2K
- URL: https://whisper-money.sentry.io/issues/123306290/

## Root cause
Sentry is collecting unhandled errors from a Safari/iOS browser
extension content script. Recent production events all came from
`webkit-masked-url://hidden/`, function `onResponse`, with message
`response.cashbackReminder`, on public app routes. No matching
application code exists.

## Fix
- Add a narrow client-side Sentry `beforeSend` filter for this Safari
cashback extension signature.
- Keep similarly named errors from application bundles.
- Add Vitest regression coverage.

## Verification
- `npx prettier --write resources/js/app.tsx resources/js/lib/sentry.ts
resources/js/lib/sentry.test.ts`
- `npx eslint --fix resources/js/app.tsx resources/js/lib/sentry.ts
resources/js/lib/sentry.test.ts`
- `npm test -- resources/js/lib/sentry.test.ts`
- `npm run types` (fails on existing unrelated TypeScript errors outside
touched files)
2026-05-28 15:10:49 +02:00
Víctor Falcón 4f46ae3e2d
fix: move community link to user menu (#442)
## Summary
- Move Community link from sidebar footer into user dropdown above
Feedback.
- Hide empty sidebar footer nav group.
- Add tests for dropdown order and footer removal.

## Tests
- `npm test -- --run resources/js/components/user-menu-content.test.tsx
resources/js/providers/menu-item-provider.test.ts`

Note: `npm run types` currently fails on unrelated existing TypeScript
errors.
2026-05-27 17:39:26 +02:00
Víctor Falcón 10da06ed84
feat(transactions): add counterparty fields (#440)
## Summary
- add creditor/debtor fields to transactions with raw_data backfill
- store counterparties on bank sync and CSV/XLS imports
- add creditor/debtor filters plus hidden table columns

## Tests
- php artisan test --compact tests/Feature/LocalizationTest.php
tests/Feature/OpenBanking/TransactionSyncServiceTest.php
tests/Feature/TransactionFilterTest.php
- npm test -- resources/js/lib/file-parser.test.ts --run
- vendor/bin/pint --dirty --format agent

Note: `npm run types` still has pre-existing unrelated errors; no
creditor/debtor/import-related errors remained in filtered output.

## Screenshot
<img width="1241" height="676" alt="8odYWtFcvUM"
src="https://github.com/user-attachments/assets/55653485-d588-4beb-9e6a-5c7c81ba7cf8"
/>
2026-05-27 16:20:55 +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 235911b960
fix(budgets): explain locked edit fields (#437)
## Summary
- add disclaimer for locked period and carry-over budget fields
- cover disclaimer rendering with Vitest

## Tests
- npm run test --
resources/js/components/budgets/edit-budget-dialog.test.tsx
2026-05-26 15:49:53 +02:00