P2 tail: cuota progress bars, Sync item rows, dialog polish
All checks were successful
Deploy to VPS / test (push) Successful in 1m29s
Deploy to VPS / deploy (push) Successful in 2m19s

Financiamientos shows cuota progress as Progress bars; Sincronización
uses a single Card of Item rows; Items Recurrentes table wrapped in a
Card like every other table; both window.confirm sites replaced with
the app's ConfirmDialog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 13:22:15 -06:00
parent 41d4306e86
commit 3a258537fc
5 changed files with 106 additions and 50 deletions

View File

@@ -25,6 +25,7 @@ import {
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Plus, Trash2 } from 'lucide-react';
import ConfirmDialog from '@/components/ConfirmDialog';
const TYPE_OPTIONS: { value: RecurringItemType; label: string }[] = [
{ value: 'INCOME', label: 'Ingreso' },
@@ -73,11 +74,13 @@ export default function RecurringItemDialog({
const snapshot = () =>
JSON.stringify([name, amount, itemType, frequency, dayOfMonth, monthOfYear, overrides, notes]);
const [confirmDiscard, setConfirmDiscard] = useState(false);
const handleOpenChange = (next: boolean) => {
if (!next && !saving && snapshot() !== baseline) {
// Closing with unsaved edits — confirm before discarding (UX-06).
const discard = window.confirm('Hay cambios sin guardar. ¿Descartarlos?');
if (!discard) return;
setConfirmDiscard(true);
return;
}
onOpenChange(next);
};
@@ -338,6 +341,19 @@ export default function RecurringItemDialog({
</Button>
</DialogFooter>
</DialogContent>
{confirmDiscard && (
<ConfirmDialog
title="Cambios sin guardar"
message="Hay cambios sin guardar. ¿Descartarlos?"
confirmLabel="Descartar"
onConfirm={() => {
setConfirmDiscard(false);
onOpenChange(false);
}}
onCancel={() => setConfirmDiscard(false)}
/>
)}
</Dialog>
);
}

View File

@@ -8,6 +8,7 @@ import {
import { formatAmount } from '@/lib/format';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { DataTable } from '@/components/ui/data-table';
import { DataTableColumnHeader } from '@/components/ui/data-table-column-header';
import { Pencil, Plus, Trash2 } from 'lucide-react';
@@ -153,14 +154,18 @@ export default function RecurringItemsManager({
</Button>
</div>
<DataTable
columns={columns}
data={items}
pagination
pageSize={20}
initialSorting={[{ id: 'item_type', desc: false }]}
emptyMessage="No hay items recurrentes."
/>
<Card>
<CardContent className="p-0">
<DataTable
columns={columns}
data={items}
pagination
pageSize={20}
initialSorting={[{ id: 'item_type', desc: false }]}
emptyMessage="No hay items recurrentes."
/>
</CardContent>
</Card>
<RecurringItemDialog
open={dialogOpen}

View File

@@ -20,6 +20,7 @@ import {
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import ConfirmDialog from '@/components/ConfirmDialog';
import { Label } from '@/components/ui/label';
/** Mirror of the backend cuota split (services/installments.py): truncate to
@@ -92,6 +93,7 @@ export default function ConvertToInstallmentsDialog({
: tx?.date ?? new Date().toISOString();
const [numStr, setNumStr] = useState('3');
const [confirmUnconvert, setConfirmUnconvert] = useState(false);
const [firstDate, setFirstDate] = useState('');
const [saving, setSaving] = useState(false);
@@ -142,10 +144,7 @@ export default function ConvertToInstallmentsDialog({
const handleUnconvert = async () => {
if (!effectivePlan) return;
const confirmed = window.confirm(
'Se eliminarán las cuotas y la compra volverá a contar como un solo cargo. ¿Continuar?',
);
if (!confirmed) return;
setConfirmUnconvert(false);
setSaving(true);
try {
await deleteInstallmentPlan(effectivePlan.id);
@@ -248,7 +247,7 @@ export default function ConvertToInstallmentsDialog({
<Button
variant="outline"
className="text-destructive hover:text-destructive"
onClick={handleUnconvert}
onClick={() => setConfirmUnconvert(true)}
disabled={saving}
>
Deshacer Tasa Cero
@@ -273,6 +272,17 @@ export default function ConvertToInstallmentsDialog({
</div>
</DialogFooter>
</DialogContent>
{confirmUnconvert && (
<ConfirmDialog
title="Deshacer Tasa Cero"
message="Se eliminarán las cuotas y la compra volverá a contar como un solo cargo. Esta acción no se puede deshacer."
confirmLabel="Deshacer plan"
loading={saving}
onConfirm={handleUnconvert}
onCancel={() => setConfirmUnconvert(false)}
/>
)}
</Dialog>
);
}