Mutation feedback: toasts on transaction save/delete, confirm override clear

Transaction create/update/delete now confirm success or failure with a
toast (UX-01); save failures always show a user-visible message instead
of console-only (FE-06, with a typed error guard replacing the any
cast). Clearing a balance override — which recalculates every following
month — now requires confirmation like the other destructive actions
(UX-15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-10 17:28:21 -06:00
parent 7a1733bf7e
commit 2a68a05ffd
3 changed files with 42 additions and 3 deletions

View File

@@ -11,6 +11,8 @@ import {
Banknote, Banknote,
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner';
import api, { type Transaction } from '@/lib/api'; import api, { type Transaction } from '@/lib/api';
import TransactionModal from './TransactionModal'; import TransactionModal from './TransactionModal';
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog';
@@ -66,8 +68,11 @@ export default function TransactionList({
setDeleting(true); setDeleting(true);
try { try {
await api.delete(`/transactions/${deleteId}`); await api.delete(`/transactions/${deleteId}`);
toast.success('Transacción eliminada');
setDeleteId(null); setDeleteId(null);
onRefresh(); onRefresh();
} catch {
toast.error('No se pudo eliminar la transacción');
} finally { } finally {
setDeleting(false); setDeleting(false);
} }

View File

@@ -1,4 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import api, { type Category, type Transaction } from '@/lib/api'; import api, { type Category, type Transaction } from '@/lib/api';
import { formatLocalDatetime } from '@/lib/format'; import { formatLocalDatetime } from '@/lib/format';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -92,12 +94,18 @@ export default function TransactionModal({ transaction, source, onClose, onSaved
} else { } else {
await api.post('/transactions/', payload); await api.post('/transactions/', payload);
} }
toast.success(transaction ? 'Transacción actualizada' : 'Transacción creada');
onSaved(); onSaved();
onClose(); onClose();
} catch (err: any) { } catch (err) {
if (err.response?.status === 409) { const status =
err && typeof err === 'object' && 'response' in err
? (err as { response: { status: number } }).response.status
: null;
if (status === 409) {
setError('Duplicate transaction: a transaction with this reference already exists.'); setError('Duplicate transaction: a transaction with this reference already exists.');
} else { } else {
setError('No se pudo guardar la transacción.');
console.error(err); console.error(err);
} }
} finally { } finally {

View File

@@ -1,3 +1,4 @@
import ConfirmDialog from '@/components/ConfirmDialog';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Pencil } from 'lucide-react'; import { Pencil } from 'lucide-react';
@@ -42,6 +43,7 @@ export default function YearlyOverview({
const currentMonth = new Date().getMonth() + 1; const currentMonth = new Date().getMonth() + 1;
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const [editingMonth, setEditingMonth] = useState<number | null>(null); const [editingMonth, setEditingMonth] = useState<number | null>(null);
const [confirmClearMonth, setConfirmClearMonth] = useState<number | null>(null);
const [editValue, setEditValue] = useState(''); const [editValue, setEditValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -61,7 +63,16 @@ export default function YearlyOverview({
if (editingMonth === null) return; if (editingMonth === null) return;
const trimmed = editValue.trim(); const trimmed = editValue.trim();
if (trimmed === '') { if (trimmed === '') {
await onClearOverride(editingMonth); // Clearing an override is destructive — confirm first (it changes the
// cumulative chain for every following month).
const hadOverride = months.find(
(m) => m.month === editingMonth,
)?.balance_overridden;
if (hadOverride) {
setConfirmClearMonth(editingMonth);
setEditingMonth(null);
return;
}
} else { } else {
const num = parseFloat(trimmed); const num = parseFloat(trimmed);
if (!isNaN(num)) { if (!isNaN(num)) {
@@ -71,6 +82,12 @@ export default function YearlyOverview({
setEditingMonth(null); setEditingMonth(null);
}; };
const handleConfirmClear = async () => {
if (confirmClearMonth === null) return;
await onClearOverride(confirmClearMonth);
setConfirmClearMonth(null);
};
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleSave(); handleSave();
@@ -202,6 +219,15 @@ export default function YearlyOverview({
})} })}
</TableBody> </TableBody>
</Table> </Table>
{confirmClearMonth !== null && (
<ConfirmDialog
title="Eliminar ajuste de balance"
message={`El balance de ${MONTH_NAMES[confirmClearMonth] ?? confirmClearMonth} volverá al valor calculado y los meses siguientes se recalcularán.`}
confirmLabel="Eliminar ajuste"
onConfirm={handleConfirmClear}
onCancel={() => setConfirmClearMonth(null)}
/>
)}
</div> </div>
); );
} }