mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 12:28:48 +02:00
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:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user