diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 032af38a..c389a8b1 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -92,7 +92,7 @@ class BudgetController extends Controller ->where('user_id', $user->id) ->with('bank:id,name,logo') ->orderBy('name') - ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']); + ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']); $banks = Bank::query() ->availableForUser($user) diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 29e2180d..ef82c69c 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -12,6 +12,7 @@ use App\Models\Bank; use App\Models\Category; use App\Models\Label; use App\Models\Transaction; +use App\Services\ManualBalanceAdjuster; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -79,7 +80,7 @@ class TransactionController extends Controller ->where('user_id', $user->id) ->with('bank:id,name,logo') ->orderBy('name') - ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']); + ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']); $banks = Bank::query() ->availableForUser($user) @@ -121,7 +122,7 @@ class TransactionController extends Controller ->where('user_id', $user->id) ->with('bank:id,name,logo') ->orderBy('name') - ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']); + ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']); $banks = Bank::query() ->availableForUser($user) @@ -213,10 +214,14 @@ class TransactionController extends Controller ]); } - public function destroy(Request $request, Transaction $transaction): JsonResponse + public function destroy(Request $request, Transaction $transaction, ManualBalanceAdjuster $balanceAdjuster): JsonResponse { $this->authorize('delete', $transaction); + if ($request->boolean('update_balance')) { + $balanceAdjuster->reverseDeletedTransaction($transaction); + } + $transaction->delete(); return response()->json([ diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 33fecc9a..2f650e7b 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -124,7 +124,7 @@ class HandleInertiaRequests extends Middleware 'accounts' => fn () => $user ? $user->accounts() ->with(['bank:id,name,logo', 'realEstateDetail:account_id,linked_loan_account_id']) ->orderBy('name') - ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']) + ->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']) ->map(function ($account) { $data = $account->only([ 'id', @@ -134,6 +134,7 @@ class HandleInertiaRequests extends Middleware 'bank_id', 'type', 'currency_code', + 'banking_connection_id', 'bank', ]); diff --git a/app/Services/ManualBalanceAdjuster.php b/app/Services/ManualBalanceAdjuster.php new file mode 100644 index 00000000..3fada2bb --- /dev/null +++ b/app/Services/ManualBalanceAdjuster.php @@ -0,0 +1,43 @@ +account; + + if ($account === null || $account->isConnected()) { + return; + } + + $today = Carbon::now()->toDateString(); + + $currentBalance = $account->balances() + ->where('balance_date', '<=', $today) + ->orderByDesc('balance_date') + ->value('balance') ?? 0; + + AccountBalance::updateOrCreate( + [ + 'account_id' => $account->id, + 'balance_date' => $today, + ], + [ + 'balance' => $currentBalance - $transaction->amount, + ], + ); + } +} diff --git a/lang/es.json b/lang/es.json index cdbced96..7f0e3b4a 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1560,6 +1560,8 @@ "Update the balance record.": "Actualiza el registro de balance.", "Update the category and notes for this transaction.": "Actualiza la categoría y notas de esta transacción.", "Update the category information.": "Actualiza la información de la categoría.", + "Update the current balance of the affected manual accounts to reflect this change.": "Actualiza el saldo actual de las cuentas manuales afectadas para reflejar este cambio.", + "Update the current balance of the manual account to reflect this change.": "Actualiza el saldo actual de la cuenta manual para reflejar este cambio.", "Update the label information.": "Actualiza la información de la etiqueta.", "Update the owed amount record.": "Actualiza el registro de monto adeudado.", "Update the rule to automatically categorize transactions and add labels.": "Actualiza la regla para categorizar automáticamente transacciones y agregar etiquetas.", diff --git a/resources/js/components/transactions/transaction-list.tsx b/resources/js/components/transactions/transaction-list.tsx index b504e0d1..4924a5e0 100644 --- a/resources/js/components/transactions/transaction-list.tsx +++ b/resources/js/components/transactions/transaction-list.tsx @@ -50,6 +50,7 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; import { ContextMenu, ContextMenuContent, @@ -240,6 +241,7 @@ export interface TransactionListProps { headerActions?: ReactNode; maxHeight?: number; hideColumns?: string[]; + onBalanceUpdated?: () => void; } export function TransactionList({ @@ -256,6 +258,7 @@ export function TransactionList({ headerActions, maxHeight, hideColumns = [], + onBalanceUpdated, }: TransactionListProps) { const { isKeySet } = useEncryptionKey(); const locale = useLocale(); @@ -308,6 +311,7 @@ export function TransactionList({ const [deleteTransaction, setDeleteTransaction] = useState(null); const [isBulkDeleteMode, setIsBulkDeleteMode] = useState(false); + const [updateBalanceOnDelete, setUpdateBalanceOnDelete] = useState(true); const [isDeleting, setIsDeleting] = useState(false); const [isBulkDeleting, setIsBulkDeleting] = useState(false); const [isBulkUpdating, setIsBulkUpdating] = useState(false); @@ -766,6 +770,16 @@ export function TransactionList({ searchInIndexedDB(); }, [filters.searchText, isKeySet, accountId]); + const manualAccountIds = useMemo( + () => + new Set( + accounts + .filter((account) => account.banking_connection_id === null) + .map((account) => account.id), + ), + [accounts], + ); + const filteredTransactions = useMemo(() => { return transactions.filter((transaction) => { if (filters.searchText && isKeySet) { @@ -1116,8 +1130,16 @@ export function TransactionList({ } setIsDeleting(true); + const balanceWasUpdated = + updateBalanceOnDelete && + manualAccountIds.has(deleteTransaction.account_id); try { - await transactionSyncService.delete(deleteTransaction.id); + await transactionSyncService.delete(deleteTransaction.id, { + updateBalance: balanceWasUpdated, + }); + if (balanceWasUpdated) { + onBalanceUpdated?.(); + } setTransactions((previous) => previous.filter( (transaction) => transaction.id !== deleteTransaction.id, @@ -1245,8 +1267,20 @@ export function TransactionList({ } setIsBulkDeleting(true); + const balanceMayHaveUpdated = + updateBalanceOnDelete && + transactions.some( + (transaction) => + selectedIds.includes(transaction.id) && + manualAccountIds.has(transaction.account_id), + ); try { - await transactionSyncService.deleteMany(selectedIds); + await transactionSyncService.deleteMany(selectedIds, { + updateBalance: updateBalanceOnDelete, + }); + if (balanceMayHaveUpdated) { + onBalanceUpdated?.(); + } setTransactions((previous) => previous.filter( (transaction) => !selectedIds.includes(transaction.id), @@ -1461,6 +1495,7 @@ export function TransactionList({ if (!open) { setDeleteTransaction(null); setIsBulkDeleteMode(false); + setUpdateBalanceOnDelete(true); } }} > @@ -1477,6 +1512,33 @@ export function TransactionList({ : 'Are you sure you want to delete this transaction? This action cannot be undone.'} + {(isBulkDeleteMode + ? transactions.some( + (transaction) => + Object.keys(rowSelection).includes( + transaction.id, + ) && + manualAccountIds.has(transaction.account_id), + ) + : deleteTransaction !== null && + manualAccountIds.has( + deleteTransaction.account_id, + )) && ( + + )} + setChartRefreshKey((key) => key + 1) + } /> )} diff --git a/resources/js/pages/transactions/index.tsx b/resources/js/pages/transactions/index.tsx index 018390ab..d4b758c9 100644 --- a/resources/js/pages/transactions/index.tsx +++ b/resources/js/pages/transactions/index.tsx @@ -51,6 +51,7 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; import { ContextMenu, ContextMenuContent, @@ -457,6 +458,7 @@ export default function Transactions({ const [deleteTransaction, setDeleteTransaction] = useState(null); const [isBulkDeleteMode, setIsBulkDeleteMode] = useState(false); + const [updateBalanceOnDelete, setUpdateBalanceOnDelete] = useState(true); const [bulkDeleteConfirmation, setBulkDeleteConfirmation] = useState(''); const [isDeleting, setIsDeleting] = useState(false); const [isBulkDeleting, setIsBulkDeleting] = useState(false); @@ -874,7 +876,11 @@ export default function Transactions({ setIsDeleting(true); try { - await transactionSyncService.delete(deleteTransaction.id); + await transactionSyncService.delete(deleteTransaction.id, { + updateBalance: + updateBalanceOnDelete && + manualAccountIds.has(deleteTransaction.account_id), + }); setAllTransactions((previous) => previous.filter( (transaction) => transaction.id !== deleteTransaction.id, @@ -963,6 +969,28 @@ export default function Transactions({ const selectedCount = useMemo(() => selectedIds.length, [selectedIds]); + const manualAccountIds = useMemo( + () => + new Set( + accounts + .filter((account) => account.banking_connection_id === null) + .map((account) => account.id), + ), + [accounts], + ); + + const bulkDeleteTouchesManualAccount = useMemo( + () => + isSelectingAll + ? manualAccountIds.size > 0 + : allTransactions.some( + (transaction) => + selectedIds.includes(transaction.id) && + manualAccountIds.has(transaction.account_id), + ), + [isSelectingAll, manualAccountIds, allTransactions, selectedIds], + ); + const bulkDeleteConfirmationText = useMemo( () => getBulkDeleteConfirmationText(selectedCount), [selectedCount], @@ -990,7 +1018,9 @@ export default function Transactions({ setIsBulkDeleting(true); try { - await transactionSyncService.deleteMany(selectedIds); + await transactionSyncService.deleteMany(selectedIds, { + updateBalance: updateBalanceOnDelete, + }); setAllTransactions((previous) => previous.filter( (transaction) => !selectedIds.includes(transaction.id), @@ -1280,6 +1310,7 @@ export default function Transactions({ onOpenChange={(open) => { if (!open) { setDeleteTransaction(null); + setUpdateBalanceOnDelete(true); } }} > @@ -1294,6 +1325,25 @@ export default function Transactions({ )} + {deleteTransaction !== null && + manualAccountIds.has(deleteTransaction.account_id) && ( + + )} {__('Cancel')} @@ -1316,6 +1366,7 @@ export default function Transactions({ setDeleteTransaction(null); setIsBulkDeleteMode(false); setBulkDeleteConfirmation(''); + setUpdateBalanceOnDelete(true); } }} > @@ -1340,6 +1391,22 @@ export default function Transactions({ disabled={isBulkDeleting} autoFocus /> + {bulkDeleteTouchesManualAccount && ( + + )}