Fixed Retirement lasts X years

This commit is contained in:
Josh 2025-07-18 14:37:46 +00:00
parent fdcae3bdfb
commit 15d28ce2e8
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 [drawerMilestone, setDrawerMilestone] = useState(null);
const [impactsById, setImpactsById] = useState({}); // id → [impacts] const [impactsById, setImpactsById] = useState({}); // id → [impacts]
const [addingNewMilestone, setAddingNewMilestone] = useState(false); const [addingNewMilestone, setAddingNewMilestone] = useState(false);
const [showMissingBanner, setShowMissingBanner] = useState(false);
// Config // Config
@ -573,16 +574,14 @@ useEffect(() => {
const dataReady = !!scenarioRow && !!financialProfile && collegeProfile !== null; const dataReady = !!scenarioRow && !!financialProfile && collegeProfile !== null;
useEffect(() => { 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 /* derive once, local to this effect -------------------------------------- */
const key = `modalChecked:${careerProfileId}`; const status = (scenarioRow?.college_enrollment_status || '').toLowerCase();
// already checked in this browser session?
if (sessionStorage.getItem(key) === '1') return;
const status = (scenarioRow.college_enrollment_status || '').toLowerCase();
const requireCollege = ['currently_enrolled','prospective_student','deferred'] const requireCollege = ['currently_enrolled','prospective_student','deferred']
.includes(status); .includes(status);
@ -591,18 +590,24 @@ useEffect(() => {
{ requireCollegeData: requireCollege } { requireCollegeData: requireCollege }
); );
if (missing.length) setShowEditModal(true); if (missing.length) {
/* if we arrived *directly* from onboarding we silently skip the banner
sessionStorage.setItem(key, '1'); // remember for this tab once, but we still want the EditScenario modal to open */
if (modalGuard.current.skip) {
setShowEditModal(true);
} else {
setShowMissingBanner(true);
}
}
}, [dataReady, scenarioRow, financialProfile, collegeProfile, careerProfileId]); }, [dataReady, scenarioRow, financialProfile, collegeProfile, careerProfileId]);
useEffect(() => { useEffect(() => {
if ( if (
financialProfile && financialProfile &&
scenarioRow && scenarioRow &&
collegeProfile && collegeProfile
scenarioMilestones.length
) { ) {
buildProjection(scenarioMilestones); // uses the latest scenarioMilestones buildProjection(scenarioMilestones); // uses the latest scenarioMilestones
} }
@ -949,7 +954,7 @@ useEffect(() => {
// 8) Build financial projection // 8) Build financial projection
async function buildProjection(milestones) { async function buildProjection(milestones) {
if (!milestones?.length) return; if (!milestones?.length) return;
const allMilestones = milestones; const allMilestones = milestones || [];
try { try {
setScenarioMilestones(allMilestones); setScenarioMilestones(allMilestones);
@ -1302,9 +1307,8 @@ const fetchMilestones = useCallback(async () => {
setScenarioRow={setScenarioRow} setScenarioRow={setScenarioRow}
careerProfileId={careerProfileId} careerProfileId={careerProfileId}
collegeProfile={collegeProfile} collegeProfile={collegeProfile}
onMilestonesCreated={() => { onMilestonesCreated={handleMilestonesCreated}
/* refresh or reload logic here */
}}
onAiRiskFetched={(riskData) => { onAiRiskFetched={(riskData) => {
@ -1447,6 +1451,28 @@ const fetchMilestones = useCallback(async () => {
</div>*/} </div>*/}
{/* --- FINANCIAL PROJECTION SECTION -------------------------------- */} {/* --- 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"> <div className="bg-white p-4 rounded shadow">
<h3 className="text-lg font-semibold mb-2 flex items-center justify-center gap-1"> <h3 className="text-lg font-semibold mb-2 flex items-center justify-center gap-1">
Financial Projection Financial Projection
@ -1600,8 +1626,8 @@ const fetchMilestones = useCallback(async () => {
milestone={milestoneForModal} /* ← edit mode */ milestone={milestoneForModal} /* ← edit mode */
fetchMilestones={fetchMilestones} fetchMilestones={fetchMilestones}
onClose={(didSave) => { onClose={(didSave) => {
if (didSave) handleMilestonesCreated();
setMilestoneForModal(null); setMilestoneForModal(null);
if (didSave) fetchMilestones();
}} }}
/> />
)} )}

View File

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

View File

@ -230,10 +230,7 @@ export default function ScenarioContainer({
}; };
// Gather milestoneImpacts // Gather milestoneImpacts
let allImpacts = []; const allImpacts = Object.values(impactsByMilestone).flat(); // safe even if []
Object.keys(impactsByMilestone).forEach((mId) => {
allImpacts = allImpacts.concat(impactsByMilestone[mId]);
});
const simYears = parseInt(simulationYearsInput, 10) || 20; const simYears = parseInt(simulationYearsInput, 10) || 20;
const simYearsUI = Math.max(1, parseInt(simulationYearsInput, 10) || 20); const simYearsUI = Math.max(1, parseInt(simulationYearsInput, 10) || 20);
@ -315,12 +312,19 @@ export default function ScenarioContainer({
surplusEmergencyAllocation: scenarioOverrides.surplusEmergencyAllocation, surplusEmergencyAllocation: scenarioOverrides.surplusEmergencyAllocation,
surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation, surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation,
additionalIncome: scenarioOverrides.additionalIncome, additionalIncome: scenarioOverrides.additionalIncome,
retirement_start_date: localScenario.retirement_start_date retirement_start_date:
|| localScenario.projected_end_date localScenario.retirement_start_date // user picked
|| null, || (localScenario.projected_end_date // often set for college scenarios
desired_retirement_income_monthly: parseScenarioOverride( ? 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, localScenario.desired_retirement_income_monthly,
0 scenarioOverrides.monthlyExpenses // ← fallback to current spend
), ),
studentLoanAmount: collegeData.studentLoanAmount, studentLoanAmount: collegeData.studentLoanAmount,
@ -354,13 +358,12 @@ export default function ScenarioContainer({
simulateFinancialProjection(mergedProfile); simulateFinancialProjection(mergedProfile);
const sliceTo = simYearsUI * 12; // months we want to keep const sliceTo = simYearsUI * 12;
let cumulative = mergedProfile.emergencySavings || 0; let cumulative = mergedProfile.emergencySavings || 0;
const finalData = pData.map(row => {
const finalData = pData.slice(0, sliceTo).map(row => {
cumulative += row.netSavings || 0; cumulative += row.netSavings || 0;
return { ...row, cumulativeNetSavings: cumulative }; return { ...row, cumulativeNetSavings: cumulative };
}); }).slice(0, sliceTo);
if (typeof onSimDone === 'function') { if (typeof onSimDone === 'function') {
onSimDone(localScenario.id, yc); onSimDone(localScenario.id, yc);
@ -958,6 +961,23 @@ return (
<Line data={chartData} options={chartOptions} /> <Line data={chartData} options={chartOptions} />
</div> </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 ───────────── */} {/* ───────────── KPI Bar ───────────── */}
{projectionData.length > 0 && ( {projectionData.length > 0 && (
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">

Binary file not shown.