From e937a8647dbe69fbd93ea2b5ddad44bbe7ba4a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vi=CC=81ctor=20Falco=CC=81n?= Date: Fri, 14 Nov 2025 14:18:43 +0100 Subject: [PATCH] feat(automation): Add re-evaluate all transactions functionality --- app/Actions/CreateDefaultCategories.php | 2 +- .../Sync/AccountSyncController.php | 2 - .../Controllers/Sync/BankSyncController.php | 2 - .../Sync/CategorySyncController.php | 2 - .../automation-rules/rule-builder.tsx | 72 +++++----- .../js/components/sync-status-button.tsx | 2 - .../transactions/category-select.tsx | 2 - .../transactions/edit-transaction-dialog.tsx | 32 +++-- .../import-transactions-button.tsx | 4 +- .../import-transactions-drawer.tsx | 2 +- .../transactions/transaction-actions-menu.tsx | 53 +++++++- resources/js/hooks/use-online-status.ts | 1 + .../hooks/use-re-evaluate-all-transactions.ts | 124 ++++++++++++++++++ resources/js/pages/transactions/index.tsx | 8 +- resources/js/services/account-sync.ts | 1 + resources/js/services/bank-sync.ts | 1 + resources/js/services/category-sync.ts | 1 + resources/js/types/automation-rule.ts | 1 + tests/Feature/Sync/AccountSyncTest.php | 2 - tests/Feature/Sync/CategorySyncTest.php | 2 - 20 files changed, 248 insertions(+), 68 deletions(-) create mode 100644 resources/js/hooks/use-re-evaluate-all-transactions.ts diff --git a/app/Actions/CreateDefaultCategories.php b/app/Actions/CreateDefaultCategories.php index 22240b9a..274e8ed4 100644 --- a/app/Actions/CreateDefaultCategories.php +++ b/app/Actions/CreateDefaultCategories.php @@ -25,7 +25,7 @@ class CreateDefaultCategories */ public static function getDefaultCategories(): array { - return + return [ ['name' => 'Administrative violations', 'icon' => 'AlertTriangle', 'color' => 'red'], ['name' => 'Alimony and child support', 'icon' => 'Users', 'color' => 'teal'], ['name' => 'Association membership fees', 'icon' => 'Users2', 'color' => 'cyan'], diff --git a/app/Http/Controllers/Sync/AccountSyncController.php b/app/Http/Controllers/Sync/AccountSyncController.php index fdcfe20f..07aa6b0c 100644 --- a/app/Http/Controllers/Sync/AccountSyncController.php +++ b/app/Http/Controllers/Sync/AccountSyncController.php @@ -26,5 +26,3 @@ class AccountSyncController extends Controller ]); } } - - diff --git a/app/Http/Controllers/Sync/BankSyncController.php b/app/Http/Controllers/Sync/BankSyncController.php index 36c68f9f..2980df14 100644 --- a/app/Http/Controllers/Sync/BankSyncController.php +++ b/app/Http/Controllers/Sync/BankSyncController.php @@ -27,5 +27,3 @@ class BankSyncController extends Controller ]); } } - - diff --git a/app/Http/Controllers/Sync/CategorySyncController.php b/app/Http/Controllers/Sync/CategorySyncController.php index ae7b7582..7dd2f824 100644 --- a/app/Http/Controllers/Sync/CategorySyncController.php +++ b/app/Http/Controllers/Sync/CategorySyncController.php @@ -24,5 +24,3 @@ class CategorySyncController extends Controller ]); } } - - diff --git a/resources/js/components/automation-rules/rule-builder.tsx b/resources/js/components/automation-rules/rule-builder.tsx index 4d9c7db9..cdd0d205 100644 --- a/resources/js/components/automation-rules/rule-builder.tsx +++ b/resources/js/components/automation-rules/rule-builder.tsx @@ -83,12 +83,12 @@ export function RuleBuilder({ value, onChange, error }: RuleBuilderProps) { variant="outline" size="sm" onClick={toggleGroupOperator} - className='py-4 px-1.5' + className="px-1.5 py-4" > Groups joined by:{' '} {structure.groupOperator.toUpperCase()} - + )} @@ -97,32 +97,39 @@ export function RuleBuilder({ value, onChange, error }: RuleBuilderProps) {
{structure.groups.map((group, groupIndex) => (
- +
- {group.conditions.length > 1 && (<> - - + {group.conditions.length > 1 && ( + <> + + )}
{structure.groups.length > 1 && ( @@ -143,14 +150,16 @@ export function RuleBuilder({ value, onChange, error }: RuleBuilderProps) {
{ + onChange={( + updatedCondition, + ) => { updateGroup(group.id, { ...group, conditions: group.conditions.map( (c) => c.id === - condition.id + condition.id ? updatedCondition : c, ), @@ -186,7 +195,7 @@ export function RuleBuilder({ value, onChange, error }: RuleBuilderProps) { type="button" variant="outline" size="sm" - className="w-full mt-0" + className="mt-0 w-full" onClick={() => { updateGroup(group.id, { ...group, @@ -267,8 +276,8 @@ function ConditionRow({ fieldConfig?.type === 'number' ? 'number' : fieldConfig?.type === 'date' - ? 'date' - : 'text'; + ? 'date' + : 'text'; return (
@@ -328,4 +337,3 @@ function ConditionRow({
); } - diff --git a/resources/js/components/sync-status-button.tsx b/resources/js/components/sync-status-button.tsx index 9d879e9e..2b000973 100644 --- a/resources/js/components/sync-status-button.tsx +++ b/resources/js/components/sync-status-button.tsx @@ -74,5 +74,3 @@ export function SyncStatusButton() { ); } - - diff --git a/resources/js/components/transactions/category-select.tsx b/resources/js/components/transactions/category-select.tsx index 96879e45..24def989 100644 --- a/resources/js/components/transactions/category-select.tsx +++ b/resources/js/components/transactions/category-select.tsx @@ -104,5 +104,3 @@ export function CategorySelect({ ); } - - diff --git a/resources/js/components/transactions/edit-transaction-dialog.tsx b/resources/js/components/transactions/edit-transaction-dialog.tsx index 39c27a36..fc5f856b 100644 --- a/resources/js/components/transactions/edit-transaction-dialog.tsx +++ b/resources/js/components/transactions/edit-transaction-dialog.tsx @@ -315,18 +315,22 @@ export function EditTransactionDialog({ /> ) : (
- {transaction && (() => { - const date = parseISO( - transaction.transaction_date, - ); - const currentYear = getYear(new Date()); - const transactionYear = getYear(date); - const formatString = - transactionYear === currentYear - ? 'MMMM d' - : 'MMMM d, yyyy'; - return format(date, formatString); - })()} + {transaction && + (() => { + const date = parseISO( + transaction.transaction_date, + ); + const currentYear = getYear( + new Date(), + ); + const transactionYear = + getYear(date); + const formatString = + transactionYear === currentYear + ? 'MMMM d' + : 'MMMM d, yyyy'; + return format(date, formatString); + })()}
)}
@@ -399,7 +403,9 @@ export function EditTransactionDialog({ new Intl.NumberFormat('en-US', { style: 'currency', currency: transaction.currency_code, - }).format(parseFloat(transaction.amount))} + }).format( + parseFloat(transaction.amount), + )}
)}
diff --git a/resources/js/components/transactions/import-transactions-button.tsx b/resources/js/components/transactions/import-transactions-button.tsx index 43f3a4b5..4f3196ce 100644 --- a/resources/js/components/transactions/import-transactions-button.tsx +++ b/resources/js/components/transactions/import-transactions-button.tsx @@ -53,7 +53,9 @@ export function ImportTransactionsButton({ - {!isKeySet ? `Unlock encryption to import transactions` : `Import transactions from CSV/Excel`} + {!isKeySet + ? `Unlock encryption to import transactions` + : `Import transactions from CSV/Excel`} diff --git a/resources/js/components/transactions/import-transactions-drawer.tsx b/resources/js/components/transactions/import-transactions-drawer.tsx index cb52b810..2476121a 100644 --- a/resources/js/components/transactions/import-transactions-drawer.tsx +++ b/resources/js/components/transactions/import-transactions-drawer.tsx @@ -29,7 +29,7 @@ import { type ColumnMapping, type ImportState, } from '@/types/import'; -import { Check, Loader2 } from 'lucide-react'; +import { Check } from 'lucide-react'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { ImportStepAccount } from './import-step-account'; diff --git a/resources/js/components/transactions/transaction-actions-menu.tsx b/resources/js/components/transactions/transaction-actions-menu.tsx index 56eeb787..05d6eb07 100644 --- a/resources/js/components/transactions/transaction-actions-menu.tsx +++ b/resources/js/components/transactions/transaction-actions-menu.tsx @@ -13,9 +13,11 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { useEncryptionKey } from '@/contexts/encryption-key-context'; +import { useReEvaluateAllTransactions } from '@/hooks/use-re-evaluate-all-transactions'; import { type Account, type Bank } from '@/types/account'; import { type Category } from '@/types/category'; -import { ChevronDown, Plus, Upload } from 'lucide-react'; +import { type DecryptedTransaction } from '@/types/transaction'; +import { ChevronDown, Plus, Upload, Zap } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; import { ImportTransactionsDrawer } from './import-transactions-drawer'; @@ -25,6 +27,8 @@ interface TransactionActionsMenuProps { accounts: Account[]; banks: Bank[]; onAddTransaction: () => void; + transactions: DecryptedTransaction[]; + onReEvaluateComplete?: () => void; } export function TransactionActionsMenu({ @@ -32,9 +36,13 @@ export function TransactionActionsMenu({ accounts, banks, onAddTransaction, + transactions, + onReEvaluateComplete, }: TransactionActionsMenuProps) { const { isKeySet } = useEncryptionKey(); const [importDrawerOpen, setImportDrawerOpen] = useState(false); + const [isReEvaluating, setIsReEvaluating] = useState(false); + const { reEvaluateAll } = useReEvaluateAllTransactions(); const handleAddTransaction = () => { if (!isKeySet) { @@ -56,6 +64,28 @@ export function TransactionActionsMenu({ setImportDrawerOpen(true); }; + const handleReEvaluateAll = async () => { + if (!isKeySet) { + toast.error( + 'Please unlock your encryption key to re-evaluate transactions', + ); + return; + } + + if (!transactions.length) { + toast.error('No transactions to re-evaluate'); + return; + } + + setIsReEvaluating(true); + try { + await reEvaluateAll(transactions, categories, accounts, banks); + onReEvaluateComplete?.(); + } finally { + setIsReEvaluating(false); + } + }; + return ( <> @@ -64,8 +94,12 @@ export function TransactionActionsMenu({