diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js
index 16e583b..8c2d14e 100644
--- a/src/components/ScenarioContainer.js
+++ b/src/components/ScenarioContainer.js
@@ -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({
Loan Paid Off: {loanPaidOffMonth || 'N/A'}
Final Retirement:{' '}
- {Math.round(
- projectionData[projectionData.length - 1].retirementSavings
+ {Math.round(retireBalAtMilestone)}
+ {readinessScore != null && (
+ = 80 ? '#15803d'
+ : readinessScore >= 60 ? '#ca8a04' : '#dc2626',
+ color: '#fff'
+ }}
+ >
+ {readinessScore}/100
+
)}
)}
diff --git a/src/components/ScenarioEditModal.js b/src/components/ScenarioEditModal.js
index 52766b6..cea2b57 100644
--- a/src/components/ScenarioEditModal.js
+++ b/src/components/ScenarioEditModal.js
@@ -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 ─ */
diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js
index 6f8cd60..bddfb9c 100644
--- a/src/utils/FinancialProjectionService.js
+++ b/src/utils/FinancialProjectionService.js
@@ -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 that’s all you need)
finalEmergencySavings: +currentEmergencySavings.toFixed(2),