mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 13:48:47 +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 {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
@@ -56,34 +56,70 @@ export default function TransactionList({
|
|||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<Transaction | null>(null);
|
const [editing, setEditing] = useState<Transaction | null>(null);
|
||||||
const [deleteId, setDeleteId] = useState<number | 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) => {
|
const handleEdit = (tx: Transaction) => {
|
||||||
setEditing(tx);
|
setEditing(tx);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const commitDelete = async (id: number) => {
|
||||||
if (deleteId === null) return;
|
deleteTimers.current.delete(id);
|
||||||
setDeleting(true);
|
|
||||||
try {
|
try {
|
||||||
await api.delete(`/transactions/${deleteId}`);
|
await api.delete(`/transactions/${id}`);
|
||||||
toast.success('Transacción eliminada');
|
|
||||||
setDeleteId(null);
|
|
||||||
onRefresh();
|
onRefresh();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('No se pudo eliminar la transacción');
|
toast.error('No se pudo eliminar la transacción');
|
||||||
} finally {
|
setPendingDeletes((prev) => {
|
||||||
setDeleting(false);
|
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(
|
const columns = useMemo(
|
||||||
() => getTransactionColumns({ showCategory, showSourceIcon, onEdit: handleEdit, onDelete: (id) => setDeleteId(id), onToggleDeferred }),
|
() => getTransactionColumns({ showCategory, showSourceIcon, onEdit: handleEdit, onDelete: (id) => setDeleteId(id), onToggleDeferred }),
|
||||||
[showCategory, showSourceIcon, 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -195,7 +231,7 @@ export default function TransactionList({
|
|||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={transactions}
|
data={visibleTransactions}
|
||||||
pagination
|
pagination
|
||||||
pageSize={25}
|
pageSize={25}
|
||||||
initialSorting={[{ id: 'date', desc: true }]}
|
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."
|
message="This transaction will be permanently deleted. This action cannot be undone."
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
onCancel={() => setDeleteId(null)}
|
onCancel={() => setDeleteId(null)}
|
||||||
loading={deleting}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user