diff --git a/resources/js/lib/rule-engine.ts b/resources/js/lib/rule-engine.ts index 178d4af3..13037701 100644 --- a/resources/js/lib/rule-engine.ts +++ b/resources/js/lib/rule-engine.ts @@ -105,7 +105,7 @@ export async function prepareTransactionData( ), amount: transaction.amount / 100, transaction_date: transaction.transaction_date, - bank_name: bank?.name || '', + bank_name: (bank?.name || '').toLowerCase(), account_name: accountName.toLowerCase(), category: category?.name || null, notes: transaction.decryptedNotes @@ -257,7 +257,7 @@ export async function evaluateRulesForNewTransaction( ), amount: transactionData.amount, transaction_date: transactionData.transaction_date, - bank_name: bank?.name || '', + bank_name: (bank?.name || '').toLowerCase(), account_name: accountName.toLowerCase(), category: null, notes: transactionData.notes diff --git a/resources/js/pages/transactions/index.tsx b/resources/js/pages/transactions/index.tsx index b7203005..471ee44b 100644 --- a/resources/js/pages/transactions/index.tsx +++ b/resources/js/pages/transactions/index.tsx @@ -689,7 +689,6 @@ export default function Transactions({ categories, accounts, banks, - labels, key, ); @@ -776,10 +775,11 @@ export default function Transactions({ consoleDebug('=== Re-evaluation complete ==='); } }, - [isKeySet, categories, accounts, banks, labels, updateTransaction], + [isKeySet, categories, accounts, banks, updateTransaction], ); async function handleBulkReEvaluateRules() { + const BATCH_SIZE = 25; consoleDebug('=== Re-evaluating rules for bulk transactions ==='); consoleDebug(`Selected ${selectedIds.length} transactions`); @@ -822,7 +822,8 @@ export default function Transactions({ })), ); - const updates: Array<{ + // Collect all updates first without updating IndexedDB + const allUpdates: Array<{ transaction: DecryptedTransaction; categoryId: number | null; category: Category | null; @@ -831,6 +832,15 @@ export default function Transactions({ decryptedNotes: string | null; }> = []; + const dbUpdates: Array<{ + id: string; + data: { + category_id: number | null; + notes: string | null; + notes_iv: string | null; + }; + }> = []; + for (const transaction of selectedTransactions) { consoleDebug(`\nEvaluating transaction ${transaction.id}...`); const result = await evaluateRules( @@ -839,7 +849,6 @@ export default function Transactions({ categories, accounts, banks, - labels, key, ); @@ -877,13 +886,13 @@ export default function Transactions({ notes: finalNotes, notes_iv: finalNotesIv, }; - consoleDebug('Updating transaction with:', updateData); + consoleDebug('Queuing update for transaction:', updateData); - await transactionSyncService.update( - transaction.id, - updateData, - ); - consoleDebug('✓ Transaction updated in IndexedDB'); + // Collect for batch IndexedDB update + dbUpdates.push({ + id: transaction.id, + data: updateData, + }); const selectedCategory = result.categoryId ? categories.find((c) => c.id === result.categoryId) || @@ -899,7 +908,7 @@ export default function Transactions({ ); } - updates.push({ + allUpdates.push({ transaction, categoryId: result.categoryId, category: selectedCategory, @@ -917,34 +926,57 @@ export default function Transactions({ } } - consoleDebug(`\nApplying ${updates.length} updates to UI state...`); - if (updates.length > 0) { - setTransactions((previous) => - previous.map((transaction) => { - const update = updates.find( - (u) => u.transaction.id === transaction.id, - ); - if (update) { - consoleDebug( - `Updating UI for transaction ${transaction.id}:`, - { - newCategory: - update.category?.name || 'None', - hasNotes: !!update.decryptedNotes, - }, - ); - return { - ...transaction, - category_id: update.categoryId, - category: update.category, - notes: update.notes, - notes_iv: update.notesIv, - decryptedNotes: update.decryptedNotes, - }; - } - return transaction; - }), + // Batch update IndexedDB + if (dbUpdates.length > 0) { + consoleDebug( + `\nBatch updating ${dbUpdates.length} transactions in IndexedDB...`, ); + await transactionSyncService.updateManyIndividual(dbUpdates); + consoleDebug('✓ IndexedDB batch update complete'); + } + + // Update UI state in batches + consoleDebug( + `\nApplying ${allUpdates.length} updates to UI state in batches of ${BATCH_SIZE}...`, + ); + if (allUpdates.length > 0) { + for (let i = 0; i < allUpdates.length; i += BATCH_SIZE) { + const batch = allUpdates.slice(i, i + BATCH_SIZE); + const batchIds = new Set( + batch.map((u) => u.transaction.id), + ); + + consoleDebug( + `Applying batch ${Math.floor(i / BATCH_SIZE) + 1} (${batch.length} updates)...`, + ); + + setTransactions((previous) => + previous.map((transaction) => { + if (!batchIds.has(transaction.id)) { + return transaction; + } + const update = batch.find( + (u) => u.transaction.id === transaction.id, + ); + if (update) { + return { + ...transaction, + category_id: update.categoryId, + category: update.category, + notes: update.notes, + notes_iv: update.notesIv, + decryptedNotes: update.decryptedNotes, + }; + } + return transaction; + }), + ); + + // Small delay between batches to allow React to process renders + if (i + BATCH_SIZE < allUpdates.length) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + } consoleDebug('✓ UI state updated successfully'); } else { consoleDebug('❌ No updates to apply'); diff --git a/resources/js/services/transaction-sync.ts b/resources/js/services/transaction-sync.ts index 88008dad..c388e4a3 100644 --- a/resources/js/services/transaction-sync.ts +++ b/resources/js/services/transaction-sync.ts @@ -497,6 +497,50 @@ class TransactionSyncService { }); } + async updateManyIndividual( + updates: Array<{ id: string; data: TransactionUpdateData }>, + ): Promise { + const timestamp = new Date().toISOString(); + const dbUpdates: Transaction[] = []; + const pendingChanges: Array<{ + store: string; + operation: string; + data: Transaction; + timestamp: string; + }> = []; + + for (const { id, data } of updates) { + const existing = await this.getById(id); + + if (!existing) { + console.warn(`Transaction ${id} not found, skipping`); + continue; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { label_ids, ...transactionData } = data; + + const updated = { + ...existing, + ...transactionData, + updated_at: timestamp, + } as Transaction; + + dbUpdates.push(updated); + pendingChanges.push({ + store: 'transactions', + operation: 'update', + data: updated, + timestamp, + }); + } + + if (dbUpdates.length > 0) { + await db.transactions.bulkPut(dbUpdates); + await db.pending_changes.bulkAdd(pendingChanges); + } + } + async deleteMany(ids: string[]): Promise { const timestamp = new Date().toISOString();