import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { Line, Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
LineElement,
BarElement,
CategoryScale,
LinearScale,
Filler,
PointElement,
Tooltip,
Legend
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import authFetch from '../utils/authFetch.js';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
import { getFullStateName } from '../utils/stateUtils.js';
import { Button } from './ui/button.js';
import CareerSelectDropdown from './CareerSelectDropdown.js';
import MilestoneTimeline from './MilestoneTimeline.js';
import ScenarioEditModal from './ScenarioEditModal.js';
import './CareerRoadmap.css';
import './MilestoneTimeline.css';
// --------------
// Register ChartJS Plugins
// --------------
ChartJS.register(
LineElement,
BarElement,
CategoryScale,
LinearScale,
Filler,
PointElement,
Tooltip,
Legend,
annotationPlugin
);
// --------------
// Helper Functions
// --------------
function stripSocCode(fullSoc) {
if (!fullSoc) return '';
return fullSoc.split('.')[0];
}
function getRelativePosition(userSal, p10, p90) {
if (!p10 || !p90) return 0;
if (userSal < p10) return 0;
if (userSal > p90) return 1;
return (userSal - p10) / (p90 - p10);
}
// A simple gauge for the user’s salary vs. percentiles
function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) {
if (!percentileRow) return null;
const p10 = parseFloatOrZero(percentileRow[`${prefix}_PCT10`], 0);
const p90 = parseFloatOrZero(percentileRow[`${prefix}_PCT90`], 0);
const median = parseFloatOrZero(percentileRow[`${prefix}_MEDIAN`], 0);
if (!p10 || !p90 || p10 >= p90) {
return null;
}
const userFrac = getRelativePosition(userSalary, p10, p90) * 100;
const medianFrac = getRelativePosition(median, p10, p90) * 100;
return (
${p10.toLocaleString()}
${p90.toLocaleString()}
{/* Median Marker */}
Median ${median.toLocaleString()}
{/* User Salary Marker */}
${userSalary.toLocaleString()}
);
}
function EconomicProjectionsBar({ data }) {
if (!data) return null;
const {
area,
baseYear,
projectedYear,
base,
projection,
change,
annualOpenings,
occupationName
} = data;
if (!area || !base || !projection) {
return No data for {area || 'this region'}.
;
}
const barData = {
labels: [`${occupationName || 'Career'}: ${area}`],
datasets: [
{
label: `Jobs in ${baseYear}`,
data: [base],
backgroundColor: 'rgba(75,192,192,0.6)'
},
{
label: `Jobs in ${projectedYear}`,
data: [projection],
backgroundColor: 'rgba(255,99,132,0.6)'
}
]
};
const barOptions = {
responsive: true,
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y.toLocaleString()}`
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (val) => val.toLocaleString()
}
}
}
};
return (
{area}
Change: {change?.toLocaleString() ?? 0} jobs
Annual Openings: {annualOpenings?.toLocaleString() ?? 0}
);
}
function getYearsInCareer(startDateString) {
if (!startDateString) return null;
const start = new Date(startDateString);
if (isNaN(start)) return null;
const now = new Date();
const diffMs = now - start;
const diffYears = diffMs / (1000 * 60 * 60 * 24 * 365.25);
if (diffYears < 1) {
return '<1';
}
return Math.floor(diffYears).toString();
}
/**
* parseAiJson
* If ChatGPT returns a fenced code block like:
* ```json
* [ { ... }, ... ]
* ```
* we extract that JSON. Otherwise, we parse the raw string.
*/
function parseAiJson(rawText) {
const fencedRegex = /```json\s*([\s\S]*?)\s*```/i;
const match = rawText.match(fencedRegex);
if (match) {
const jsonStr = match[1].trim();
const arr = JSON.parse(jsonStr);
// Add an "id" for each milestone
arr.forEach((m) => {
m.id = crypto.randomUUID();
});
return arr;
} else {
// fallback if no fences
const arr = JSON.parse(rawText);
arr.forEach((m) => {
m.id = crypto.randomUUID();
});
return arr;
}
}
export default function CareerRoadmap({ selectedCareer: initialCareer }) {
const location = useLocation();
const apiURL = process.env.REACT_APP_API_URL;
const [interestStrategy, setInterestStrategy] = useState('FLAT'); // 'NONE' | 'FLAT' | 'MONTE_CARLO'
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06); // 6% default
const [randomRangeMin, setRandomRangeMin] = useState(-0.02); // -3% monthly
const [randomRangeMax, setRandomRangeMax] = useState(0.02); // 8% monthly
// Basic states
const [userProfile, setUserProfile] = useState(null);
const [financialProfile, setFinancialProfile] = useState(null);
const [masterCareerRatings, setMasterCareerRatings] = useState([]);
const [existingCareerProfiles, setExistingCareerProfiles] = useState([]);
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
const [careerProfileId, setCareerProfileId] = useState(null);
const [scenarioRow, setScenarioRow] = useState(null);
const [collegeProfile, setCollegeProfile] = useState(null);
const [strippedSocCode, setStrippedSocCode] = useState(null);
const [salaryData, setSalaryData] = useState(null);
const [economicProjections, setEconomicProjections] = useState(null);
// Milestones & Projection
const [scenarioMilestones, setScenarioMilestones] = useState([]);
const [projectionData, setProjectionData] = useState([]);
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
// Config
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
const simulationYears = parseInt(simulationYearsInput, 10) || 20;
const [showEditModal, setShowEditModal] = useState(false);
// AI
const [aiLoading, setAiLoading] = useState(false);
const [recommendations, setRecommendations] = useState([]); // parsed array
const [selectedIds, setSelectedIds] = useState([]); // which rec IDs are checked
const [lastClickTime, setLastClickTime] = useState(null);
const RATE_LIMIT_SECONDS = 15; // adjust as needed
const [buttonDisabled, setButtonDisabled] = useState(false);
const {
projectionData: initProjData = [],
loanPayoffMonth: initLoanMonth = null
} = location.state || {};
// 1) Fetch user + financial
useEffect(() => {
async function fetchUser() {
try {
const r = await authFetch('/api/user-profile');
if (r.ok) setUserProfile(await r.json());
} catch (err) {
console.error('Error user-profile =>', err);
}
}
async function fetchFin() {
try {
const r = await authFetch(`${apiURL}/premium/financial-profile`);
if (r.ok) setFinancialProfile(await r.json());
} catch (err) {
console.error('Error financial =>', err);
}
}
fetchUser();
fetchFin();
}, [apiURL]);
const userSalary = parseFloatOrZero(financialProfile?.current_salary, 0);
const userArea = userProfile?.area || 'U.S.';
const userState = getFullStateName(userProfile?.state || '') || 'United States';
useEffect(() => {
let timer;
if (buttonDisabled) {
timer = setTimeout(() => setButtonDisabled(false), RATE_LIMIT_SECONDS * 1000);
}
return () => clearTimeout(timer);
}, [buttonDisabled]);
useEffect(() => {
const storedRecs = localStorage.getItem('aiRecommendations');
if (storedRecs) {
try {
const arr = JSON.parse(storedRecs);
arr.forEach((m) => {
if (!m.id) {
m.id = crypto.randomUUID();
}
});
setRecommendations(arr);
} catch (err) {
console.error('Error parsing stored AI recs =>', err);
}
}
}, []);
useEffect(() => {
if (recommendations.length > 0) {
localStorage.setItem('aiRecommendations', JSON.stringify(recommendations));
} else {
// if it's empty, we can remove from localStorage if you want
localStorage.removeItem('aiRecommendations');
}
}, [recommendations]);
// 2) load local JSON => masterCareerRatings
useEffect(() => {
fetch('/careers_with_ratings.json')
.then((res) => {
if (!res.ok) throw new Error('Failed to load local career data');
return res.json();
})
.then((data) => setMasterCareerRatings(data))
.catch((err) => console.error('Error loading local career data =>', err));
}, []);
// 3) fetch user’s career-profiles
useEffect(() => {
async function fetchProfiles() {
const r = await authFetch(`${apiURL}/premium/career-profile/all`);
if (!r || !r.ok) return;
const d = await r.json();
setExistingCareerProfiles(d.careerProfiles);
const fromPopout = location.state?.selectedCareer;
if (fromPopout) {
setSelectedCareer(fromPopout);
setCareerProfileId(fromPopout.career_profile_id);
} else {
const stored = localStorage.getItem('lastSelectedCareerProfileId');
if (stored) {
const match = d.careerProfiles.find((p) => p.id === stored);
if (match) {
setSelectedCareer(match);
setCareerProfileId(stored);
return;
}
}
// fallback => latest
const lr = await authFetch(`${apiURL}/premium/career-profile/latest`);
if (lr && lr.ok) {
const ld = await lr.json();
if (ld?.id) {
setSelectedCareer(ld);
setCareerProfileId(ld.id);
}
}
}
}
fetchProfiles();
}, [apiURL, location.state]);
// 4) scenarioRow + college
useEffect(() => {
if (!careerProfileId) {
setScenarioRow(null);
setCollegeProfile(null);
setScenarioMilestones([]);
return;
}
localStorage.setItem('lastSelectedCareerProfileId', careerProfileId);
async function fetchScenario() {
const s = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`);
if (s.ok) setScenarioRow(await s.json());
}
async function fetchCollege() {
const c = await authFetch(`${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`);
if (c.ok) setCollegeProfile(await c.json());
}
fetchScenario();
fetchCollege();
}, [careerProfileId, apiURL]);
// 5) from scenarioRow => find the full SOC => strip
useEffect(() => {
if (!scenarioRow?.career_name || !masterCareerRatings.length) {
setStrippedSocCode(null);
return;
}
const lower = scenarioRow.career_name.trim().toLowerCase();
const found = masterCareerRatings.find(
(obj) => obj.title?.trim().toLowerCase() === lower
);
if (!found) {
console.warn('No matching SOC =>', scenarioRow.career_name);
setStrippedSocCode(null);
return;
}
setStrippedSocCode(stripSocCode(found.soc_code));
}, [scenarioRow, masterCareerRatings]);
// 6) Salary
useEffect(() => {
if (!strippedSocCode) {
setSalaryData(null);
return;
}
(async () => {
try {
const qs = new URLSearchParams({ socCode: strippedSocCode, area: userArea }).toString();
const url = `${apiURL}/salary?${qs}`;
const r = await fetch(url);
if (!r.ok) {
console.error('[Salary fetch non-200 =>]', r.status);
setSalaryData(null);
return;
}
const dd = await r.json();
setSalaryData(dd);
} catch (err) {
console.error('[Salary fetch error]', err);
setSalaryData(null);
}
})();
}, [strippedSocCode, userArea, apiURL]);
// 7) Econ
useEffect(() => {
if (!strippedSocCode || !userState) {
setEconomicProjections(null);
return;
}
(async () => {
const qs = new URLSearchParams({ state: userState }).toString();
const econUrl = `${apiURL}/projections/${strippedSocCode}?${qs}`;
try {
const r = await authFetch(econUrl);
if (!r.ok) {
console.error('[Econ fetch non-200 =>]', r.status);
setEconomicProjections(null);
return;
}
const econData = await r.json();
setEconomicProjections(econData);
} catch (err) {
console.error('[Econ fetch error]', err);
setEconomicProjections(null);
}
})();
}, [strippedSocCode, userState, apiURL]);
// 8) Build financial projection
async function buildProjection() {
try {
const milUrl = `${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`;
const mr = await authFetch(milUrl);
if (!mr.ok) {
console.error('Failed to fetch milestones =>', mr.status);
return;
}
const md = await mr.json();
const allMilestones = md.milestones || [];
setScenarioMilestones(allMilestones);
// fetch impacts
const imPromises = allMilestones.map((m) =>
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
.then((r) => (r.ok ? r.json() : null))
.then((dd) => dd?.impacts || [])
.catch((e) => {
console.warn('Error fetching impacts =>', e);
return [];
})
);
const impactsForEach = await Promise.all(imPromises);
const allImpacts = allMilestones
.map((m, i) => ({ ...m, impacts: impactsForEach[i] || [] }))
.flatMap((m) => m.impacts);
const f = financialProfile;
const financialBase = {
currentSalary: parseFloatOrZero(f.current_salary, 0),
additionalIncome: parseFloatOrZero(f.additional_income, 0),
monthlyExpenses: parseFloatOrZero(f.monthly_expenses, 0),
monthlyDebtPayments: parseFloatOrZero(f.monthly_debt_payments, 0),
retirementSavings: parseFloatOrZero(f.retirement_savings, 0),
emergencySavings: parseFloatOrZero(f.emergency_fund, 0),
retirementContribution: parseFloatOrZero(f.retirement_contribution, 0),
emergencyContribution: parseFloatOrZero(f.emergency_contribution, 0),
extraCashEmergencyPct: parseFloatOrZero(f.extra_cash_emergency_pct, 50),
extraCashRetirementPct: parseFloatOrZero(f.extra_cash_retirement_pct, 50)
};
function parseScenarioOverride(overrideVal, fallbackVal) {
if (overrideVal === null) {
return fallbackVal;
}
return parseFloatOrZero(overrideVal, fallbackVal);
}
const s = scenarioRow;
const scenarioOverrides = {
monthlyExpenses: parseScenarioOverride(
s.planned_monthly_expenses,
financialBase.monthlyExpenses
),
monthlyDebtPayments: parseScenarioOverride(
s.planned_monthly_debt_payments,
financialBase.monthlyDebtPayments
),
monthlyRetirementContribution: parseScenarioOverride(
s.planned_monthly_retirement_contribution,
financialBase.retirementContribution
),
monthlyEmergencyContribution: parseScenarioOverride(
s.planned_monthly_emergency_contribution,
financialBase.emergencyContribution
),
surplusEmergencyAllocation: parseScenarioOverride(
s.planned_surplus_emergency_pct,
financialBase.extraCashEmergencyPct
),
surplusRetirementAllocation: parseScenarioOverride(
s.planned_surplus_retirement_pct,
financialBase.extraCashRetirementPct
),
additionalIncome: parseScenarioOverride(
s.planned_additional_income,
financialBase.additionalIncome
)
};
const c = collegeProfile;
const collegeData = {
studentLoanAmount: parseFloatOrZero(c.existing_college_debt, 0),
interestRate: parseFloatOrZero(c.interest_rate, 5),
loanTerm: parseFloatOrZero(c.loan_term, 10),
loanDeferralUntilGraduation: !!c.loan_deferral_until_graduation,
academicCalendar: c.academic_calendar || 'monthly',
annualFinancialAid: parseFloatOrZero(c.annual_financial_aid, 0),
calculatedTuition: parseFloatOrZero(c.tuition, 0),
extraPayment: parseFloatOrZero(c.extra_payment, 0),
inCollege:
c.college_enrollment_status === 'currently_enrolled' ||
c.college_enrollment_status === 'prospective_student',
gradDate: c.expected_graduation || null,
programType: c.program_type || null,
creditHoursPerYear: parseFloatOrZero(c.credit_hours_per_year, 0),
hoursCompleted: parseFloatOrZero(c.hours_completed, 0),
programLength: parseFloatOrZero(c.program_length, 0),
expectedSalary:
parseFloatOrZero(c.expected_salary) || parseFloatOrZero(f.current_salary, 0)
};
const mergedProfile = {
currentSalary: financialBase.currentSalary,
monthlyExpenses: scenarioOverrides.monthlyExpenses,
monthlyDebtPayments: scenarioOverrides.monthlyDebtPayments,
retirementSavings: financialBase.retirementSavings,
emergencySavings: financialBase.emergencySavings,
monthlyRetirementContribution: scenarioOverrides.monthlyRetirementContribution,
monthlyEmergencyContribution: scenarioOverrides.monthlyEmergencyContribution,
surplusEmergencyAllocation: scenarioOverrides.surplusEmergencyAllocation,
surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation,
additionalIncome: scenarioOverrides.additionalIncome,
// college
studentLoanAmount: collegeData.studentLoanAmount,
interestRate: collegeData.interestRate,
loanTerm: collegeData.loanTerm,
loanDeferralUntilGraduation: collegeData.loanDeferralUntilGraduation,
academicCalendar: collegeData.academicCalendar,
annualFinancialAid: collegeData.annualFinancialAid,
calculatedTuition: collegeData.calculatedTuition,
extraPayment: collegeData.extraPayment,
inCollege: collegeData.inCollege,
gradDate: collegeData.gradDate,
programType: collegeData.programType,
creditHoursPerYear: collegeData.creditHoursPerYear,
hoursCompleted: collegeData.hoursCompleted,
programLength: collegeData.programLength,
expectedSalary: collegeData.expectedSalary,
startDate: new Date().toISOString().slice(0, 10),
simulationYears,
milestoneImpacts: allImpacts,
interestStrategy,
flatAnnualRate,
monthlyReturnSamples: [], // or keep an array if you have historical data
randomRangeMin,
randomRangeMax
};
const { projectionData: pData, loanPaidOffMonth } =
simulateFinancialProjection(mergedProfile);
let cumu = mergedProfile.emergencySavings || 0;
const finalData = pData.map((mo) => {
cumu += mo.netSavings || 0;
return { ...mo, cumulativeNetSavings: cumu };
});
setProjectionData(finalData);
setLoanPayoffMonth(loanPaidOffMonth);
} catch (err) {
console.error('Error in scenario simulation =>', err);
}
}
useEffect(() => {
if (!financialProfile || !scenarioRow || !collegeProfile) return;
buildProjection();
}, [financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears, interestStrategy, flatAnnualRate, randomRangeMin, randomRangeMax]);
// Build chart datasets / annotations
const milestoneAnnotationLines = {};
scenarioMilestones.forEach((m) => {
if (!m.date) return;
const d = new Date(m.date);
if (isNaN(d)) return;
const yyyy = d.getUTCFullYear();
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
const short = `${yyyy}-${mm}`;
if (!projectionData.some((p) => p.month === short)) return;
milestoneAnnotationLines[`milestone_${m.id}`] = {
type: 'line',
xMin: short,
xMax: short,
borderColor: 'orange',
borderWidth: 2,
label: {
display: true,
content: m.title || 'Milestone',
color: 'orange',
position: 'end'
}
};
});
const [clickCount, setClickCount] = useState(() => {
const storedCount = localStorage.getItem('aiClickCount');
const storedDate = localStorage.getItem('aiClickDate');
const today = new Date().toISOString().slice(0, 10).slice(0, 10);
if (storedDate !== today) {
localStorage.setItem('aiClickDate', today);
localStorage.setItem('aiClickCount', '0');
return 0;
}
return parseInt(storedCount || '0', 10);
});
const DAILY_CLICK_LIMIT = 10; // example limit per day
const hasStudentLoan = projectionData.some((p) => p.loanBalance > 0);
const annotationConfig = {};
if (loanPayoffMonth && hasStudentLoan) {
annotationConfig.loanPaidOffLine = {
type: 'line',
xMin: loanPayoffMonth,
xMax: loanPayoffMonth,
borderColor: 'rgba(255, 206, 86, 1)',
borderWidth: 2,
borderDash: [6, 6],
label: {
display: true,
content: 'Loan Paid Off',
position: 'end',
backgroundColor: 'rgba(255, 206, 86, 0.8)',
color: '#000',
font: { size: 12 },
rotation: 0,
yAdjust: -10
}
};
}
const allAnnotations = { ...milestoneAnnotationLines, ...annotationConfig };
const emergencyData = {
label: 'Emergency Savings',
data: projectionData.map((p) => p.emergencySavings),
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.2)',
tension: 0.4,
fill: true
};
const retirementData = {
label: 'Retirement Savings',
data: projectionData.map((p) => p.retirementSavings),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4,
fill: true
};
const totalSavingsData = {
label: 'Total Savings',
data: projectionData.map((p) => p.totalSavings),
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
tension: 0.4,
fill: true
};
const loanBalanceData = {
label: 'Loan Balance',
data: projectionData.map((p) => p.loanBalance),
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
tension: 0.4,
fill: {
target: 'origin',
above: 'rgba(255,99,132,0.3)',
below: 'transparent'
}
};
const chartDatasets = [emergencyData, retirementData];
if (hasStudentLoan) chartDatasets.push(loanBalanceData);
chartDatasets.push(totalSavingsData);
const yearsInCareer = getYearsInCareer(scenarioRow?.start_date);
// -- AI Handler --
async function handleAiClick() {
if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
alert('You have reached the daily limit for suggestions.');
return;
}
if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
alert('You have reached your daily limit of AI-generated recommendations. Please check back tomorrow.');
return;
}
setAiLoading(true);
setSelectedIds([]);
const oldRecTitles = recommendations.map(r => r.title.trim()).filter(Boolean);
const acceptedTitles = scenarioMilestones.map(m => (m.title || '').trim()).filter(Boolean);
const allToAvoid = [...oldRecTitles, ...acceptedTitles];
try {
const payload = {
userProfile,
scenarioRow,
financialProfile,
collegeProfile,
previouslyUsedTitles: allToAvoid
};
const res = await authFetch('/api/premium/ai/next-steps', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('AI request failed');
const data = await res.json();
const rawText = data.recommendations || '';
const arr = parseAiJson(rawText);
setRecommendations(arr);
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
// Update click count
setClickCount(prev => {
const newCount = prev + 1;
localStorage.setItem('aiClickCount', newCount);
return newCount;
});
} catch (err) {
console.error('Error fetching AI next steps =>', err);
} finally {
setAiLoading(false);
}
}
function handleToggle(recId) {
setSelectedIds((prev) => {
if (prev.includes(recId)) {
return prev.filter((x) => x !== recId);
} else {
return [...prev, recId];
}
});
}
async function handleCreateSelectedMilestones() {
if (!careerProfileId) return;
const confirm = window.confirm('Create the selected AI suggestions as milestones?');
if (!confirm) return;
const selectedRecs = recommendations.filter((r) => selectedIds.includes(r.id));
if (!selectedRecs.length) return;
// Use the AI-suggested date:
const payload = selectedRecs.map((rec) => ({
title: rec.title,
description: rec.description || '',
date: rec.date, // <-- use AI's date, not today's date
career_profile_id: careerProfileId
}));
try {
const r = await authFetch('/api/premium/milestone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ milestones: payload })
});
if (!r.ok) throw new Error('Failed to create new milestones');
// re-run projection to see them in the chart
await buildProjection();
// optionally clear
alert('Milestones created successfully!');
setSelectedIds([]);
} catch (err) {
console.error('Error creating milestones =>', err);
alert('Error saving new AI milestones.');
}
}
function handleSimulationYearsChange(e) {
setSimulationYearsInput(e.target.value);
}
function handleSimulationYearsBlur() {
if (!simulationYearsInput.trim()) setSimulationYearsInput('20');
}
const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?';
return (
Where Am I Now?
{/* 1) Career */}
Current Career:{' '}
{scenarioRow?.career_name || '(Select a career)'}
{yearsInCareer && (
Time in this career: {yearsInCareer}{' '}
{yearsInCareer === '<1' ? 'year' : 'years'}
)}
{/* 2) Salary Benchmarks */}
{salaryData?.regional && (
Regional Salary Data ({userArea || 'U.S.'})
10th percentile:{' '}
{salaryData.regional.regional_PCT10
? `$${parseFloat(salaryData.regional.regional_PCT10).toLocaleString()}`
: 'N/A'}
Median:{' '}
{salaryData.regional.regional_MEDIAN
? `$${parseFloat(salaryData.regional.regional_MEDIAN).toLocaleString()}`
: 'N/A'}
90th percentile:{' '}
{salaryData.regional.regional_PCT90
? `$${parseFloat(salaryData.regional.regional_PCT90).toLocaleString()}`
: 'N/A'}
)}
{salaryData?.national && (
National Salary Data
10th percentile:{' '}
{salaryData.national.national_PCT10
? `$${parseFloat(salaryData.national.national_PCT10).toLocaleString()}`
: 'N/A'}
Median:{' '}
{salaryData.national.national_MEDIAN
? `$${parseFloat(salaryData.national.national_MEDIAN).toLocaleString()}`
: 'N/A'}
90th percentile:{' '}
{salaryData.national.national_PCT90
? `$${parseFloat(salaryData.national.national_PCT90).toLocaleString()}`
: 'N/A'}
)}
{/* 3) Economic Projections */}
{economicProjections?.state && (
)}
{economicProjections?.national && (
)}
{!economicProjections?.state && !economicProjections?.national && (
)}
{/* 4) Career Goals */}
Your Career Goals
{scenarioRow?.career_goals || 'No career goals entered yet.'}
{/* 5) Financial Projection */}
Financial Projection
{projectionData.length > 0 ? (
<>
p.month),
datasets: chartDatasets
}}
options={{
responsive: true,
plugins: {
legend: { position: 'bottom' },
tooltip: { mode: 'index', intersect: false },
annotation: { annotations: allAnnotations }
},
scales: {
y: {
beginAtZero: false,
ticks: {
callback: (val) => `$${val.toLocaleString()}`
}
}
}
}}
/>
{loanPayoffMonth && hasStudentLoan && (
Loan Paid Off at:{' '}
{loanPayoffMonth}
)}
>
) : (
No financial projection data found.
)}
{/* 6) Simulation length + Edit scenario */}
{
setShowEditModal(false);
window.location.reload();
}}
scenario={scenarioRow}
financialProfile={financialProfile}
setFinancialProfile={setFinancialProfile}
collegeProfile={collegeProfile}
setCollegeProfile={setCollegeProfile}
apiURL={apiURL}
authFetch={authFetch}
/>
{/* (E1) Interest Strategy */}
{/* (E2) If FLAT => show the annual rate */}
{interestStrategy === 'FLAT' && (
setFlatAnnualRate(parseFloatOrZero(e.target.value, 0.06))}
className="border rounded p-1 w-20"
/>
)}
{/* (E3) If MONTE_CARLO => show the random range */}
{interestStrategy === 'MONTE_CARLO' && (
setRandomRangeMin(parseFloatOrZero(e.target.value, -0.02))}
className="border rounded p-1 w-20 mr-2"
/>
setRandomRangeMax(parseFloatOrZero(e.target.value, 0.02))}
className="border rounded p-1 w-20"
/>
)}
{/* 7) AI Next Steps */}
{aiLoading &&
Generating your next steps…
}
{/* If we have structured recs, show checkboxes */}
{recommendations.length > 0 && (
Select the Advice You Want to Keep
{selectedIds.length > 0 && (
)}
)}
);
}