Commit Graph

16 Commits

Author SHA1 Message Date
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 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 9bb835e79b
fix(categorizer): fetch uncategorized transactions from backend instead of IndexedDB (#165)
## Problem

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

## Solution

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

## Tests

Added 5 new feature tests covering:
- Categorize page is accessible to authenticated users and includes the
`transactions` prop
- Only uncategorized transactions are returned
- Transactions from deleted accounts are excluded
- Transactions from other users are not visible
- Guests are redirected to login
2026-02-28 16:30:54 +00:00
Víctor Falcón d1efc03fc5
Move transaction filtering from client-side to server-side (#126)
## Summary

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

## Changes

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

## Test plan

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

- Store account names in plaintext instead of encrypting them
client-side. Encryption remains only for transaction descriptions and
notes.
- Existing encrypted account names are silently migrated when the user
unlocks their encryption key, via a new `useDecryptAccountNames` hook
that calls API endpoints to update each account.
- New `AccountName` component handles rendering both encrypted (legacy)
and plaintext accounts across the app.

## Test plan

- [x] Create a new account — name should be stored in plaintext
(`encrypted = false`, `name_iv = null`)
- [x] Unlock encryption key with existing encrypted accounts — they
should auto-migrate silently
- [x] After migration, verify accounts show `encrypted = false` in DB
and names are readable plaintext
- [x] Edit a migrated account — should work without encryption
- [x] Verify all account name displays work across dashboard, account
details, transactions, settings
2026-02-09 14:15:26 +01:00
Víctor Falcón a6a2a0d58c
fix: Apply automation rule labels on transaction creation and import (#79)
## Summary
- Fixes automation rules not applying labels when creating transactions
manually or via CSV import
- Eager-loads the `labels` relationship on automation rules in both
`TransactionController` and `HandleInertiaRequests`
- Syncs `label_ids` on the transaction `store` endpoint (was accepted
but never persisted)
- Passes `automationRules` prop through the full component chain:
`index.tsx` → `TransactionActionsMenu` → `ImportTransactionsDrawer`, and
`index.tsx` → `EditTransactionDialog`
- Passes `automationRules` as the missing 5th argument to
`reEvaluateAll()`

Closes #61

## Test plan
- [x] Existing feature tests pass (`php artisan test
--filter=Transaction`, `--filter=AutomationRule`)
- [x] Pint, ESLint, and Prettier all pass
- [x] Manually verify: create a transaction matching an automation rule
with labels → labels are applied
- [x] Manually verify: import CSV with transactions matching rules with
labels → labels are applied
- [x] Manually verify: "Re-evaluate All" applies labels from matched
rules
2026-01-27 11:11:29 +01:00
Víctor Falcón 8a8d5962b5
refactor: Simplify transaction endpoints architecture (#76)
## Summary

Simplifies transaction endpoints by separating sync API (read-only) from
web routes (mutations). This creates clearer architectural boundaries
and fixes inconsistent label behavior.

## Architecture Changes

### Before
- Sync API handled both sync (GET) and mutations (POST/PATCH/DELETE)
- Frontend used sync API for all operations
- Bulk updates merged labels, single updates replaced them

### After  
- **Sync API**: Read-only GET endpoint for IndexedDB sync
- **Web Routes**: All mutations (create, update, delete, bulk
operations)
- **Consistent behavior**: All label updates replace instead of merge

## Endpoint Mapping

| Operation | Old Endpoint | New Endpoint |
|-----------|-------------|--------------|
| Fetch/Sync | `GET /api/sync/transactions` | `GET
/api/sync/transactions`  |
| Create | `POST /api/sync/transactions` | `POST /transactions` |
| Update | `PATCH /api/sync/transactions/{id}` | `PATCH
/transactions/{id}` |
| Delete | `DELETE /api/sync/transactions/{id}` | `DELETE
/transactions/{id}` |
| Bulk Update | `PATCH /transactions/bulk` | `PATCH /transactions/bulk`
 |

## Backend Changes

### TransactionSyncController
-  Simplified to read-only `index()` method
-  Removed `store()`, `update()`, `destroy()` methods
-  Added docblock clarifying purpose

### TransactionController
-  Fixed `store()` to return labels with `id, name, color`
-  Fixed `update()` to return labels with `id, name, color`  
-  Changed `bulkUpdate()` to replace labels instead of merging

### Cleanup
-  Removed `UpdateTransactionSyncRequest` (no longer needed)
-  Updated `routes/api.php` to only have GET for sync

## Frontend Changes

### transaction-sync.ts
-  Updated `create()` → `POST /transactions`
-  Updated `update()` → `PATCH /transactions/{id}`
-  Updated `delete()` → `DELETE /transactions/{id}`
-  Replaced all `fetch()` calls with `axios`
-  Removed manual CSRF token handling

## Test Changes

### TransactionSyncTest
-  Removed create/update/delete tests
-  Kept only read-only sync tests
-  Added test for labels format

### BulkUpdateTransactionsTest  
-  Added test verifying label replacement behavior

## Test Results

All tests passing! 

```
Tests:    42 passed (235 assertions)
Duration: 6.17s

✓ TransactionTest: 29 tests
✓ TransactionSyncTest: 4 tests
✓ BulkUpdateTransactionsTest: 9 tests
```

## Benefits

1. **Clear separation of concerns**: Sync API is read-only, web routes
handle mutations
2. **Consistent label behavior**: All updates replace labels (not merge)
3. **Standardized HTTP client**: Axios everywhere, automatic CSRF
handling
4. **Reduced complexity**: Removed duplicate form request class
5. **Better architecture**: Aligns with intended design

## Breaking Changes

None - All changes are internal to how the frontend calls the backend.
The functionality remains the same from the user's perspective.

## Files Changed

- `app/Http/Controllers/Sync/TransactionSyncController.php`
- `app/Http/Controllers/TransactionController.php`
- `app/Http/Requests/UpdateTransactionSyncRequest.php` (deleted)
- `resources/js/services/transaction-sync.ts`
- `routes/api.php`
- `tests/Feature/Sync/TransactionSyncTest.php`
- `tests/Feature/BulkUpdateTransactionsTest.php`
2026-01-25 16:15:17 +01:00
Víctor Falcón e5eca1eacb
feat: Add label support to single transaction update endpoint (#75)
## Summary
- Added label update support to the single transaction `update()`
endpoint
- Previously, labels could only be updated via the bulk update endpoint
- Now both endpoints support label updates with consistent behavior

## Changes
- **UpdateTransactionRequest**: Added `label_ids` validation (array of
UUIDs that must belong to user)
- **TransactionController@update**: Now handles label updates alongside
other attributes
  - Syncs labels when `label_ids` is provided
  - Reloads labels relationship for fresh data in events
  - Fires single `updated` event after all changes
- **AssignTransactionToBudget listener**: Ensures labels are loaded
fresh after queue serialization
- **Tests**: Added 6 comprehensive tests covering all label update
scenarios

## Behavior
- **Don't send `label_ids`** → labels unchanged (partial update)
- **Send `label_ids: []`** → removes all labels
- **Send `label_ids: [uuid1, uuid2]`** → replaces labels with these

## Testing
When labels are updated, the `TransactionUpdated` event fires and the
transaction is automatically assigned to matching budgets (via queued
listener).

All tests pass (29 tests, 134 assertions).

Fixes the issue where transactions wouldn't appear in budget after
adding a matching label via the single transaction update endpoint.
2026-01-24 17:54:54 +01:00
Víctor Falcón 134a292ddb
fix: Fire transaction updated event after a label change (#73) 2026-01-22 15:25:52 +01:00
Víctor Falcón 439ec86722
chore: Simplify IndexedDB sync by moving to Inertia shared props (#63)
This PR simplifies the IndexedDB synchronization mechanism by removing
individual sync controllers and services, and instead using Inertia.js
shared props to provide data globally across the application.

## Benefits

1. **Simplified Architecture**: Removed complex sync logic for
static/semi-static data (accounts, categories, banks, labels, automation
rules)
2. **Better Performance**: Data is now shared via Inertia props,
eliminating unnecessary API calls and IndexedDB operations
3. **Reduced Complexity**: Significantly reduced codebase size (~2000
lines removed)
4. **Better UX**: Data is immediately available on page load without
waiting for sync operations
5. **Maintainability**: Fewer moving parts means easier to maintain and
debug

## Migration Notes

- Transaction syncing still uses IndexedDB for offline support
- All other data (accounts, categories, banks, labels, automation rules)
is now fetched via Inertia shared props
- Components automatically receive updated data on navigation without
manual sync operations
2026-01-19 19:15:26 +01:00
Víctor Falcón f8bea791e8
Bulk actions/select all (#33) 2025-12-15 17:59:58 +01:00
Víctor Falcón 4b5d65ba03
feat: add transaction labels feature (#24)
## Summary

Add a labeling system for transactions that differs from categories in
that labels are ephemeral and created on-the-fly when assigned (no
pre-creation required). A transaction can have multiple labels
(many-to-many relationship), and labels can be used for filtering in the
transaction table and as actions in automation rules.

<img width="1372" height="484" alt="image"
src="https://github.com/user-attachments/assets/fd342b11-dafb-44ed-b818-578e1ea856e6"
/>
<img width="1373" height="613" alt="image"
src="https://github.com/user-attachments/assets/aa5cfb8b-50b7-4101-a872-54904924f234"
/>
<img width="1035" height="594" alt="image"
src="https://github.com/user-attachments/assets/ffa946b2-b01f-496b-8cb8-ab55ab5b79ed"
/>


## Changes

### Backend (Laravel)

- Add `labels` table with user_id, name, color, and soft deletes
- Add `label_transaction` pivot table for transaction-label many-to-many
- Add `automation_rule_labels` pivot table for automation rule-label
many-to-many
- Create Label model with relationships to User, Transaction, and
AutomationRule
- Add LabelController for CRUD operations (returns JSON for on-the-fly
creation)
- Add LabelSyncController for frontend sync
- Add LabelPolicy for authorization
- Update TransactionController to handle bulk label updates
- Update AutomationRuleController to handle label actions
- Add validation for label_ids in transactions and automation rules

### Frontend (React/TypeScript)

- Add Label type and color utilities
- Add labels table to IndexedDB (Dexie version 6)
- Create label-sync service with findOrCreate functionality
- Create LabelCombobox component with multi-select and create-on-type
- Add labels column to transactions table
- Add labels filter to transaction filters
- Add bulk label assignment in bulk actions bar
- Add labels action in create automation rule dialog
- Update rule engine to include labels in evaluation results

## Testing

- 11 feature tests for label CRUD
- 3 tests for label sync
- All 241 feature tests pass

## Screenshots

N/A - UI changes can be reviewed in browser
2025-12-13 13:02:19 +01:00
Víctor Falcón e51bad0cae
Quick Categorization Wizard (#21)
## Summary

Add a new quick categorization page that allows users to categorize
transactions without categories one by one, in a game-like interface.


https://github.com/user-attachments/assets/14fd7386-e87e-4a75-ae94-e487e3a48112

## Features

- **Quick Categorization Page**: A clean, focused interface for
categorizing transactions
  - White screen with centered transaction card
  - Categories sorted by usage frequency (stored in localStorage)
- Smooth animations when categorizing (card exits, success feedback,
next card enters)
  - Progress indicator showing remaining transactions

- **Keyboard Shortcuts**:
  - `Ctrl+R`: Open automation rules dialog
  - `Ctrl+N`: Skip current transaction
  - `Escape`: Go back to transactions page
  - Arrow keys + Enter: Navigate and select categories

- **Automation Rules Integration**:
  - Rules button in header to manage automation rules
- When closing the rules dialog, all remaining transactions are checked
against rules
  - Matching transactions are automatically categorized

- **UI/UX**:
  - Command component for category search and selection
  - Encrypted text component for account names
  - Responsive amount display with positive/negative styling
  - Skip button moved to header for cleaner card design

## Access

New "Categorize" button added to the transactions page toolbar (left of
"Add Transaction" button) showing the count of uncategorized
transactions.
2025-12-09 14:38:39 +01:00
Víctor Falcón c1fbd4d09f feat(TransactionController): Add store method for creating transactions 2025-11-11 12:20:39 +00:00
Víctor Falcón ea9cec184f Bulk actions on transactions 2025-11-10 10:02:28 +00:00
Víctor Falcón 509065e28d Transactions page 2025-11-08 23:38:35 +00:00