diff --git a/frontend/src/components/TransactionList.tsx b/frontend/src/components/TransactionList.tsx index 9c54aab..e073cc3 100644 --- a/frontend/src/components/TransactionList.tsx +++ b/frontend/src/components/TransactionList.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { Plus, Search, @@ -56,34 +56,70 @@ export default function TransactionList({ const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleteId, setDeleteId] = useState(null); - const [deleting, setDeleting] = useState(false); + const [pendingDeletes, setPendingDeletes] = useState>(new Set()); + const deleteTimers = useRef>>(new Map()); const handleEdit = (tx: Transaction) => { setEditing(tx); setModalOpen(true); }; - const handleDelete = async () => { - if (deleteId === null) return; - setDeleting(true); + const commitDelete = async (id: number) => { + deleteTimers.current.delete(id); try { - await api.delete(`/transactions/${deleteId}`); - toast.success('Transacción eliminada'); - setDeleteId(null); + await api.delete(`/transactions/${id}`); onRefresh(); } catch { toast.error('No se pudo eliminar la transacción'); - } finally { - setDeleting(false); + setPendingDeletes((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); } }; + // Undo grace: the row disappears immediately, the API call fires after 5s + // unless the user clicks Deshacer (UX-10). + const handleDelete = () => { + if (deleteId === null) return; + const id = deleteId; + setDeleteId(null); + setPendingDeletes((prev) => new Set(prev).add(id)); + const toastId = toast('Transacción eliminada', { + duration: 5000, + action: { + label: 'Deshacer', + onClick: () => { + const timer = deleteTimers.current.get(id); + if (timer) clearTimeout(timer); + deleteTimers.current.delete(id); + setPendingDeletes((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, + }, + }); + deleteTimers.current.set( + id, + setTimeout(() => { + toast.dismiss(toastId); + void commitDelete(id); + }, 5000), + ); + }; + const columns = useMemo( () => getTransactionColumns({ showCategory, showSourceIcon, onEdit: handleEdit, onDelete: (id) => setDeleteId(id), onToggleDeferred }), [showCategory, showSourceIcon, onToggleDeferred], ); - const empty = transactions.length === 0 && !loading; + const visibleTransactions = pendingDeletes.size + ? transactions.filter((t) => !pendingDeletes.has(t.id)) + : transactions; + const empty = visibleTransactions.length === 0 && !loading; return ( <> @@ -195,7 +231,7 @@ export default function TransactionList({ setDeleteId(null)} - loading={deleting} /> )}