diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js
index 2b15dcf..d7f6c7d 100644
--- a/src/components/MilestoneTimeline.js
+++ b/src/components/MilestoneTimeline.js
@@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from 'react';
const today = new Date();
-const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
+const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView, onMilestoneUpdated }) => {
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
// The "new or edit" milestone form state
@@ -148,6 +148,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
alert(errorData.error || 'Error saving milestone');
return;
}
+ if (onMilestoneUpdated) onMilestoneUpdated();
const savedMilestone = await res.json();
console.log('Milestone saved/updated:', savedMilestone);
diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js
index c94c2b6..bd6c395 100644
--- a/src/components/MilestoneTracker.js
+++ b/src/components/MilestoneTracker.js
@@ -1,51 +1,74 @@
// src/components/MilestoneTracker.js
+
import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import { Line } from 'react-chartjs-2';
-import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend } from 'chart.js';
+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 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 './MilestoneTracker.css';
import './MilestoneTimeline.css';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
-import ScenarioEditModal from './ScenarioEditModal.js';
-ChartJS.register(LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin);
+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 [pendingCareerForModal, setPendingCareerForModal] = useState(null);
const [activeView, setActiveView] = useState("Career");
- // Store each profile separately
const [financialProfile, setFinancialProfile] = useState(null);
const [collegeProfile, setCollegeProfile] = useState(null);
- // For the chart
const [projectionData, setProjectionData] = useState([]);
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
const [showEditModal, setShowEditModal] = useState(false);
- const apiURL = process.env.REACT_APP_API_URL;
-
// Possibly loaded from location.state
- const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {};
+ const {
+ projectionData: initialProjectionData = [],
+ loanPayoffMonth: initialLoanPayoffMonth = null
+ } = location.state || {};
- // ----------------------------
- // 1. Fetch career paths + financialProfile
- // ----------------------------
+ // -------------------------
+ // 1. Fetch career paths + financialProfile on mount
+ // -------------------------
useEffect(() => {
const fetchCareerPaths = async () => {
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
@@ -58,6 +81,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
setSelectedCareer(fromPopout);
setCareerPathId(fromPopout.career_path_id);
} else if (!selectedCareer) {
+ // Try to fetch the latest
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
if (latest && latest.ok) {
const latestData = await latest.json();
@@ -81,9 +105,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
fetchFinancialProfile();
}, [apiURL, location.state, selectedCareer]);
- // ----------------------------
+ // -------------------------
// 2. Fetch the college profile for the selected careerPathId
- // ----------------------------
+ // -------------------------
useEffect(() => {
if (!careerPathId) {
setCollegeProfile(null);
@@ -91,109 +115,234 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
}
const fetchCollegeProfile = async () => {
- // If you have a route like GET /api/premium/college-profile?careerPathId=XYZ
const res = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
if (!res || !res.ok) {
setCollegeProfile(null);
return;
}
const data = await res.json();
- setCollegeProfile(data); // could be an object or empty {}
+ setCollegeProfile(data);
};
fetchCollegeProfile();
}, [careerPathId, apiURL]);
- // ----------------------------
- // 3. Merge data + simulate once both profiles + selectedCareer are loaded
- // ----------------------------
+ // -------------------------
+ // 3. Initial simulation when profiles + career loaded
+ // (But this does NOT update after milestone changes yet)
+ // -------------------------
useEffect(() => {
- if (!financialProfile || !collegeProfile || !selectedCareer) return;
- console.log("About to build mergedProfile");
- console.log("collegeProfile from DB/fetch = ", collegeProfile);
- console.log(
- "college_enrollment_status check:",
- "[" + collegeProfile.college_enrollment_status + "]",
- "length=", collegeProfile.college_enrollment_status?.length
- );
- console.log(
- "Comparison => ",
- collegeProfile.college_enrollment_status === 'currently_enrolled'
- );
+ if (!financialProfile || !collegeProfile || !selectedCareer || !careerPathId) return;
+ // 1) Fetch the raw milestones for this careerPath
+ (async () => {
+ try {
+ const milRes = await authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`);
+ if (!milRes.ok) {
+ console.error('Failed to fetch initial milestones');
+ return;
+ }
+ const milestonesData = await milRes.json();
+ const allMilestones = milestonesData.milestones || [];
+
+ // 2) For each milestone, fetch impacts
+ 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.error('Failed fetching impacts for milestone', m.id, err);
+ return [];
+ })
+ );
+ const impactsForEach = await Promise.all(impactPromises);
+ const milestonesWithImpacts = allMilestones.map((m, i) => ({
+ ...m,
+ impacts: impactsForEach[i] || [],
+ }));
+
+ // 3) Flatten them
+ const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts || []);
+
+ // 4) Build the mergedProfile (like you already do)
+ const mergedProfile = {
+ // From financialProfile
+ currentSalary: financialProfile.current_salary || 0,
+ monthlyExpenses: financialProfile.monthly_expenses || 0,
+ monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
+ retirementSavings: financialProfile.retirement_savings || 0,
+ emergencySavings: financialProfile.emergency_fund || 0,
+ monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
+ monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
+ surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
+ surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
+
+ // From collegeProfile
+ 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,
+ creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
+ hoursCompleted: collegeProfile.hours_completed || 0,
+ programLength: collegeProfile.program_length || 0,
+ startDate: new Date().toISOString(),
+ expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
+
+ // The key: impacts
+ milestoneImpacts: allImpacts
+ };
+
+ // 5) Run the simulation
+ const { projectionData: initialProjData, loanPaidOffMonth: payoff } =
+ simulateFinancialProjection(mergedProfile);
+
+ let cumulativeSavings = mergedProfile.emergencySavings || 0;
+ const finalData = initialProjData.map((month) => {
+ cumulativeSavings += (month.netSavings || 0);
+ return { ...month, cumulativeNetSavings: cumulativeSavings };
+ });
+
+ setProjectionData(finalData);
+ setLoanPayoffMonth(payoff);
+
+ } catch (err) {
+ console.error('Error fetching initial milestones/impacts or simulating:', err);
+ }
+ })();
+ }, [financialProfile, collegeProfile, selectedCareer, careerPathId]);
- // Merge financial + college data
- const mergedProfile = {
- // From financialProfile
- currentSalary: financialProfile.current_salary || 0,
- monthlyExpenses: financialProfile.monthly_expenses || 0,
- monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
- retirementSavings: financialProfile.retirement_savings || 0,
- emergencySavings: financialProfile.emergency_fund || 0,
- monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
- monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
- surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
- surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
+ // -------------------------------------------------
+ // 4. reSimulate() => re-fetch everything (financial, college, milestones & impacts),
+ // re-run the simulation. This is triggered AFTER user updates a milestone in MilestoneTimeline.
+ // -------------------------------------------------
+ const reSimulate = async () => {
+ if (!careerPathId) return;
- // From collegeProfile
- 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,
- partTimeIncome: 0, // or collegeProfile.part_time_income if you store it
- gradDate: collegeProfile.expected_graduation || null,
- programType: collegeProfile.program_type,
- creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
- hoursCompleted: collegeProfile.hours_completed || 0,
- programLength: collegeProfile.program_length || 0,
+ try {
+ // 1) Fetch financial + college + raw milestones
+ const [finResp, colResp, milResp] = await Promise.all([
+ authFetch(`${apiURL}/premium/financial-profile`),
+ authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`),
+ authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`)
+ ]);
- // Are they in college?
- inCollege: (collegeProfile.college_enrollment_status === 'currently_enrolled' ||
- collegeProfile.college_enrollment_status === 'prospective_student'),
- // If they've graduated or not in college, false
- startDate: new Date().toISOString(),
- // Future logic could set expectedSalary if there's a difference
- expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
- };
+ if (!finResp.ok || !colResp.ok || !milResp.ok) {
+ console.error('One reSimulate fetch failed:', finResp.status, colResp.status, milResp.status);
+ return;
+ }
- const result = simulateFinancialProjection(mergedProfile);
- console.log("mergedProfile for simulation:", mergedProfile);
+ const [updatedFinancial, updatedCollege, milestonesData] = await Promise.all([
+ finResp.json(),
+ colResp.json(),
+ milResp.json()
+ ]);
- const { projectionData, loanPaidOffMonth } = result;
-
- // If you want to accumulate net savings:
- let cumulativeSavings = mergedProfile.emergencySavings || 0;
- const cumulativeProjectionData = projectionData.map(month => {
- cumulativeSavings += (month.netSavings || 0);
- return { ...month, cumulativeNetSavings: cumulativeSavings };
- });
+ // 2) For each milestone, fetch its impacts separately (if not already included)
+ const allMilestones = milestonesData.milestones || [];
+ const impactsPromises = 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.error('Failed fetching impacts for milestone', m.id, err);
+ return [];
+ })
+ );
- if (cumulativeProjectionData.length > 0) {
- setProjectionData(cumulativeProjectionData);
- setLoanPayoffMonth(loanPaidOffMonth);
+ const impactsForEach = await Promise.all(impactsPromises);
+ // Merge them onto the milestone array if desired
+ const milestonesWithImpacts = allMilestones.map((m, i) => ({
+ ...m,
+ impacts: impactsForEach[i] || []
+ }));
+
+ // Flatten or gather all impacts if your simulation function needs them
+ const allImpacts = milestonesWithImpacts.flatMap(m => m.impacts || []);
+
+ // 3) Build mergedProfile
+ const mergedProfile = {
+ // From updatedFinancial
+ currentSalary: updatedFinancial.current_salary || 0,
+ monthlyExpenses: updatedFinancial.monthly_expenses || 0,
+ monthlyDebtPayments: updatedFinancial.monthly_debt_payments || 0,
+ retirementSavings: updatedFinancial.retirement_savings || 0,
+ emergencySavings: updatedFinancial.emergency_fund || 0,
+ monthlyRetirementContribution: updatedFinancial.retirement_contribution || 0,
+ monthlyEmergencyContribution: updatedFinancial.emergency_contribution || 0,
+ surplusEmergencyAllocation: updatedFinancial.extra_cash_emergency_pct || 50,
+ surplusRetirementAllocation: updatedFinancial.extra_cash_retirement_pct || 50,
+
+ // From updatedCollege
+ studentLoanAmount: updatedCollege.existing_college_debt || 0,
+ interestRate: updatedCollege.interest_rate || 5,
+ loanTerm: updatedCollege.loan_term || 10,
+ loanDeferralUntilGraduation: !!updatedCollege.loan_deferral_until_graduation,
+ academicCalendar: updatedCollege.academic_calendar || 'monthly',
+ annualFinancialAid: updatedCollege.annual_financial_aid || 0,
+ calculatedTuition: updatedCollege.tuition || 0,
+ extraPayment: updatedCollege.extra_payment || 0,
+ inCollege:
+ updatedCollege.college_enrollment_status === 'currently_enrolled' ||
+ updatedCollege.college_enrollment_status === 'prospective_student',
+ gradDate: updatedCollege.expected_graduation || null,
+ programType: updatedCollege.program_type,
+ creditHoursPerYear: updatedCollege.credit_hours_per_year || 0,
+ hoursCompleted: updatedCollege.hours_completed || 0,
+ programLength: updatedCollege.program_length || 0,
+ startDate: new Date().toISOString(),
+ expectedSalary: updatedCollege.expected_salary || updatedFinancial.current_salary,
+
+ // The key: pass the impacts to the simulation if needed
+ milestoneImpacts: allImpacts
+ };
+
+ // 4) Re-run simulation
+ const { projectionData: newProjData, loanPaidOffMonth: payoff } =
+ simulateFinancialProjection(mergedProfile);
+
+ // 5) If you track cumulative net savings:
+ let cumulativeSavings = mergedProfile.emergencySavings || 0;
+ const finalData = newProjData.map(month => {
+ cumulativeSavings += (month.netSavings || 0);
+ return { ...month, cumulativeNetSavings: cumulativeSavings };
+ });
+
+ // 6) Update states => triggers chart refresh
+ setProjectionData(finalData);
+ setLoanPayoffMonth(payoff);
+
+ // Optionally store the new profiles in state if you like
+ setFinancialProfile(updatedFinancial);
+ setCollegeProfile(updatedCollege);
+
+ console.log('Re-simulated after Milestone update!', {
+ mergedProfile,
+ milestonesWithImpacts
+ });
+
+ } catch (err) {
+ console.error('Error in reSimulate:', err);
}
+ };
- console.log('mergedProfile for simulation:', mergedProfile);
-
- }, [financialProfile, collegeProfile, selectedCareer]);
-
- // 4. The rest of your code is unchanged, e.g. handleConfirmCareerSelection, etc.
- // ...
-
-
+ // ...
+ // The rest of your component logic
+ // ...
console.log(
'First 5 items of projectionData:',
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
);
- // ...
- // The remainder of your component: timeline, chart, AISuggestedMilestones, etc.
- // ...
return (
{
authFetch={authFetch}
/>
+ {/* Pass reSimulate as onMilestoneUpdated: */}
{
Financial Projection
p.month),
+ labels: projectionData.map((p) => p.month),
datasets: [
{
label: 'Total Savings',
- data: projectionData.map(p => p.cumulativeNetSavings),
+ data: projectionData.map((p) => p.cumulativeNetSavings),
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
tension: 0.4,
- fill: true,
+ fill: true
},
{
label: 'Loan Balance',
- data: projectionData.map(p => p.loanBalance),
+ data: projectionData.map((p) => p.loanBalance),
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
tension: 0.4,
@@ -251,7 +402,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
},
{
label: 'Retirement Savings',
- data: projectionData.map(p => p.retirementSavings),
+ data: projectionData.map((p) => p.retirementSavings),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4,
@@ -308,23 +459,23 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
authFetch={authFetch}
/>
-
- {/* SCENARIO EDIT MODAL */}
- setShowEditModal(false)}
- financialProfile={financialProfile}
- setFinancialProfile={setFinancialProfile}
- collegeProfile={collegeProfile}
- setCollegeProfile={setCollegeProfile}
- apiURL={apiURL}
- authFetch={authFetch}
- />
+ setShowEditModal(false)}
+ financialProfile={financialProfile}
+ setFinancialProfile={setFinancialProfile}
+ collegeProfile={collegeProfile}
+ setCollegeProfile={setCollegeProfile}
+ apiURL={apiURL}
+ authFetch={authFetch}
+ />
{pendingCareerForModal && (
-