From 15d28ce2e8a88cf18e1dedd6d25cdd9f0421b0d1 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 18 Jul 2025 14:37:46 +0000 Subject: [PATCH] Fixed Retirement lasts X years --- src/components/CareerRoadmap.js | 64 +++++++++++++++++++--------- src/components/RetirementPlanner.js | 1 + src/components/ScenarioContainer.js | 56 ++++++++++++++++-------- user_profile.db | Bin 208896 -> 208896 bytes 4 files changed, 84 insertions(+), 37 deletions(-) diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index 9f594db..8924e41 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -368,6 +368,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { const [drawerMilestone, setDrawerMilestone] = useState(null); const [impactsById, setImpactsById] = useState({}); // id → [impacts] const [addingNewMilestone, setAddingNewMilestone] = useState(false); + const [showMissingBanner, setShowMissingBanner] = useState(false); // Config @@ -573,16 +574,14 @@ useEffect(() => { const dataReady = !!scenarioRow && !!financialProfile && collegeProfile !== null; useEffect(() => { + if (!dataReady || !careerProfileId) return; // wait for all rows - if (!dataReady || !careerProfileId) return; + /* run once per profile‑id ------------------------------------------------ */ + if (modalGuard.current.checked) return; + modalGuard.current.checked = true; - // one key per career profile - const key = `modalChecked:${careerProfileId}`; - - // already checked in this browser session? - if (sessionStorage.getItem(key) === '1') return; - - const status = (scenarioRow.college_enrollment_status || '').toLowerCase(); + /* derive once, local to this effect -------------------------------------- */ + const status = (scenarioRow?.college_enrollment_status || '').toLowerCase(); const requireCollege = ['currently_enrolled','prospective_student','deferred'] .includes(status); @@ -591,18 +590,24 @@ useEffect(() => { { requireCollegeData: requireCollege } ); - if (missing.length) setShowEditModal(true); - - sessionStorage.setItem(key, '1'); // remember for this tab - + if (missing.length) { + /* if we arrived *directly* from onboarding we silently skip the banner + once, but we still want the Edit‑Scenario modal to open */ + if (modalGuard.current.skip) { + setShowEditModal(true); + } else { + setShowMissingBanner(true); + } + } }, [dataReady, scenarioRow, financialProfile, collegeProfile, careerProfileId]); + + useEffect(() => { if ( financialProfile && scenarioRow && - collegeProfile && - scenarioMilestones.length + collegeProfile ) { buildProjection(scenarioMilestones); // uses the latest scenarioMilestones } @@ -949,7 +954,7 @@ useEffect(() => { // 8) Build financial projection async function buildProjection(milestones) { if (!milestones?.length) return; - const allMilestones = milestones; + const allMilestones = milestones || []; try { setScenarioMilestones(allMilestones); @@ -1302,9 +1307,8 @@ const fetchMilestones = useCallback(async () => { setScenarioRow={setScenarioRow} careerProfileId={careerProfileId} collegeProfile={collegeProfile} - onMilestonesCreated={() => { - /* refresh or reload logic here */ - }} + onMilestonesCreated={handleMilestonesCreated} + onAiRiskFetched={(riskData) => { @@ -1447,6 +1451,28 @@ const fetchMilestones = useCallback(async () => { */} {/* --- FINANCIAL PROJECTION SECTION -------------------------------- */} + + {showMissingBanner && ( +
+

+ We need a few basics (income, expenses, etc.) before we can show a full + projection. +

+ + +
+ )} +

