Fixed Retirement lasts X years
This commit is contained in:
parent
fdcae3bdfb
commit
15d28ce2e8
@ -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 profile‑id ------------------------------------------------ */
|
||||||
|
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 Edit‑Scenario 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();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,13 +312,20 @@ 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)
|
||||||
localScenario.desired_retirement_income_monthly,
|
.startOf('month')
|
||||||
0
|
.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,
|
studentLoanAmount: collegeData.studentLoanAmount,
|
||||||
interestRate: collegeData.interestRate,
|
interestRate: collegeData.interestRate,
|
||||||
@ -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
|
||||||
|
<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">
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user