// src/components/MilestoneTracker.js import React, { useState, useEffect } from 'react'; import { useLocation, useNavigate } 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 { Button } from './ui/button.js'; import authFetch from '../utils/authFetch.js'; import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSearch from './CareerSearch.js'; // Keep MilestoneTimeline for +Add Milestone & tasks CRUD import MilestoneTimeline from './MilestoneTimeline.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; import ScenarioEditModal from './ScenarioEditModal.js'; import './MilestoneTracker.css'; import './MilestoneTimeline.css'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; // Register Chart + annotation plugin ChartJS.register( LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin ); const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const location = useLocation(); const navigate = useNavigate(); const apiURL = process.env.REACT_APP_API_URL; // -------------------------------------------------- // State // -------------------------------------------------- const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); const [careerPathId, setCareerPathId] = useState(null); const [existingCareerPaths, setExistingCareerPaths] = useState([]); const [activeView, setActiveView] = useState('Career'); const [financialProfile, setFinancialProfile] = useState(null); const [scenarioRow, setScenarioRow] = useState(null); const [collegeProfile, setCollegeProfile] = useState(null); // We will store the scenario’s milestones in state so we can build annotation lines const [scenarioMilestones, setScenarioMilestones] = useState([]); const [projectionData, setProjectionData] = useState([]); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const simulationYears = parseInt(simulationYearsInput, 10) || 20; // --- ADDED: showEditModal state const [showEditModal, setShowEditModal] = useState(false); const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {}; // -------------------------------------------------- // 1) Fetch user’s scenario list + financialProfile // -------------------------------------------------- useEffect(() => { const fetchCareerPaths = async () => { const res = await authFetch(`${apiURL}/premium/career-profile/all`); if (!res || !res.ok) return; const data = await res.json(); setExistingCareerPaths(data.careerPaths); const fromPopout = location.state?.selectedCareer; if (fromPopout) { setSelectedCareer(fromPopout); setCareerPathId(fromPopout.career_path_id); } else if (!selectedCareer) { // fallback: fetch 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); setCareerPathId(latestData.id); } } } }; const fetchFinancialProfile = async () => { const res = await authFetch(`${apiURL}/premium/financial-profile`); if (res?.ok) { const data = await res.json(); setFinancialProfile(data); } }; fetchCareerPaths(); fetchFinancialProfile(); }, [apiURL, location.state, selectedCareer]); // -------------------------------------------------- // 2) When careerPathId changes => fetch scenarioRow + collegeProfile // -------------------------------------------------- useEffect(() => { if (!careerPathId) { setScenarioRow(null); setCollegeProfile(null); setScenarioMilestones([]); return; } async function fetchScenario() { const scenRes = await authFetch(`${apiURL}/premium/career-profile/${careerPathId}`); if (scenRes.ok) { const data = await scenRes.json(); setScenarioRow(data); } else { console.error('Failed to fetch scenario row:', scenRes.status); setScenarioRow(null); } } async function fetchCollege() { const colRes = await authFetch( `${apiURL}/premium/college-profile?careerPathId=${careerPathId}` ); if (!colRes?.ok) { setCollegeProfile(null); return; } const data = await colRes.json(); setCollegeProfile(data); } fetchScenario(); fetchCollege(); }, [careerPathId, apiURL]); // -------------------------------------------------- // 3) Once scenarioRow + collegeProfile + financialProfile => run simulation // + fetch milestones for annotation lines // -------------------------------------------------- useEffect(() => { if (!financialProfile || !scenarioRow || !collegeProfile) return; (async () => { try { // fetch milestones for this scenario const milRes = await authFetch( `${apiURL}/premium/milestones?careerPathId=${careerPathId}` ); if (!milRes.ok) { console.error('Failed to fetch milestones for scenario', careerPathId); return; } const milestonesData = await milRes.json(); const allMilestones = milestonesData.milestones || []; setScenarioMilestones(allMilestones); // store them for annotation lines // fetch impacts for each 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); const milestonesWithImpacts = allMilestones.map((m, i) => ({ ...m, impacts: impactsForEach[i] || [] })); // flatten all const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts); // mergedProfile const mergedProfile = { currentSalary: financialProfile.current_salary || 0, monthlyExpenses: scenarioRow.planned_monthly_expenses ?? financialProfile.monthly_expenses ?? 0, monthlyDebtPayments: scenarioRow.planned_monthly_debt_payments ?? financialProfile.monthly_debt_payments ?? 0, retirementSavings: financialProfile.retirement_savings ?? 0, emergencySavings: financialProfile.emergency_fund ?? 0, monthlyRetirementContribution: scenarioRow.planned_monthly_retirement_contribution ?? financialProfile.retirement_contribution ?? 0, monthlyEmergencyContribution: scenarioRow.planned_monthly_emergency_contribution ?? financialProfile.emergency_contribution ?? 0, surplusEmergencyAllocation: scenarioRow.planned_surplus_emergency_pct ?? financialProfile.extra_cash_emergency_pct ?? 50, surplusRetirementAllocation: scenarioRow.planned_surplus_retirement_pct ?? financialProfile.extra_cash_retirement_pct ?? 50, additionalIncome: scenarioRow.planned_additional_income ?? financialProfile.additional_income ?? 0, // college studentLoanAmount: collegeProfile.existing_college_debt || 0, interestRate: collegeProfile.interest_rate || 5, loanTerm: collegeProfile.loan_term || 10, loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation, academicCalendar: collegeProfile.academic_calendar || 'monthly', annualFinancialAid: collegeProfile.annual_financial_aid || 0, calculatedTuition: collegeProfile.tuition || 0, extraPayment: 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: collegeProfile.credit_hours_per_year || 0, hoursCompleted: collegeProfile.hours_completed || 0, programLength: collegeProfile.program_length || 0, expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary || 0, // scenario horizon startDate: new Date().toISOString(), simulationYears, milestoneImpacts: allImpacts }; const { projectionData: pData, loanPaidOffMonth: payoff } = simulateFinancialProjection(mergedProfile); 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, careerPathId, apiURL, simulationYears]); // If you want to re-run simulation after any milestone changes: const reSimulate = async () => { // Put your logic to re-fetch scenario + milestones, then re-run sim (if needed). }; // handle user typing simulation length const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value); const handleSimulationYearsBlur = () => { if (!simulationYearsInput.trim()) { setSimulationYearsInput('20'); } }; // Build annotation lines 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' }, // If you want them clickable: onClick: () => { console.log('Clicked milestone line => open editing for', m.title); // e.g. open the MilestoneTimeline's edit feature, or do something } }; }); // If we also show a line for payoff: 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 }; return (
Loan Paid Off at: {loanPayoffMonth}
)}