Financial Projection @@ -1600,8 +1626,8 @@ const fetchMilestones = useCallback(async () => { milestone={milestoneForModal} /* ← edit mode */ fetchMilestones={fetchMilestones} onClose={(didSave) => { + if (didSave) handleMilestonesCreated(); setMilestoneForModal(null); - if (didSave) fetchMilestones(); }} /> )} diff --git a/src/components/RetirementPlanner.js b/src/components/RetirementPlanner.js index c6a105b..4a6dbbb 100644 --- a/src/components/RetirementPlanner.js +++ b/src/components/RetirementPlanner.js @@ -42,6 +42,7 @@ export default function RetirementPlanner () { const isMobile = useIsMobile(); const { openRetire } = useContext(ChatCtx); + /* ----------------------- data loading -------------------------- */ const loadAll = useCallback(async () => { try { diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js index 7bedb4c..644501c 100644 --- a/src/components/ScenarioContainer.js +++ b/src/components/ScenarioContainer.js @@ -230,10 +230,7 @@ export default function ScenarioContainer({ }; // Gather milestoneImpacts - let allImpacts = []; - Object.keys(impactsByMilestone).forEach((mId) => { - allImpacts = allImpacts.concat(impactsByMilestone[mId]); - }); + const allImpacts = Object.values(impactsByMilestone).flat(); // safe even if [] const simYears = parseInt(simulationYearsInput, 10) || 20; const simYearsUI = Math.max(1, parseInt(simulationYearsInput, 10) || 20); @@ -315,13 +312,20 @@ export default function ScenarioContainer({ surplusEmergencyAllocation: scenarioOverrides.surplusEmergencyAllocation, surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation, additionalIncome: scenarioOverrides.additionalIncome, - retirement_start_date: localScenario.retirement_start_date - || localScenario.projected_end_date - || null, - desired_retirement_income_monthly: parseScenarioOverride( - localScenario.desired_retirement_income_monthly, - 0 - ), + retirement_start_date: + localScenario.retirement_start_date // user picked + || (localScenario.projected_end_date // often set for college scenarios + ? moment(localScenario.projected_end_date) + .startOf('month') + .add(1,'month') // start drawing a month later + .format('YYYY-MM-DD') + : null), + + desired_retirement_income_monthly: + parseScenarioOverride( + localScenario.desired_retirement_income_monthly, + scenarioOverrides.monthlyExpenses // ← fallback to current spend + ), studentLoanAmount: collegeData.studentLoanAmount, interestRate: collegeData.interestRate, @@ -354,13 +358,12 @@ export default function ScenarioContainer({ simulateFinancialProjection(mergedProfile); - const sliceTo = simYearsUI * 12; // months we want to keep - let cumulative = mergedProfile.emergencySavings || 0; - - const finalData = pData.slice(0, sliceTo).map(row => { - cumulative += row.netSavings || 0; - return { ...row, cumulativeNetSavings: cumulative }; - }); + const sliceTo = simYearsUI * 12; + let cumulative = mergedProfile.emergencySavings || 0; + const finalData = pData.map(row => { + cumulative += row.netSavings || 0; + return { ...row, cumulativeNetSavings: cumulative }; + }).slice(0, sliceTo); if (typeof onSimDone === 'function') { onSimDone(localScenario.id, yc); @@ -958,6 +961,23 @@ return (

+ {(!localScenario?.retirement_start_date || + !localScenario?.desired_retirement_income_monthly) && ( +
+

+ Add a retirement date and spending goal to see  + Money Lasts. +

+ +
+ )} + {/* ───────────── KPI Bar ───────────── */} {projectionData.length > 0 && (
diff --git a/user_profile.db b/user_profile.db index 140a195b7cc4a5baefb61a450d6d5256def776e1..20d38533537deb740391c4ee502b9ba68772e9ec 100644 GIT binary patch delta 398 zcmYk1K}!Nb6vxv&mE(E{g3#i5>!9LVmUi0(pkbKD(mXm)0snYE-tnn8H0LwJm! zbI?INMc<%{U%h|FGe((Q(oeAnp(0x2KeMszwrrp}G7JB(kl#yOCpUijk zY3`cRd_~W($Jhb-h@B%n+zeMy%rufAE1$M<#bPFFmG62eJnA8%ySidT`hknak^Wov zDzbHRSYA$A(dfo)eEUy0CUvMdF6C5diM5bwF#8I}*GWsT7D>TH!0~@RNY27**OY^c zIc`$k1ng_!Voe-SZKa8;Qb4)Qd}j_7v3*H(8(Kc&MB+MSR8yh=a|dkUdW>Rj6QD}@ zL}j3Q8qD051gp8e*dUkl1EK+yqu+=V6%d~}zzB9g3-E*{TvNEao)jLD otPQTFCr&1p&E?bCy|h&TYo}xtN>*+(P8n(A2av6Yi2wiq delta 86 zcmV-c0IC0g;0%D^43HZE>X95n0qTKZwO|3H{~A6Fg8&Yn4!sP{4mJ*Q4z3KL50wu@ s48IR?wIBji43Ur#hl30Pw}T7=ffEP<4ATG(pA6HnfyWHD(<}om13!@+O#lD@