Pension assumptions editable + dirty-form guard + empty-state CTA

Per-fund contribution and annual-rate assumptions are now editable in a
Supuestos dialog on Pensiones and persisted server-side in UserSettings
so projections survive devices and reloads; saldo inicial keeps coming
from the latest snapshot (UX-12). The recurring-item dialog confirms
before discarding unsaved edits, with the dirty baseline computed from
the same values the populate effect sets (UX-06). Empty transaction
lists now offer an inline add button (UX-14).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-11 21:14:44 -06:00
parent 0dfac6a285
commit 8713eaa4d8
5 changed files with 283 additions and 33 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useMemo, useCallback, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import ErrorState from '@/components/ErrorState';
import PensionAssumptionsDialog, { type PensionAssumptions } from '@/components/PensionAssumptionsDialog';
import {
LineChart,
Line,
@@ -31,6 +32,7 @@ import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import {
getUserSettings,
uploadPensionPDFs,
getPensionFundSummary,
getPensionSnapshots,
@@ -38,7 +40,7 @@ import {
type PensionUploadResult,
} from '@/lib/api';
import PensionManualEntryModal from '@/components/PensionManualEntryModal';
import { ClipboardPaste } from 'lucide-react';
import { ClipboardPaste, SlidersHorizontal } from 'lucide-react';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -196,8 +198,9 @@ function calcProjection(
/** Merge API snapshots into the default fund definitions. */
function applySnapshots(
snapshots: PensionSnapshot[],
base: Record<FundKey, FundDef> = FUNDS_DEFAULT,
): Record<FundKey, FundDef> {
const funds = { ...FUNDS_DEFAULT };
const funds = { ...base };
for (const snap of snapshots) {
const key = snap.fund as FundKey;
if (key in funds) {
@@ -273,7 +276,46 @@ export default function Pensions() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pensionQ.refetch]);
const FUNDS = useMemo(() => applySnapshots(fundSummary), [fundSummary]);
const settingsQ = useQuery({
queryKey: ['settings'],
queryFn: () => getUserSettings().then((r) => r.data),
});
const storedAssumptions = (settingsQ.data?.data?.pension_assumptions ??
{}) as PensionAssumptions;
const FUNDS = useMemo(() => {
const base = { ...FUNDS_DEFAULT };
for (const key of FUND_KEYS) {
const stored = storedAssumptions[key];
if (stored) {
base[key] = {
...base[key],
monthlyContribution: stored.monthlyContribution,
annualRate: stored.annualRate,
};
}
}
return applySnapshots(fundSummary, base);
}, [fundSummary, storedAssumptions]);
const currentAssumptions = useMemo(
() => ({
FCL: {
monthlyContribution: FUNDS.FCL.monthlyContribution,
annualRate: FUNDS.FCL.annualRate,
},
ROP: {
monthlyContribution: FUNDS.ROP.monthlyContribution,
annualRate: FUNDS.ROP.annualRate,
},
VOL: {
monthlyContribution: FUNDS.VOL.monthlyContribution,
annualRate: FUNDS.VOL.annualRate,
},
}),
[FUNDS],
);
const [showAssumptions, setShowAssumptions] = useState(false);
// Build a map of fund -> latest snapshot for rendimientos display
const snapshotByFund = useMemo(() => {
@@ -628,10 +670,16 @@ export default function Pensions() {
{/* ── Section 4: Projections ───────────────────────────────────────── */}
<section className="space-y-3">
<h2 className="text-base font-semibold font-heading flex items-center gap-2 text-muted-foreground uppercase tracking-wider">
<TrendingUp className="w-4 h-4" />
Proyecciones
</h2>
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold font-heading flex items-center gap-2 text-muted-foreground uppercase tracking-wider">
<TrendingUp className="w-4 h-4" />
Proyecciones
</h2>
<Button variant="outline" size="sm" onClick={() => setShowAssumptions(true)}>
<SlidersHorizontal className="w-4 h-4 mr-2" aria-hidden="true" />
Supuestos
</Button>
</div>
<p className="text-sm text-muted-foreground">
Basado en edad actual de {CURRENT_AGE} años. Edita los campos para simular escenarios.
</p>
@@ -710,6 +758,14 @@ export default function Pensions() {
</section>
{/* ── Manual Entry Modal ──────────────────────────────────────────── */}
{showAssumptions && (
<PensionAssumptionsDialog
current={currentAssumptions}
settingsData={settingsQ.data?.data ?? {}}
onClose={() => setShowAssumptions(false)}
/>
)}
{showManualEntry && (
<PensionManualEntryModal
onClose={() => setShowManualEntry(false)}