Fixed Retirement lasts X years

This commit is contained in:
Josh 2025-07-18 14:37:46 +00:00
parent f1807af1f2
commit 48133de297
4 changed files with 84 additions and 37 deletions

View File

@ -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 profileid ------------------------------------------------ */
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 EditScenario 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 () => {
</div>*/}
{/* --- FINANCIAL PROJECTION SECTION -------------------------------- */}
{showMissingBanner && (
<div className="bg-yellow-100 border-l-4 border-yellow-500 p-4 rounded shadow mb-4">
<p className="text-sm text-gray-800">
We need a few basics (income, expenses, etc.) before we can show a full
projection.
</p>
<Button
className="mt-2"
onClick={() => { setShowEditModal(true); setShowMissingBanner(false); }}
>
Add Details
</Button>
<button
className="ml-3 text-xs text-gray-600 underline"
onClick={() => setShowMissingBanner(false)}
>
Dismiss
</button>
</div>
)}
<div className="bg-white p-4 rounded shadow">
<h3 className="text-lg font-semibold mb-2 flex items-center justify-center gap-1">
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();
}}
/>
)}

View File

@ -42,6 +42,7 @@ export default function RetirementPlanner () {
const isMobile = useIsMobile();
const { openRetire } = useContext(ChatCtx);
/* ----------------------- data loading -------------------------- */
const loadAll = useCallback(async () => {
try {

View File

@ -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 (
<Line data={chartData} options={chartOptions} />
</div>
{(!localScenario?.retirement_start_date ||
!localScenario?.desired_retirement_income_monthly) && (
<div className="bg-yellow-100 border-l-4 border-yellow-500 p-3 rounded mb-3 text-sm">
<p className="text-gray-800">
Add a retirement date and spending goal to see&nbsp;
<em>Money Lasts</em>.
</p>
<Button
size="sm"
className="mt-1"
onClick={() => setShowScenarioModal(true)}
>
Edit Scenario
</Button>
</div>
)}
{/* ───────────── KPI Bar ───────────── */}
{projectionData.length > 0 && (
<div className="space-y-1 text-sm">

Binary file not shown.