816 lines
28 KiB
JavaScript
816 lines
28 KiB
JavaScript
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 (
|
||
<div className="mb-4">
|
||
<div
|
||
style={{
|
||
border: '1px solid #ccc',
|
||
width: '100%',
|
||
height: '12px',
|
||
position: 'relative'
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
left: `${fraction}%`,
|
||
transform: 'translateX(-50%)',
|
||
top: 0,
|
||
bottom: 0,
|
||
width: '2px',
|
||
backgroundColor: 'red'
|
||
}}
|
||
></div>
|
||
</div>
|
||
<p>
|
||
You are at <strong>{Math.round(fraction)}%</strong> between the 10th and 90th percentiles (
|
||
{prefix}).
|
||
</p>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-6">
|
||
{/* 1) Career dropdown */}
|
||
<CareerSelectDropdown
|
||
existingCareerProfiles={existingCareerProfiles}
|
||
selectedCareer={selectedCareer}
|
||
onChange={(selected) => {
|
||
setSelectedCareer(selected);
|
||
setCareerProfileId(selected?.id || null);
|
||
}}
|
||
loading={!existingCareerProfiles.length}
|
||
authFetch={authFetch}
|
||
/>
|
||
|
||
{/* 2) Salary Data Display */}
|
||
{salaryData && (
|
||
<div className="salary-display-container bg-white p-4 rounded shadow">
|
||
<h3 className="text-lg font-semibold mb-2">Salary Overview</h3>
|
||
{/* Regional Salaries */}
|
||
{salaryData.regional && (
|
||
<div className="mb-4">
|
||
<h4 className="font-medium">Regional Salaries (Area: {userLocation || 'U.S.'})</h4>
|
||
<p>
|
||
<strong>10th percentile:</strong>{' '}
|
||
${salaryData.regional.regional_PCT10?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>25th percentile:</strong>{' '}
|
||
${salaryData.regional.regional_PCT25?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>Median:</strong>{' '}
|
||
${salaryData.regional.regional_MEDIAN?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>75th percentile:</strong>{' '}
|
||
${salaryData.regional.regional_PCT75?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>90th percentile:</strong>{' '}
|
||
${salaryData.regional.regional_PCT90?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<SalaryGauge
|
||
userSalary={userSalary}
|
||
percentileRow={salaryData.regional}
|
||
prefix="regional"
|
||
/>
|
||
</div>
|
||
)}
|
||
{/* National Salaries */}
|
||
{salaryData.national && (
|
||
<div>
|
||
<h4 className="font-medium">National Salaries</h4>
|
||
<p>
|
||
<strong>10th percentile:</strong>{' '}
|
||
${salaryData.national.national_PCT10?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>25th percentile:</strong>{' '}
|
||
${salaryData.national.national_PCT25?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>Median:</strong>{' '}
|
||
${salaryData.national.national_MEDIAN?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>75th percentile:</strong>{' '}
|
||
${salaryData.national.national_PCT75?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>90th percentile:</strong>{' '}
|
||
${salaryData.national.national_PCT90?.toLocaleString() ?? 'N/A'}
|
||
</p>
|
||
<SalaryGauge
|
||
userSalary={userSalary}
|
||
percentileRow={salaryData.national}
|
||
prefix="national"
|
||
/>
|
||
</div>
|
||
)}
|
||
<p className="mt-2">
|
||
Your current salary: <strong>${userSalary.toLocaleString()}</strong>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 3) Milestone Timeline */}
|
||
<MilestoneTimeline
|
||
careerProfileId={careerProfileId}
|
||
authFetch={authFetch}
|
||
activeView="Career"
|
||
onMilestoneUpdated={() => {}}
|
||
/>
|
||
|
||
{/* 4) AI Suggestions Button */}
|
||
{!showAISuggestions && (
|
||
<Button
|
||
onClick={() => setShowAISuggestions(true)}
|
||
className="bg-green-500 hover:bg-green-600 text-white font-semibold px-4 py-2 rounded"
|
||
>
|
||
Show AI Suggestions
|
||
</Button>
|
||
)}
|
||
|
||
{/* 5) AI-Suggested Milestones */}
|
||
{showAISuggestions && (
|
||
<AISuggestedMilestones
|
||
career={selectedCareer?.career_name}
|
||
careerProfileId={careerProfileId}
|
||
authFetch={authFetch}
|
||
activeView="Career"
|
||
projectionData={projectionData}
|
||
/>
|
||
)}
|
||
|
||
{/* 6) Financial Projection Chart */}
|
||
{projectionData.length > 0 && (
|
||
<div className="bg-white p-4 rounded shadow space-y-4">
|
||
<h3 className="text-lg font-semibold">Financial Projection</h3>
|
||
<Line
|
||
data={{
|
||
labels: projectionData.map((p) => p.month),
|
||
datasets: [
|
||
{
|
||
label: 'Total Savings',
|
||
data: projectionData.map((p) => p.cumulativeNetSavings),
|
||
borderColor: 'rgba(54, 162, 235, 1)',
|
||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||
tension: 0.4,
|
||
fill: true
|
||
},
|
||
{
|
||
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'
|
||
}
|
||
},
|
||
{
|
||
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
|
||
}
|
||
]
|
||
}}
|
||
options={{
|
||
responsive: true,
|
||
plugins: {
|
||
legend: { position: 'bottom' },
|
||
tooltip: { mode: 'index', intersect: false },
|
||
annotation: {
|
||
annotations: allAnnotations
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: false,
|
||
ticks: {
|
||
callback: (value) => `$${value.toLocaleString()}`
|
||
}
|
||
}
|
||
}
|
||
}}
|
||
/>
|
||
<div>
|
||
{loanPayoffMonth && (
|
||
<p className="font-semibold text-sm">
|
||
Loan Paid Off at: <span className="text-yellow-600">{loanPayoffMonth}</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 7) Simulation length + "Edit" => open ScenarioEditModal */}
|
||
<div className="space-x-2">
|
||
<label className="font-medium">Simulation Length (years):</label>
|
||
<input
|
||
type="text"
|
||
value={simulationYearsInput}
|
||
onChange={handleSimulationYearsChange}
|
||
onBlur={handleSimulationYearsBlur}
|
||
className="border rounded p-1 w-16"
|
||
/>
|
||
<Button onClick={() => setShowEditModal(true)} className="ml-2">
|
||
Edit
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 8) Economic Projections Section */}
|
||
{economicProjections && (
|
||
<div className="bg-white p-4 rounded shadow">
|
||
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
|
||
<p>
|
||
<strong>Growth Outlook:</strong> {economicProjections.growthOutlook || 'N/A'}
|
||
</p>
|
||
<p>
|
||
<strong>AI Automation Risk:</strong> {economicProjections.aiRisk || 'N/A'}
|
||
</p>
|
||
{economicProjections.chatGPTAnalysis && (
|
||
<div className="mt-2 border border-gray-200 p-2 rounded">
|
||
<h4 className="font-semibold">ChatGPT Analysis:</h4>
|
||
<p>{economicProjections.chatGPTAnalysis}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 9) Career Search & Potential new scenario creation */}
|
||
<CareerSearch
|
||
onCareerSelected={(careerObj) => {
|
||
setPendingCareerForModal(careerObj.title);
|
||
}}
|
||
/>
|
||
{pendingCareerForModal && (
|
||
<Button
|
||
onClick={() => {
|
||
console.log('User confirmed new career path:', pendingCareerForModal);
|
||
setPendingCareerForModal(null);
|
||
}}
|
||
className="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded mt-2"
|
||
>
|
||
Confirm Career Change to {pendingCareerForModal}
|
||
</Button>
|
||
)}
|
||
|
||
{/* 10) Scenario Edit Modal */}
|
||
<ScenarioEditModal
|
||
show={showEditModal}
|
||
onClose={() => {
|
||
setShowEditModal(false);
|
||
window.location.reload();
|
||
}}
|
||
scenario={scenarioRow}
|
||
financialProfile={financialProfile}
|
||
setFinancialProfile={setFinancialProfile}
|
||
collegeProfile={collegeProfile}
|
||
setCollegeProfile={setCollegeProfile}
|
||
apiURL={apiURL}
|
||
authFetch={authFetch}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default MilestoneTracker;
|