import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { Line } from 'react-chartjs-2'; import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; import { Filler } from 'chart.js'; import authFetch from '../utils/authFetch.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import { Button } from './ui/button.js'; import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSearch from './CareerSearch.js'; import MilestoneTimeline from './MilestoneTimeline.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; import ScenarioEditModal from './ScenarioEditModal.js'; import parseFloatOrZero from '../utils/ParseFloatorZero.js'; import './MilestoneTracker.css'; import './MilestoneTimeline.css'; // Register Chart + annotation plugin ChartJS.register( LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin ); const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const location = useLocation(); const apiURL = process.env.REACT_APP_API_URL; // -------------------------------------------------- // State // -------------------------------------------------- // User and Financial Profile Data const [userProfile, setUserProfile] = useState(null); const [financialProfile, setFinancialProfile] = useState(null); // Career & Scenario Data const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); const [careerProfileId, setCareerProfileId] = useState(null); const [existingCareerProfiles, setExistingCareerProfiles] = useState([]); const [scenarioRow, setScenarioRow] = useState(null); const [collegeProfile, setCollegeProfile] = useState(null); // Milestones & Simulation const [scenarioMilestones, setScenarioMilestones] = useState([]); const [projectionData, setProjectionData] = useState([]); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const simulationYears = parseInt(simulationYearsInput, 10) || 20; // Salary Data & Economic Projections const [salaryData, setSalaryData] = useState(null); const [economicProjections, setEconomicProjections] = useState(null); // UI Toggles const [showEditModal, setShowEditModal] = useState(false); const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [showAISuggestions, setShowAISuggestions] = useState(false); // If coming from location.state const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {}; // -------------------------------------------------- // 1) Fetch User Profile & Financial Profile // -------------------------------------------------- useEffect(() => { const fetchUserProfile = async () => { try { const res = await authFetch('/api/user-profile'); // or wherever user profile is fetched if (res.ok) { const data = await res.json(); setUserProfile(data); } else { console.error('Failed to fetch user profile:', res.status); } } catch (error) { console.error('Error fetching user profile:', error); } }; const fetchFinancialProfile = async () => { try { const res = await authFetch(`${apiURL}/premium/financial-profile`); if (res.ok) { const data = await res.json(); setFinancialProfile(data); } else { console.error('Failed to fetch financial profile:', res.status); } } catch (error) { console.error('Error fetching financial profile:', error); } }; fetchUserProfile(); fetchFinancialProfile(); }, [apiURL]); const userLocation = userProfile?.area || ''; const userSalary = financialProfile?.current_salary ?? 0; // -------------------------------------------------- // 2) Fetch user’s Career Profiles => set initial scenario // -------------------------------------------------- useEffect(() => { const fetchCareerProfiles = async () => { const res = await authFetch(`${apiURL}/premium/career-profile/all`); if (!res || !res.ok) return; const data = await res.json(); setExistingCareerProfiles(data.careerProfiles); // If there's a career in location.state, pick that const fromPopout = location.state?.selectedCareer; if (fromPopout) { setSelectedCareer(fromPopout); setCareerProfileId(fromPopout.career_profile_id); } else { // Else try localStorage const storedCareerProfileId = localStorage.getItem('lastSelectedCareerProfileId'); if (storedCareerProfileId) { const matchingCareer = data.careerProfiles.find((p) => p.id === storedCareerProfileId); if (matchingCareer) { setSelectedCareer(matchingCareer); setCareerProfileId(storedCareerProfileId); return; } } // Fallback to the "latest" scenario const latest = await authFetch(`${apiURL}/premium/career-profile/latest`); if (latest && latest.ok) { const latestData = await latest.json(); if (latestData?.id) { setSelectedCareer(latestData); setCareerProfileId(latestData.id); } } } }; fetchCareerProfiles(); }, [apiURL, location.state]); // -------------------------------------------------- // 3) Fetch scenarioRow + collegeProfile for chosen careerProfileId // -------------------------------------------------- useEffect(() => { if (!careerProfileId) { setScenarioRow(null); setCollegeProfile(null); setScenarioMilestones([]); return; } localStorage.setItem('lastSelectedCareerProfileId', careerProfileId); const fetchScenario = async () => { const scenRes = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`); if (scenRes.ok) { const data = await scenRes.json(); setScenarioRow(data); } else { console.error('Failed to fetch scenario row:', scenRes.status); setScenarioRow(null); } }; const fetchCollege = async () => { const colRes = await authFetch( `${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}` ); if (colRes.ok) { const data = await colRes.json(); setCollegeProfile(data); } else { setCollegeProfile(null); } }; fetchScenario(); fetchCollege(); }, [careerProfileId, apiURL]); // -------------------------------------------------- // 4) Fetch Salary Data for selectedCareer + userLocation // -------------------------------------------------- useEffect(() => { if (!selectedCareer?.soc_code) { setSalaryData(null); return; } const areaParam = userLocation || 'U.S.'; const fetchSalaryData = async () => { try { const queryParams = new URLSearchParams({ socCode: selectedCareer.soc_code, area: areaParam }).toString(); const res = await fetch(`/api/salary?${queryParams}`); if (!res.ok) { console.error('Error fetching salary data:', res.status); setSalaryData(null); return; } const data = await res.json(); if (data.error) { console.log('No salary data found for these params:', data.error); } setSalaryData(data); } catch (err) { console.error('Exception fetching salary data:', err); setSalaryData(null); } }; fetchSalaryData(); }, [selectedCareer, userLocation]); // -------------------------------------------------- // 5) (Optional) Fetch Economic Projections // -------------------------------------------------- useEffect(() => { if (!selectedCareer?.career_name) { setEconomicProjections(null); return; } const fetchEconomicProjections = async () => { try { const encodedCareer = encodeURIComponent(selectedCareer.career_name); const res = await authFetch('/api/projections/:socCode'); if (res.ok) { const data = await res.json(); setEconomicProjections(data); } } catch (err) { console.error('Error fetching economic projections:', err); setEconomicProjections(null); } }; fetchEconomicProjections(); }, [selectedCareer, apiURL]); // -------------------------------------------------- // 6) Once we have scenario + financial + college => run simulation // -------------------------------------------------- useEffect(() => { if (!financialProfile || !scenarioRow || !collegeProfile) return; (async () => { try { // 1) Fetch milestones for this scenario const milRes = await authFetch(`${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`); if (!milRes.ok) { console.error('Failed to fetch milestones for scenario', careerProfileId); return; } const milestonesData = await milRes.json(); const allMilestones = milestonesData.milestones || []; setScenarioMilestones(allMilestones); // 2) Fetch impacts for each milestone const impactPromises = allMilestones.map((m) => authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) .then((r) => (r.ok ? r.json() : null)) .then((data) => data?.impacts || []) .catch((err) => { console.warn('Error fetching impacts for milestone', m.id, err); return []; }) ); const impactsForEach = await Promise.all(impactPromises); // Flatten all milestone impacts const allImpacts = allMilestones.map((m, i) => ({ ...m, impacts: impactsForEach[i] || [], })).flatMap((m) => m.impacts); /******************************************************* * A) Parse numeric "financialProfile" fields *******************************************************/ const financialBase = { currentSalary: parseFloatOrZero(financialProfile.current_salary, 0), additionalIncome: parseFloatOrZero(financialProfile.additional_income, 0), monthlyExpenses: parseFloatOrZero(financialProfile.monthly_expenses, 0), monthlyDebtPayments: parseFloatOrZero(financialProfile.monthly_debt_payments, 0), retirementSavings: parseFloatOrZero(financialProfile.retirement_savings, 0), emergencySavings: parseFloatOrZero(financialProfile.emergency_fund, 0), retirementContribution: parseFloatOrZero(financialProfile.retirement_contribution, 0), emergencyContribution: parseFloatOrZero(financialProfile.emergency_contribution, 0), extraCashEmergencyPct: parseFloatOrZero(financialProfile.extra_cash_emergency_pct, 50), extraCashRetirementPct: parseFloatOrZero(financialProfile.extra_cash_retirement_pct, 50), }; /******************************************************* * B) Parse scenario overrides from "scenarioRow" *******************************************************/ const scenarioOverrides = { monthlyExpenses: parseFloatOrZero( scenarioRow.planned_monthly_expenses, financialBase.monthlyExpenses ), monthlyDebtPayments: parseFloatOrZero( scenarioRow.planned_monthly_debt_payments, financialBase.monthlyDebtPayments ), monthlyRetirementContribution: parseFloatOrZero( scenarioRow.planned_monthly_retirement_contribution, financialBase.retirementContribution ), monthlyEmergencyContribution: parseFloatOrZero( scenarioRow.planned_monthly_emergency_contribution, financialBase.emergencyContribution ), surplusEmergencyAllocation: parseFloatOrZero( scenarioRow.planned_surplus_emergency_pct, financialBase.extraCashEmergencyPct ), surplusRetirementAllocation: parseFloatOrZero( scenarioRow.planned_surplus_retirement_pct, financialBase.extraCashRetirementPct ), additionalIncome: parseFloatOrZero( scenarioRow.planned_additional_income, financialBase.additionalIncome ), }; /******************************************************* * C) Parse numeric "collegeProfile" fields *******************************************************/ const collegeData = { studentLoanAmount: parseFloatOrZero(collegeProfile.existing_college_debt, 0), interestRate: parseFloatOrZero(collegeProfile.interest_rate, 5), loanTerm: parseFloatOrZero(collegeProfile.loan_term, 10), loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation, academicCalendar: collegeProfile.academic_calendar || 'monthly', annualFinancialAid: parseFloatOrZero(collegeProfile.annual_financial_aid, 0), calculatedTuition: parseFloatOrZero(collegeProfile.tuition, 0), extraPayment: parseFloatOrZero(collegeProfile.extra_payment, 0), inCollege: collegeProfile.college_enrollment_status === 'currently_enrolled' || collegeProfile.college_enrollment_status === 'prospective_student', gradDate: collegeProfile.expected_graduation || null, programType: collegeProfile.program_type || null, creditHoursPerYear: parseFloatOrZero(collegeProfile.credit_hours_per_year, 0), hoursCompleted: parseFloatOrZero(collegeProfile.hours_completed, 0), programLength: parseFloatOrZero(collegeProfile.program_length, 0), expectedSalary: parseFloatOrZero(collegeProfile.expected_salary) || parseFloatOrZero(financialProfile.current_salary, 0), }; /******************************************************* * D) Combine them into a single mergedProfile *******************************************************/ const mergedProfile = { // Financial base currentSalary: financialBase.currentSalary, // scenario overrides (with scenario > financial precedence) monthlyExpenses: scenarioOverrides.monthlyExpenses, monthlyDebtPayments: scenarioOverrides.monthlyDebtPayments, // big items from financialProfile that had no scenario override retirementSavings: financialBase.retirementSavings, emergencySavings: financialBase.emergencySavings, // scenario overrides for monthly contributions monthlyRetirementContribution: scenarioOverrides.monthlyRetirementContribution, monthlyEmergencyContribution: scenarioOverrides.monthlyEmergencyContribution, // scenario overrides for surplus distribution surplusEmergencyAllocation: scenarioOverrides.surplusEmergencyAllocation, surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation, // scenario override for additionalIncome additionalIncome: scenarioOverrides.additionalIncome, // college fields 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, // scenario horizon + milestone impacts startDate: new Date().toISOString(), simulationYears, milestoneImpacts: allImpacts }; // 3) Run the simulation const { projectionData: pData, loanPaidOffMonth: payoff } = simulateFinancialProjection(mergedProfile); // 4) Add cumulative net savings let cumu = mergedProfile.emergencySavings || 0; const finalData = pData.map((mo) => { cumu += mo.netSavings || 0; return { ...mo, cumulativeNetSavings: cumu }; }); setProjectionData(finalData); setLoanPayoffMonth(payoff); } catch (err) { console.error('Error in scenario simulation:', err); } })(); }, [ financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears ]); // -------------------------------------------------- // Handlers & Chart Setup // -------------------------------------------------- const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value); const handleSimulationYearsBlur = () => { if (!simulationYearsInput.trim()) { setSimulationYearsInput('20'); } }; // Build chart annotations from scenarioMilestones const milestoneAnnotationLines = {}; scenarioMilestones.forEach((m) => { if (!m.date) return; const d = new Date(m.date); if (isNaN(d)) return; const year = d.getUTCFullYear(); const month = String(d.getUTCMonth() + 1).padStart(2, '0'); const short = `${year}-${month}`; 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' } }; }); // Loan payoff line const annotationConfig = {}; if (loanPayoffMonth) { 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 }; // Salary Gauge function getRelativePosition(userSal, p10, p90) { if (!p10 || !p90) return 0; // avoid NaN if (userSal < p10) return 0; if (userSal > p90) return 1; return (userSal - p10) / (p90 - p10); } const SalaryGauge = ({ userSalary, percentileRow, prefix = 'regional' }) => { if (!percentileRow) return null; const p10 = percentileRow[`${prefix}_PCT10`]; const p90 = percentileRow[`${prefix}_PCT90`]; if (!p10 || !p90) return null; const fraction = getRelativePosition(userSalary, p10, p90) * 100; return (
You are at {Math.round(fraction)}% between the 10th and 90th percentiles ( {prefix}).
10th percentile:{' '} ${salaryData.regional.regional_PCT10?.toLocaleString() ?? 'N/A'}
25th percentile:{' '} ${salaryData.regional.regional_PCT25?.toLocaleString() ?? 'N/A'}
Median:{' '} ${salaryData.regional.regional_MEDIAN?.toLocaleString() ?? 'N/A'}
75th percentile:{' '} ${salaryData.regional.regional_PCT75?.toLocaleString() ?? 'N/A'}
90th percentile:{' '} ${salaryData.regional.regional_PCT90?.toLocaleString() ?? 'N/A'}
10th percentile:{' '} ${salaryData.national.national_PCT10?.toLocaleString() ?? 'N/A'}
25th percentile:{' '} ${salaryData.national.national_PCT25?.toLocaleString() ?? 'N/A'}
Median:{' '} ${salaryData.national.national_MEDIAN?.toLocaleString() ?? 'N/A'}
75th percentile:{' '} ${salaryData.national.national_PCT75?.toLocaleString() ?? 'N/A'}
90th percentile:{' '} ${salaryData.national.national_PCT90?.toLocaleString() ?? 'N/A'}
Your current salary: ${userSalary.toLocaleString()}
Loan Paid Off at: {loanPayoffMonth}
)}Growth Outlook: {economicProjections.growthOutlook || 'N/A'}
AI Automation Risk: {economicProjections.aiRisk || 'N/A'}
{economicProjections.chatGPTAnalysis && ({economicProjections.chatGPTAnalysis}