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 [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 profile‑id ------------------------------------------------ */
|
||||
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 Edit‑Scenario 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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -42,6 +42,7 @@ export default function RetirementPlanner () {
|
||||
const isMobile = useIsMobile();
|
||||
const { openRetire } = useContext(ChatCtx);
|
||||
|
||||
|
||||
/* ----------------------- data loading -------------------------- */
|
||||
const loadAll = useCallback(async () => {
|
||||
try {
|
||||
|
@ -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
|
||||
<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">
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user