mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:08:46 +02:00
Undo grace for transaction deletes (UX-10)
After confirming a delete the row disappears immediately but the API call waits 5 seconds behind a Deshacer toast action; undo restores the row without any server round-trip. A failed commit restores the row and reports the error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Transaction | null>(null);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [pendingDeletes, setPendingDeletes] = useState<Set<number>>(new Set());
|
||||
const deleteTimers = useRef<Map<number, ReturnType<typeof setTimeout>>>(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({
|
||||
<CardContent className="p-0">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={transactions}
|
||||
data={visibleTransactions}
|
||||
pagination
|
||||
pageSize={25}
|
||||
initialSorting={[{ id: 'date', desc: true }]}
|
||||
@@ -220,7 +256,6 @@ export default function TransactionList({
|
||||
message="This transaction will be permanently deleted. This action cannot be undone."
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setDeleteId(null)}
|
||||
loading={deleting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user