## 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.
## 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.
## 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.
## 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.
## 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`.
## 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
## 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**

**Mobile**

## 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.
## 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.
## 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.
## 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
## 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
## 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.
## 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.
## 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.
## 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"
/>
## 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
## 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).
## 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).
## 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).
## 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
\`\`\`
## 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)
## 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
## 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.
## 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
## 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.
## 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.
## 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.
## 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.
Swap Discord invite link `discord.gg/2WZmDW9QZ8` →
`discord.gg/m8hUhx6D9D` across all files (README, header, user menu,
connections page, welcome email, test).
## 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.
## 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.
## 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"
/>
## 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
## Summary
- translate cashflow period type labels during render
- keep Spanish labels as Mes / Trimestre / Año
- add regression coverage for period label translation
## Tests
- npx eslint resources/js/components/cashflow/period-navigation.tsx
resources/js/components/cashflow/period-navigation.test.tsx
- npx vitest run
resources/js/components/cashflow/period-navigation.test.tsx
## Summary
- stack cashflow header on mobile
- make period type and period selector rows full width on mobile
## Tests
- npx eslint resources/js/pages/cashflow/index.tsx
resources/js/components/cashflow/period-navigation.tsx
## Summary
- fix budget spending chart today marker on first period
- use local date keys for budget chart dates
- add today marker tests
## Tests
- npm test --
resources/js/components/budgets/budget-spending-chart.test.ts
- npm run types (fails: existing unrelated TypeScript errors)
## Why
Some banks return transactions without a balance column. The CSV/Excel
importer happily creates the transactions but leaves the account without
any balance history, making the account look incomplete.
## What
Opt-in flow that derives per-day balances from the transactions
themselves, using a single reference point (the balance on the date of
the latest transaction).
### Mapping step
- New checkbox **"Calculate balances from transactions"** next to the
Balance column select.
- Available only when no balance column is mapped (disabled + visually
faded otherwise).
- When checked, a balance input appears labeled with the latest
transaction date using a relative format:
- `Today`
- `Yesterday`
- `Monday, 3 of Jun`
- If an `AccountBalance` already exists for that date, it is pre-filled
automatically (no need to ask) with a small hint.
- The reference balance is **mandatory** when the checkbox is checked;
the Next button is disabled until it is provided.
### Preview step
- A new `Balance` column shows the balance about to be created for each
date, regardless of whether it came from the file or was calculated.
### Persistence
- Balances are computed by walking backwards across distinct transaction
dates and subtracting each day's net movement.
- They flow through the existing import path that POSTs to
`AccountBalanceController@store`, so no new endpoint was needed.
## Feature flag
Hidden behind a new Pennant feature, **off by default**:
```bash
php artisan feature:enable CalculateBalancesOnImport user@example.com
# or
php artisan feature:enable CalculateBalancesOnImport all
```
The flag is exposed to the frontend via Inertia shared props
(`features.calculateBalancesOnImport`).
## Tests
- New vitest cases for `getLatestTransactionDate` and
`calculateBalancesFromTransactions` (sparse dates, reference date
without txns, empty list).
- `LocalizationTest` passes (new ES strings added).
- Full vitest suite green (106 tests).
## Summary
- remove CoinbaseIntegration Pennant feature flag
- always show Coinbase in open banking institution picker
- drop shared Coinbase feature flag type
## Tests
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/InertiaSharedDataTest.php
- php artisan test --compact
tests/Feature/OpenBanking/CoinbaseControllerTest.php
- npx eslint
resources/js/components/open-banking/connect-account-inline.tsx
resources/js/components/open-banking/connect-account-dialog.tsx
resources/js/types/index.d.ts
Note: npm run types still fails on existing project-wide Wayfinder/type
issues.
## Summary
- only auto-select the sole import account during onboarding
- hide the upload-step Back button after onboarding auto-select
- add component coverage for account selection and hidden back button
## Tests
- npm test --
resources/js/components/transactions/import-step-account.test.tsx
resources/js/components/transactions/import-step-upload.test.tsx
- npx eslint
resources/js/components/transactions/import-step-account.tsx
resources/js/components/transactions/import-step-upload.tsx
resources/js/components/transactions/import-transactions-drawer.tsx
resources/js/components/onboarding/step-import-transactions.tsx
resources/js/lib/import-config-storage.ts resources/js/types/import.ts
resources/js/components/transactions/import-step-account.test.tsx
resources/js/components/transactions/import-step-upload.test.tsx
## Notes
- npm run types still fails on existing unrelated project-wide
TypeScript errors.