MultiScnearioView Final Retirement number and readiness score.

This commit is contained in:
Josh 2025-06-25 14:10:51 +00:00
parent a7d52598b6
commit cd57a139c1
3 changed files with 112 additions and 15 deletions

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { Line } from 'react-chartjs-2';
import { Chart as ChartJS } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import moment from 'moment';
import { Button } from './ui/button.js';
import authFetch from '../utils/authFetch.js';
@ -34,13 +35,16 @@ export default function ScenarioContainer({
// Projection
const [projectionData, setProjectionData] = useState([]);
const [loanPaidOffMonth, setLoanPaidOffMonth] = useState(null);
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
const [simulationYearsInput, setSimulationYearsInput] = useState('50');
const [readinessScore, setReadinessScore] = useState(null);
const [retireBalAtMilestone, setRetireBalAtMilestone] = useState(0);
// Interest
const [interestStrategy, setInterestStrategy] = useState('NONE');
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06);
const [randomRangeMin, setRandomRangeMin] = useState(-0.02);
const [randomRangeMax, setRandomRangeMax] = useState(0.02);
// The “milestone modal” for editing
const [showMilestoneModal, setShowMilestoneModal] = useState(false);
@ -218,6 +222,17 @@ export default function ScenarioContainer({
});
const simYears = parseInt(simulationYearsInput, 10) || 20;
const simYearsUI = Math.max(1, parseInt(simulationYearsInput, 10) || 20);
const yearsUntilRet = localScenario.retirement_start_date
? Math.ceil(
moment(localScenario.retirement_start_date)
.startOf('month')
.diff(moment().startOf('month'), 'months') / 12
)
: 0;
const simYearsEngine = Math.max(simYearsUI, yearsUntilRet + 1);
// scenario overrides
const scenarioOverrides = {
@ -311,7 +326,7 @@ export default function ScenarioContainer({
expectedSalary: collegeData.expectedSalary,
startDate: (localScenario.start_date || new Date().toISOString().slice(0,10)),
simulationYears: simYears,
simulationYears: simYearsEngine,
milestoneImpacts: allImpacts,
@ -321,17 +336,21 @@ export default function ScenarioContainer({
randomRangeMax
};
const { projectionData: pData, loanPaidOffMonth } =
const { projectionData: pData, loanPaidOffMonth, readinessScore:simReadiness, retirementAtMilestone } =
simulateFinancialProjection(mergedProfile);
const sliceTo = simYearsUI * 12; // months we want to keep
let cumulative = mergedProfile.emergencySavings || 0;
const finalData = pData.map((row) => {
const finalData = pData.slice(0, sliceTo).map(row => {
cumulative += row.netSavings || 0;
return { ...row, cumulativeNetSavings: cumulative };
});
setProjectionData(finalData);
setLoanPaidOffMonth(loanPaidOffMonth);
setReadinessScore(simReadiness);
setRetireBalAtMilestone(retirementAtMilestone);
}, [
financialProfile,
localScenario,
@ -392,6 +411,10 @@ export default function ScenarioContainer({
chartDatasets.push(totalSavingsData);
const milestoneAnnotations = milestones
.filter((m) => // <-- filter FIRST
m.title === "Retirement" ||
(impactsByMilestone[m.id] ?? []).length > 0
)
.map((m) => {
if (!m.date) return null;
const d = new Date(m.date);
@ -407,13 +430,15 @@ export default function ScenarioContainer({
type: 'line',
xMin: short,
xMax: short,
borderColor: 'orange',
borderColor: m.title === 'Retirement' ? 'black' : 'orange',
borderWidth: 2,
label: {
display: true,
content: m.title || 'Milestone',
color: 'orange',
position: 'end'
backgroundColor: m.title === 'Retirement' ? 'black' : 'orange',
color: 'white',
position: 'end',
padding: 4
}
};
})
@ -908,8 +933,23 @@ export default function ScenarioContainer({
<div style={{ marginTop: '0.5rem' }}>
<strong>Loan Paid Off:</strong> {loanPaidOffMonth || 'N/A'} <br />
<strong>Final Retirement:</strong>{' '}
{Math.round(
projectionData[projectionData.length - 1].retirementSavings
{Math.round(retireBalAtMilestone)}
{readinessScore != null && (
<span
title="Retirement income / desired"
style={{
marginLeft: '0.5rem',
padding: '0 6px',
borderRadius: '12px',
fontWeight: 600,
background:
readinessScore >= 80 ? '#15803d'
: readinessScore >= 60 ? '#ca8a04' : '#dc2626',
color: '#fff'
}}
>
{readinessScore}/100
</span>
)}
</div>
)}

View File

@ -512,6 +512,35 @@ async function handleSave() {
if (!scenRes.ok) throw new Error(await scenRes.text());
const { career_profile_id } = await scenRes.json();
// ─── AUTO-CREATE / UPDATE “Retirement” milestone ──────────────────
if (formData.retirement_start_date) {
const payload = {
title : 'Retirement',
description : 'User-defined retirement date (auto-generated)',
date : formData.retirement_start_date,
career_profile_id: career_profile_id,
progress : 0,
status : 'planned',
is_universal : 0
};
// Ask the backend if one already exists for this scenario
const check = await authFetch(
`/api/premium/milestones?careerProfileId=${career_profile_id}`
).then(r => r.json());
const existing = (check.milestones || []).find(m => m.title === 'Retirement');
await authFetch(
existing ? `/api/premium/milestones/${existing.id}` : '/api/premium/milestone',
{
method : existing ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(payload)
}
);
}
/* ─── 3) (optional) upsert college profile keep yours… ─ */
/* ─── 4) update localStorage so CareerRoadmap re-hydrates ─ */

View File

@ -202,8 +202,10 @@ const randomRangeMax = num(_randomRangeMax);
* 2) CLAMP THE SCENARIO START TO MONTH-BEGIN
***************************************************/
const scenarioStartClamped = moment(startDate || new Date()).startOf('month');
const scenarioStartClamped = moment().startOf('month'); // always “today”
let reachedRetirement = false; // flip to true once we hit the month
let firstRetirementBalance = null; // snapshot of balance in that first month
const retirementSpendMonthly = num(_retSpend);
const retirementStartISO =
_retStart ? moment(_retStart).startOf('month')
@ -358,9 +360,14 @@ function simulateDrawdown(opts){
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
const reachedRetirement =
retirementSpendMonthly > 0 &&
currentSimDate.isSameOrAfter(retirementStartISO);
if (!reachedRetirement && currentSimDate.isSameOrAfter(retirementStartISO)) {
reachedRetirement = true;
firstRetirementBalance = currentRetirementSavings; // capture once
}
const isRetiredThisMonth =
retirementSpendMonthly > 0 &&
currentSimDate.isSameOrAfter(retirementStartISO)
// figure out if we are in the college window
let stillInCollege = false;
@ -506,8 +513,8 @@ baseMonthlyIncome += salaryAdjustThisMonth; // adjust gross BEFORE tax
let leftover = netMonthlyIncome - totalMonthlyExpenses;
// baseline contributions
const monthlyRetContrib = reachedRetirement ? 0 : monthlyRetirementContribution;
const monthlyEmergContrib = reachedRetirement ? 0 : monthlyEmergencyContribution;
const monthlyRetContrib = isRetiredThisMonth ? 0 : monthlyRetirementContribution;
const monthlyEmergContrib = isRetiredThisMonth ? 0 : monthlyEmergencyContribution;
const baselineContributions = monthlyRetContrib + monthlyEmergContrib;
let effectiveRetirementContribution = 0;
let effectiveEmergencyContribution = 0;
@ -585,6 +592,9 @@ baseMonthlyIncome += salaryAdjustThisMonth; // adjust gross BEFORE tax
wasInDeferral = (stillInCollege && loanDeferralUntilGraduation);
}
if (!firstRetirementBalance && reachedRetirement) {
firstRetirementBalance = currentRetirementSavings;
}
// final loanPaidOffMonth if never set
if (loanBalance <= 0 && !loanPaidOffMonth) {
@ -611,9 +621,27 @@ const yearsCovered = Math.round(monthsCovered / 12);
/* ---- 9) RETURN ------------------------------------------------ */
/* 9.1  »  Readiness Score (0-100, whole-number) */
const safeWithdrawalRate = flatAnnualRate; // 6 % for now (plug 0.04 if you prefer)
const projectedAnnualFlow = (firstRetirementBalance ?? currentRetirementSavings)
* safeWithdrawalRate;
const desiredAnnualIncome = retirementSpendMonthly // user override
? retirementSpendMonthly * 12
: monthlyExpenses * 12; // fallback to current spend
const readinessScore = desiredAnnualIncome > 0
? Math.min(
100,
Math.round((projectedAnnualFlow / desiredAnnualIncome) * 100)
)
: 0;
return {
projectionData,
loanPaidOffMonth,
readinessScore,
retirementAtMilestone : firstRetirementBalance ?? 0,
yearsCovered, // <-- add these two
monthsCovered, // (or just yearsCovered if thats all you need)
finalEmergencySavings: +currentEmergencySavings.toFixed(2),