diff --git a/backend/server3.js b/backend/server3.js
index 41c8fe9..da56036 100644
--- a/backend/server3.js
+++ b/backend/server3.js
@@ -71,14 +71,17 @@ const authenticatePremiumUser = (req, res, next) => {
// GET the latest selected career profile
app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => {
try {
- const [rows] = await pool.query(`
- SELECT *
+ const sql = `
+ SELECT
+ *,
+ DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
+ DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles
WHERE user_id = ?
ORDER BY start_date DESC
LIMIT 1
- `, [req.id]);
-
+ `;
+ const [rows] = await pool.query(sql, [req.id]);
res.json(rows[0] || {});
} catch (error) {
console.error('Error fetching latest career profile:', error);
@@ -89,13 +92,16 @@ app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (re
// GET all career profiles for the user
app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => {
try {
- const [rows] = await pool.query(`
- SELECT *
+ const sql = `
+ SELECT
+ *,
+ DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
+ DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles
WHERE user_id = ?
ORDER BY start_date ASC
- `, [req.id]);
-
+ `;
+ const [rows] = await pool.query(sql, [req.id]);
res.json({ careerProfiles: rows });
} catch (error) {
console.error('Error fetching career profiles:', error);
@@ -107,13 +113,18 @@ app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req,
app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => {
const { careerProfileId } = req.params;
try {
- const [rows] = await pool.query(`
- SELECT *
+ const sql = `
+ SELECT
+ *,
+ DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
+ DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles
WHERE id = ?
AND user_id = ?
- `, [careerProfileId, req.id]);
-
+ LIMIT 1
+ `;
+ const [rows] = await pool.query(sql, [careerProfileId, req.id]);
+
if (!rows[0]) {
return res.status(404).json({ error: 'Career profile not found or not yours.' });
}
@@ -124,6 +135,7 @@ app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser,
}
});
+
// POST a new career profile (upsert)
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const {
diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js
index 41e79b7..7de3787 100644
--- a/src/components/MilestoneTracker.js
+++ b/src/components/MilestoneTracker.js
@@ -1,35 +1,39 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
-import { Line } from 'react-chartjs-2';
+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 { Filler } from 'chart.js';
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 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';
+
+// If you need AI suggestions in the future:
+// import AISuggestedMilestones from './AISuggestedMilestones.js';
import './MilestoneTracker.css';
import './MilestoneTimeline.css';
-// Register Chart + annotation plugin
ChartJS.register(
LineElement,
+ BarElement,
CategoryScale,
LinearScale,
Filler,
@@ -39,130 +43,311 @@ ChartJS.register(
annotationPlugin
);
-const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
+// ----------------------
+// 1) Remove decimals from SOC code
+// ----------------------
+function stripSocCode(fullSoc) {
+ if (!fullSoc) return '';
+ return fullSoc.split('.')[0];
+}
+
+// ----------------------
+// 2) Salary Gauge
+// ----------------------
+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);
+}
+
+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 ${median.toLocaleString()}
+
+
+
+
+
+ ${userSalary.toLocaleString()}
+
+
+
+
+ );
+}
+
+// ----------------------
+// 3) Economic Bar
+// ----------------------
+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();
+}
+
+// ----------------------
+// 4) MilestoneTracker
+// ----------------------
+export default function 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 [masterCareerRatings, setMasterCareerRatings] = useState([]);
+ const [existingCareerProfiles, setExistingCareerProfiles] = useState([]);
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 [strippedSocCode, setStrippedSocCode] = useState(null);
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);
+ const [scenarioMilestones, setScenarioMilestones] = useState([]);
+ const [projectionData, setProjectionData] = useState([]);
+ const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
+
+ const [simulationYearsInput, setSimulationYearsInput] = useState('20');
+ const simulationYears = parseInt(simulationYearsInput, 10) || 20;
+
+ const [showEditModal, setShowEditModal] = useState(false);
- // If coming from location.state
const {
- projectionData: initialProjectionData = [],
- loanPayoffMonth: initialLoanPayoffMonth = null
+ projectionData: initProjData = [],
+ loanPayoffMonth: initLoanMonth = null
} = location.state || {};
- // --------------------------------------------------
- // 1) Fetch User Profile & Financial Profile
- // --------------------------------------------------
+ // 1) Fetch user + financial
useEffect(() => {
- const fetchUserProfile = async () => {
+ const fetchUser = 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 r = await authFetch('/api/user-profile');
+ if (r.ok) setUserProfile(await r.json());
+ } catch (err) {
+ console.error('Error user-profile =>', err);
}
};
-
- const fetchFinancialProfile = async () => {
+ const fetchFin = 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);
+ const r = await authFetch(`${apiURL}/premium/financial-profile`);
+ if (r.ok) setFinancialProfile(await r.json());
+ } catch (err) {
+ console.error('Error financial =>', err);
}
};
-
- fetchUserProfile();
- fetchFinancialProfile();
+ fetchUser();
+ fetchFin();
}, [apiURL]);
- const userLocation = userProfile?.area || '';
- const userSalary = financialProfile?.current_salary ?? 0;
+ const userSalary = parseFloatOrZero(financialProfile?.current_salary, 0);
+ const userArea = userProfile?.area || 'U.S.';
+ const userState = getFullStateName(userProfile?.state || '') || 'United States';
- // --------------------------------------------------
- // 2) Fetch user’s Career Profiles => set initial scenario
- // --------------------------------------------------
+ // 2) load local JSON => masterCareerRatings
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);
+ 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(() => {
+ const fetchProfiles = async () => {
+ const r = await authFetch(`${apiURL}/premium/career-profile/all`);
+ if (!r || !r.ok) return;
+ const d = await r.json();
+ setExistingCareerProfiles(d.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);
+ const stored = localStorage.getItem('lastSelectedCareerProfileId');
+ if (stored) {
+ const match = d.careerProfiles.find((p) => p.id === stored);
+ if (match) {
+ setSelectedCareer(match);
+ setCareerProfileId(stored);
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);
+ // 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);
}
}
}
};
-
- fetchCareerProfiles();
+ fetchProfiles();
}, [apiURL, location.state]);
- // --------------------------------------------------
- // 3) Fetch scenarioRow + collegeProfile for chosen careerProfileId
- // --------------------------------------------------
+ // 4) scenarioRow + college
useEffect(() => {
if (!careerProfileId) {
setScenarioRow(null);
@@ -170,239 +355,214 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
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 s = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`);
+ if (s.ok) setScenarioRow(await s.json());
};
-
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);
- }
+ const c = await authFetch(`${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`);
+ if (c.ok) setCollegeProfile(await c.json());
};
-
fetchScenario();
fetchCollege();
}, [careerProfileId, apiURL]);
- // --------------------------------------------------
- // 4) Fetch Salary Data for selectedCareer + userLocation
- // --------------------------------------------------
+ // 5) from scenarioRow.career_name => find the full SOC => strip
useEffect(() => {
- if (!selectedCareer?.soc_code) {
+ 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;
}
-
- const areaParam = userLocation || 'U.S.';
-
- const fetchSalaryData = async () => {
+ (async () => {
try {
- const queryParams = new URLSearchParams({
- socCode: selectedCareer.soc_code,
- area: areaParam
+ const qs = new URLSearchParams({
+ socCode: strippedSocCode,
+ area: userArea
}).toString();
-
- const res = await fetch(`/api/salary?${queryParams}`);
- if (!res.ok) {
- console.error('Error fetching salary data:', res.status);
+ const url = `${apiURL}/salary?${qs}`;
+ console.log('[Salary fetch =>]', url);
+ const r = await fetch(url);
+ if (!r.ok) {
+ console.error('[Salary fetch non-200 =>]', r.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);
+ const dd = await r.json();
+ console.log('[Salary success =>]', dd);
+ setSalaryData(dd);
} catch (err) {
- console.error('Exception fetching salary data:', err);
+ console.error('[Salary fetch error]', err);
setSalaryData(null);
}
- };
+ })();
+ }, [strippedSocCode, userArea, apiURL]);
- fetchSalaryData();
- }, [selectedCareer, userLocation]);
-
- // --------------------------------------------------
- // 5) (Optional) Fetch Economic Projections
- // --------------------------------------------------
+ // 7) Econ
useEffect(() => {
- if (!selectedCareer?.career_name) {
+ if (!strippedSocCode || !userState) {
setEconomicProjections(null);
return;
}
-
- const fetchEconomicProjections = async () => {
+ (async () => {
+ const qs = new URLSearchParams({ state: userState }).toString();
+ const econUrl = `${apiURL}/projections/${strippedSocCode}?${qs}`;
+ console.log('[Econ fetch =>]', econUrl);
try {
- const encodedCareer = encodeURIComponent(selectedCareer.career_name);
- const res = await authFetch('/api/projections/:socCode');
- if (res.ok) {
- const data = await res.json();
- setEconomicProjections(data);
+ const r = await authFetch(econUrl);
+ if (!r.ok) {
+ console.error('[Econ fetch non-200 =>]', r.status);
+ setEconomicProjections(null);
+ return;
}
+ const econData = await r.json();
+ console.log('[Econ success =>]', econData);
+ setEconomicProjections(econData);
} catch (err) {
- console.error('Error fetching economic projections:', err);
+ console.error('[Econ fetch error]', err);
setEconomicProjections(null);
}
- };
+ })();
+ }, [strippedSocCode, userState, apiURL]);
- fetchEconomicProjections();
- }, [selectedCareer, apiURL]);
-
-
- // --------------------------------------------------
- // 6) Once we have scenario + financial + college => run simulation
- // --------------------------------------------------
- useEffect(() => {
- if (!financialProfile || !scenarioRow || !collegeProfile) return;
-
- (async () => {
+ // 8) Build financial projection
+ const buildProjection = 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);
+ 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 milestonesData = await milRes.json();
- const allMilestones = milestonesData.milestones || [];
+ const md = await mr.json();
+ const allMilestones = md.milestones || [];
setScenarioMilestones(allMilestones);
- // 2) Fetch impacts for each milestone
- const impactPromises = allMilestones.map((m) =>
+ function parseScenarioOverride(overrideVal, fallbackVal) {
+ // If the DB field is NULL => means user never entered anything
+ if (overrideVal === null) {
+ return fallbackVal;
+ }
+ // Otherwise user typed a number, even if it's "0"
+ return parseFloatOrZero(overrideVal, fallbackVal);
+}
+
+
+ const imPromises = 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);
+ .then((dd) => dd?.impacts || [])
+ .catch((e) => {
+ console.warn('Error fetching impacts =>', e);
return [];
})
);
- const impactsForEach = await Promise.all(impactPromises);
+ const impactsForEach = await Promise.all(imPromises);
+ const allImpacts = allMilestones
+ .map((m, i) => ({ ...m, impacts: impactsForEach[i] || [] }))
+ .flatMap((m) => m.impacts);
- // Flatten all milestone impacts
- const allImpacts = allMilestones.map((m, i) => ({
- ...m,
- impacts: impactsForEach[i] || [],
- })).flatMap((m) => m.impacts);
-
- /*******************************************************
- * A) Parse numeric "financialProfile" fields
- *******************************************************/
+ const f = financialProfile;
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),
+ 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)
};
- /*******************************************************
- * B) Parse scenario overrides from "scenarioRow"
- *******************************************************/
+ const s = 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
- ),
- };
+ 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
+ ),
+ };
- /*******************************************************
- * C) Parse numeric "collegeProfile" fields
- *******************************************************/
+
+ const c = collegeProfile;
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),
+ 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:
- 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),
+ 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(collegeProfile.expected_salary) ||
- parseFloatOrZero(financialProfile.current_salary, 0),
+ parseFloatOrZero(c.expected_salary) || parseFloatOrZero(f.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
+ // college
studentLoanAmount: collegeData.studentLoanAmount,
interestRate: collegeData.interestRate,
loanTerm: collegeData.loanTerm,
@@ -419,17 +579,15 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
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 } =
+ const { projectionData: pData, loanPaidOffMonth } =
simulateFinancialProjection(mergedProfile);
- // 4) Add cumulative net savings
+ // Build "cumulativeNetSavings" ourselves, plus each row has .retirementSavings and .emergencySavings
let cumu = mergedProfile.emergencySavings || 0;
const finalData = pData.map((mo) => {
cumu += mo.netSavings || 0;
@@ -437,41 +595,34 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
});
setProjectionData(finalData);
- setLoanPayoffMonth(payoff);
+ setLoanPayoffMonth(loanPaidOffMonth);
} 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');
+ console.error('Error in scenario simulation =>', err);
}
};
- // Build chart annotations from scenarioMilestones
+ useEffect(() => {
+ if (!financialProfile || !scenarioRow || !collegeProfile) return;
+ buildProjection();
+ }, [financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears]);
+
+ // Handlers
+ const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value);
+ const handleSimulationYearsBlur = () => {
+ if (!simulationYearsInput.trim()) setSimulationYearsInput('20');
+ };
+
+ // -- Annotations --
+ // 1) Milestone lines
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}`;
-
+ 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}`] = {
@@ -489,9 +640,12 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
};
});
- // Loan payoff line
+ // 2) Check if there's ever a positive loan balance
+ const hasStudentLoan = projectionData.some((p) => p.loanBalance > 0);
+
+ // 3) Conditionally add the loan payoff annotation
const annotationConfig = {};
- if (loanPayoffMonth) {
+ if (loanPayoffMonth && hasStudentLoan) {
annotationConfig.loanPaidOffLine = {
type: 'line',
xMin: loanPayoffMonth,
@@ -511,238 +665,234 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
}
};
}
+
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}).
-
-
- );
+ // Build the chart datasets:
+ const emergencyData = {
+ label: 'Emergency Savings',
+ data: projectionData.map((p) => p.emergencySavings),
+ borderColor: 'rgba(255, 159, 64, 1)', // orange
+ 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
+ };
+
+ // The total leftover each month (sum of any net gains so far).
+ 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
+ };
+
+ // We'll insert the Loan Balance dataset only if they actually have a loan
+ 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'
+ }
+ };
+
+ // The final dataset array:
+ // 1) Emergency
+ // 2) Retirement
+ // 3) Loan (conditional)
+ // 4) Total
+ const chartDatasets = [emergencyData, retirementData];
+ if (hasStudentLoan) {
+ // Insert loan after the first two lines, or wherever you prefer
+ chartDatasets.push(loanBalanceData);
+ }
+ chartDatasets.push(totalSavingsData);
+
+ const yearsInCareer = getYearsInCareer(scenarioRow?.start_date);
+
return (
- {/* 1) Career dropdown */}
-
{
- setSelectedCareer(selected);
- setCareerProfileId(selected?.id || null);
- }}
- loading={!existingCareerProfiles.length}
- authFetch={authFetch}
- />
+ Where Am I Now?
- {/* 2) Salary Data Display */}
- {salaryData && (
-
-
Salary Overview
- {/* Regional Salaries */}
- {salaryData.regional && (
-
-
Regional Salaries (Area: {userLocation || 'U.S.'})
-
- 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'}
-
-
-
- )}
- {/* National Salaries */}
- {salaryData.national && (
-
-
National Salaries
-
- 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()}
+ {/* 1) Career */}
+
+
{
+ setSelectedCareer(sel);
+ setCareerProfileId(sel?.id || null);
+ }}
+ loading={!existingCareerProfiles.length}
+ authFetch={authFetch}
+ />
+
+
+ Current Career:{' '}
+ {scenarioRow?.career_name || '(Select a career)'}
+ {yearsInCareer && (
+
+ Time in this career: {yearsInCareer}{' '}
+ {yearsInCareer === '<1' ? 'year' : 'years'}
+
+ )}
+
+
+
+ {/* 2) Salary Benchmarks */}
+
+ {salaryData?.regional && (
+
+
+ Regional Data ({userArea || 'U.S.'})
+
+
+ 10th percentile:{' '}
+ {salaryData.regional.regional_PCT10
+ ? `$${salaryData.regional.regional_PCT10.toLocaleString()}`
+ : 'N/A'}
+
+
+
+ Median:{' '}
+ {salaryData.regional.regional_MEDIAN
+ ? `$${salaryData.regional.regional_MEDIAN.toLocaleString()}`
+ : 'N/A'}
+
+
+
+ 90th percentile:{' '}
+ {salaryData.regional.regional_PCT90
+ ? `$${salaryData.regional.regional_PCT90.toLocaleString()}`
+ : 'N/A'}
+
+
+
+
+ )}
+
+ {salaryData?.national && (
+
+
National Data
+
+ 10th percentile:{' '}
+ {salaryData.national.national_PCT10
+ ? `$${salaryData.national.national_PCT10.toLocaleString()}`
+ : 'N/A'}
+
+
+
+ Median:{' '}
+ {salaryData.national.national_MEDIAN
+ ? `$${salaryData.national.national_MEDIAN.toLocaleString()}`
+ : 'N/A'}
+
+
+
+ 90th percentile:{' '}
+ {salaryData.national.national_PCT90
+ ? `$${salaryData.national.national_PCT90.toLocaleString()}`
+ : 'N/A'}
+
+
+
+
+ )}
+
+
+ {/* 3) Economic Projections */}
+
+ {economicProjections?.state && (
+
+ )}
+ {economicProjections?.national && (
+
+ )}
+
+ {!economicProjections?.state && !economicProjections?.national && (
+
+
No economic data found.
)}
- {/* 3) Milestone Timeline */}
-
{}}
- />
-
- {/* 4) AI Suggestions Button */}
- {!showAISuggestions && (
-
- )}
-
- {/* 5) AI-Suggested Milestones */}
- {showAISuggestions && (
-
+ Your Milestones
+ {}}
/>
- )}
+
- {/* 6) Financial Projection Chart */}
- {projectionData.length > 0 && (
-
-
Financial Projection
-
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'
+ {/* 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
}
},
- {
- 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()}`
+ scales: {
+ y: {
+ beginAtZero: false,
+ ticks: {
+ callback: (val) => `$${val.toLocaleString()}`
+ }
}
}
- }
- }}
- />
-
- {loanPayoffMonth && (
-
- Loan Paid Off at: {loanPayoffMonth}
+ }}
+ />
+ {loanPayoffMonth && hasStudentLoan && (
+
+ Loan Paid Off at:{' '}
+ {loanPayoffMonth}
)}
-
-
- )}
+ >
+ ) : (
+ No financial projection data found.
+ )}
+
- {/* 7) Simulation length + "Edit" => open ScenarioEditModal */}
-
+ {/* 6) Simulation length + Edit scenario */}
+
{
Edit
-
- {/* 8) Economic Projections Section */}
- {economicProjections && (
-
-
Economic Projections
-
- Growth Outlook: {economicProjections.growthOutlook || 'N/A'}
-
-
- AI Automation Risk: {economicProjections.aiRisk || 'N/A'}
-
- {economicProjections.chatGPTAnalysis && (
-
-
ChatGPT Analysis:
-
{economicProjections.chatGPTAnalysis}
-
- )}
-
- )}
-
- {/* 9) Career Search & Potential new scenario creation */}
-
{
- setPendingCareerForModal(careerObj.title);
- }}
- />
- {pendingCareerForModal && (
-
- )}
-
- {/* 10) Scenario Edit Modal */}
{
@@ -808,8 +919,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
apiURL={apiURL}
authFetch={authFetch}
/>
+
);
-};
-
-export default MilestoneTracker;
+}
diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js
index 0214685..37cb6d1 100644
--- a/src/components/PremiumOnboarding/OnboardingContainer.js
+++ b/src/components/PremiumOnboarding/OnboardingContainer.js
@@ -21,6 +21,16 @@ const OnboardingContainer = () => {
const nextStep = () => setStep(step + 1);
const prevStep = () => setStep(step - 1);
+ function parseFloatOrNull(value) {
+ // If user left it blank ("" or undefined), treat it as NULL.
+ if (value == null || value === '') {
+ return null;
+ }
+ const parsed = parseFloat(value);
+ // If parseFloat can't parse, also return null
+ return isNaN(parsed) ? null : parsed;
+}
+
console.log('Final collegeData in OnboardingContainer:', collegeData);
// Final “all done” submission when user finishes the last step
@@ -29,17 +39,14 @@ const OnboardingContainer = () => {
// Build a scenarioPayload that includes optional planned_* fields:
const scenarioPayload = {
...careerData,
- planned_monthly_expenses: parseFloat(careerData.planned_monthly_expenses) || 0,
- planned_monthly_debt_payments: parseFloat(careerData.planned_monthly_debt_payments) || 0,
- planned_monthly_retirement_contribution:
- parseFloat(careerData.planned_monthly_retirement_contribution) || 0,
- planned_monthly_emergency_contribution:
- parseFloat(careerData.planned_monthly_emergency_contribution) || 0,
- planned_surplus_emergency_pct: parseFloat(careerData.planned_surplus_emergency_pct) || 0,
- planned_surplus_retirement_pct:
- parseFloat(careerData.planned_surplus_retirement_pct) || 0,
- planned_additional_income: parseFloat(careerData.planned_additional_income) || 0,
- };
+ planned_monthly_expenses: parseFloatOrNull(careerData.planned_monthly_expenses),
+ planned_monthly_debt_payments: parseFloatOrNull(careerData.planned_monthly_debt_payments),
+ planned_monthly_retirement_contribution: parseFloatOrNull(careerData.planned_monthly_retirement_contribution),
+ planned_monthly_emergency_contribution: parseFloatOrNull(careerData.planned_monthly_emergency_contribution),
+ planned_surplus_emergency_pct: parseFloatOrNull(careerData.planned_surplus_emergency_pct),
+ planned_surplus_retirement_pct: parseFloatOrNull(careerData.planned_surplus_retirement_pct),
+ planned_additional_income: parseFloatOrNull(careerData.planned_additional_income),
+};
// 1) POST career-profile (scenario)
const careerRes = await authFetch('/api/premium/career-profile', {
@@ -62,21 +69,28 @@ const OnboardingContainer = () => {
});
if (!financialRes.ok) throw new Error('Failed to save financial profile');
- // 3) POST college-profile (now uses career_profile_id)
- const mergedCollege = {
- ...collegeData,
- career_profile_id,
- college_enrollment_status: careerData.college_enrollment_status,
- };
- const collegeRes = await authFetch('/api/premium/college-profile', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(mergedCollege),
- });
- if (!collegeRes.ok) throw new Error('Failed to save college profile');
+ // 3) Only do college-profile if user is "currently_enrolled" or "prospective_student"
+ if (
+ careerData.college_enrollment_status === 'currently_enrolled' ||
+ careerData.college_enrollment_status === 'prospective_student'
+ ) {
+ const mergedCollege = {
+ ...collegeData,
+ career_profile_id,
+ college_enrollment_status: careerData.college_enrollment_status,
+ };
+ const collegeRes = await authFetch('/api/premium/college-profile', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(mergedCollege),
+ });
+ if (!collegeRes.ok) throw new Error('Failed to save college profile');
+ } else {
+ console.log('Skipping college-profile upsert because user is not enrolled/planning.');
+ }
- // All done → navigate away
- navigate('/milestone-tracker');
+ // Done => navigate
+ navigate('/milestone-tracker');
} catch (err) {
console.error(err);
// (optionally show error to user)
diff --git a/src/components/ScenarioEditModal.js b/src/components/ScenarioEditModal.js
index b76c788..972c4c9 100644
--- a/src/components/ScenarioEditModal.js
+++ b/src/components/ScenarioEditModal.js
@@ -606,16 +606,24 @@ export default function ScenarioEditModal({
collegePayload.loan_deferral_until_graduation = 1;
}
- // 3) Upsert college
- const colRes = await authFetch('/api/premium/college-profile', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(collegePayload)
- });
- if (!colRes.ok) {
- const msg2 = await colRes.text();
- throw new Error(`College upsert failed: ${msg2}`);
- }
+ // 3) Upsert or skip
+ if (finalCollegeStatus === 'currently_enrolled' ||
+ finalCollegeStatus === 'prospective_student')
+ {
+ const colRes = await authFetch('/api/premium/college-profile', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(collegePayload),
+ });
+ if (!colRes.ok) {
+ const msg2 = await colRes.text();
+ throw new Error(`College upsert failed: ${msg2}`);
+ }
+ } else {
+ console.log('Skipping college-profile upsert in EditScenarioModal because user not enrolled');
+ // Optionally: if you want to delete an existing college profile:
+ // await authFetch(`/api/premium/college-profile/delete/${updatedScenarioId}`, { method: 'DELETE' });
+ }
// 4) Re-fetch scenario, college, financial => aggregator => simulate
const [scenResp2, colResp2, finResp] = await Promise.all([
diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js
index 01f7bc7..b4fcc58 100644
--- a/src/utils/FinancialProjectionService.js
+++ b/src/utils/FinancialProjectionService.js
@@ -433,6 +433,7 @@ export function simulateFinancialProjection(userProfile) {
loanBalance: +loanBalance.toFixed(2),
loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2),
+ totalSavings: (currentEmergencySavings + currentRetirementSavings).toFixed(2),
fedYTDgross: +fedYTDgross.toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2),
diff --git a/src/utils/fetchCareerEnrichment.js b/src/utils/fetchCareerEnrichment.js
new file mode 100644
index 0000000..5b29cf2
--- /dev/null
+++ b/src/utils/fetchCareerEnrichment.js
@@ -0,0 +1,22 @@
+// utils/fetchCareerEnrichment.js
+
+import axios from 'axios';
+
+export async function fetchCareerEnrichment(apiUrl, socCode, area) {
+ // strippedSoc = remove decimals from e.g. "15-1132.00" => "15-1132"
+ const strippedSoc = socCode.includes('.') ? socCode.split('.')[0] : socCode;
+
+ const [cipData, jobDetailsData, economicData, salaryData] = await Promise.all([
+ axios.get(`${apiUrl}/cip/${socCode}`).catch(() => null),
+ axios.get(`${apiUrl}/onet/career-description/${socCode}`).catch(() => null),
+ axios.get(`${apiUrl}/projections/${strippedSoc}`, { params: { area } }).catch(() => null),
+ axios.get(`${apiUrl}/salary`, { params: { socCode: strippedSoc, area } }).catch(() => null),
+ ]);
+
+ return {
+ cip: cipData?.data || null,
+ jobDetails: jobDetailsData?.data || null,
+ economic: economicData?.data || null,
+ salary: salaryData?.data || null,
+ };
+}