Migration of server3 to MySQL, changed table name to career_profiles, adjusted all corresponding components of career_path variable changes to career_profile, assigned forced numeric handling of table entities in the simulator

This commit is contained in:
Josh 2025-05-26 14:50:22 +00:00
parent 1e9af5a13d
commit 98661b1c5a
24 changed files with 1246 additions and 1004 deletions

View File

@ -180,7 +180,7 @@ app.post('/api/register', async (req, res) => {
// 2) Insert into user_auth, referencing user_profile.id // 2) Insert into user_auth, referencing user_profile.id
const authQuery = ` const authQuery = `
INSERT INTO user_auth (user_id, username, hashed_password) INSERT INTO user_auth (id, username, hashed_password)
VALUES (?, ?, ?) VALUES (?, ?, ?)
`; `;
pool.query( pool.query(
@ -229,7 +229,7 @@ app.post('/api/signin', (req, res) => {
const query = ` const query = `
SELECT SELECT
user_auth.user_id, user_auth.id,
user_auth.hashed_password, user_auth.hashed_password,
user_profile.firstname, user_profile.firstname,
user_profile.lastname, user_profile.lastname,
@ -243,7 +243,7 @@ app.post('/api/signin', (req, res) => {
user_profile.career_priorities, user_profile.career_priorities,
user_profile.career_list user_profile.career_list
FROM user_auth FROM user_auth
LEFT JOIN user_profile ON user_auth.user_id = user_profile.id LEFT JOIN user_profile ON user_auth.id = user_profile.id
WHERE user_auth.username = ? WHERE user_auth.username = ?
`; `;
pool.query(query, [username], async (err, results) => { pool.query(query, [username], async (err, results) => {
@ -265,8 +265,8 @@ app.post('/api/signin', (req, res) => {
return res.status(401).json({ error: 'Invalid username or password' }); return res.status(401).json({ error: 'Invalid username or password' });
} }
// The user_profile id is stored in user_auth.user_id // The user_profile id is stored in user_auth.id
const token = jwt.sign({ id: row.user_id }, SECRET_KEY, { const token = jwt.sign({ id: row.id }, SECRET_KEY, {
expiresIn: '2h', expiresIn: '2h',
}); });
@ -274,7 +274,7 @@ app.post('/api/signin', (req, res) => {
res.status(200).json({ res.status(200).json({
message: 'Login successful', message: 'Login successful',
token, token,
userId: row.user_id, // The user_profile.id id: row.id, // The user_profile.id
user: { user: {
firstname: row.firstname, firstname: row.firstname,
lastname: row.lastname, lastname: row.lastname,

File diff suppressed because it is too large Load Diff

0
schema.sql Normal file
View File

View File

@ -131,6 +131,7 @@ function App() {
text-xs sm:text-sm md:text-base text-xs sm:text-sm md:text-base
font-semibold font-semibold
`} `}
onClick={() => navigate('/planning')}
> >
Find Your Career Find Your Career
</Button> </Button>
@ -163,16 +164,12 @@ function App() {
text-xs sm:text-sm md:text-base text-xs sm:text-sm md:text-base
font-semibold font-semibold
`} `}
onClick={() => navigate('/preparing')}
> >
Prepare for Your Career Prepare for Your Career
</Button> </Button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50"> <div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
<Link {/* Only Educational Programs as submenu */}
to="/preparing"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Preparing Landing
</Link>
<Link <Link
to="/educational-programs" to="/educational-programs"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700" className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
@ -195,6 +192,7 @@ function App() {
text-xs sm:text-sm md:text-base text-xs sm:text-sm md:text-base
font-semibold font-semibold
`} `}
onClick={() => navigate('/enhancing')}
> >
Enhancing Your Career Enhancing Your Career
{!canAccessPremium && ( {!canAccessPremium && (
@ -202,7 +200,13 @@ function App() {
)} )}
</Button> </Button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50"> <div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
{/* Add your premium sub-links here */} <Link
to="/resume-optimizer"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Resume Optimizer
</Link>
{/* Add more enhancing submenu items here if needed */}
</div> </div>
</div> </div>
@ -219,6 +223,7 @@ function App() {
text-xs sm:text-sm md:text-base text-xs sm:text-sm md:text-base
font-semibold font-semibold
`} `}
onClick={() => navigate('/retirement')}
> >
Retirement Planning Retirement Planning
{!canAccessPremium && ( {!canAccessPremium && (
@ -226,7 +231,14 @@ function App() {
)} )}
</Button> </Button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50"> <div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
{/* Add your premium sub-links here */} {/* Example retirement submenu item */}
{/* <Link
to="/retirement/financial-tools"
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
>
Financial Tools
</Link> */}
{/* Add more retirement submenu items here if needed */}
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView, projectionData }) => { const AISuggestedMilestones = ({ id, career, careerProfileId, authFetch, activeView, projectionData }) => {
const [suggestedMilestones, setSuggestedMilestones] = useState([]); const [suggestedMilestones, setSuggestedMilestones] = useState([]);
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -9,7 +9,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
useEffect(() => { useEffect(() => {
const fetchAISuggestions = async () => { const fetchAISuggestions = async () => {
if (!career || !careerPathId || !Array.isArray(projectionData) || projectionData.length === 0) { if (!career || !careerProfileId || !Array.isArray(projectionData) || projectionData.length === 0) {
console.warn('Holding fetch, required data not yet available.'); console.warn('Holding fetch, required data not yet available.');
setAiLoading(true); setAiLoading(true);
return; return;
@ -17,12 +17,12 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
setAiLoading(true); setAiLoading(true);
try { try {
const milestonesRes = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); const milestonesRes = await authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`);
const { milestones } = await milestonesRes.json(); const { milestones } = await milestonesRes.json();
const response = await authFetch('/api/premium/milestone/ai-suggestions', { const response = await authFetch('/api/premium/milestone/ai-suggestions', {
method: 'POST', method: 'POST',
body: JSON.stringify({ career, careerPathId, projectionData, existingMilestones: milestones }), body: JSON.stringify({ career, careerProfileId, projectionData, existingMilestones: milestones }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}); });
@ -43,12 +43,12 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
}; };
fetchAISuggestions(); fetchAISuggestions();
}, [career, careerPathId, projectionData, authFetch]); }, [career, careerProfileId, projectionData, authFetch]);
const regenerateSuggestions = async () => { const regenerateSuggestions = async () => {
setAiLoading(true); setAiLoading(true);
try { try {
const milestonesRes = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); const milestonesRes = await authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`);
const { milestones } = await milestonesRes.json(); const { milestones } = await milestonesRes.json();
const previouslySuggestedMilestones = suggestedMilestones; const previouslySuggestedMilestones = suggestedMilestones;
@ -63,7 +63,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
career, career,
careerPathId, careerProfileId,
projectionData: sampledProjectionData, projectionData: sampledProjectionData,
existingMilestones: milestones, existingMilestones: milestones,
previouslySuggestedMilestones, previouslySuggestedMilestones,
@ -107,7 +107,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
date: m.date, date: m.date,
progress: m.progress, progress: m.progress,
milestone_type: activeView || 'Career', milestone_type: activeView || 'Career',
career_path_id: careerPathId career_profile_id: careerProfileId
}; };
}); });

View File

@ -1,9 +1,9 @@
// src/components/CareerSelectDropdown.js // src/components/CareerSelectDropdown.js
import React from 'react'; import React from 'react';
const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => { const CareerSelectDropdown = ({ existingCareerProfiles, selectedCareer, onChange, loading, authFetch }) => {
const fetchMilestones = (careerPathId) => { const fetchMilestones = (careerProfileId) => {
authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`) authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
console.log('Milestones:', data); console.log('Milestones:', data);
@ -34,12 +34,12 @@ const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, l
value={selectedCareer?.id || ''} value={selectedCareer?.id || ''}
onChange={(e) => { onChange={(e) => {
const selectedId = e.target.value; const selectedId = e.target.value;
const selected = existingCareerPaths.find(path => path.id === selectedId); const selected = existingCareerProfiles.find(path => path.id === selectedId);
handleChange(selected); // ✅ Pass the full object handleChange(selected); // ✅ Pass the full object
}} }}
> >
<option value="" disabled>Select career path...</option> <option value="" disabled>Select career path...</option>
{existingCareerPaths.map((path) => ( {existingCareerProfiles.map((path) => (
<option key={path.id} value={path.id}> <option key={path.id} value={path.id}>
{path.career_name} {path.career_name}
</option> </option>

View File

@ -624,7 +624,7 @@ function Dashboard() {
className="confirm-btn" className="confirm-btn"
onClick={() => { onClick={() => {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('UserId'); localStorage.removeItem('id');
setShowSessionExpiredModal(false); setShowSessionExpiredModal(false);
navigate('/signin'); navigate('/signin');
}} }}

View File

@ -2,22 +2,45 @@ import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
function EnhancingLanding() { import EconomicProjections from './EconomicProjections.js';
function EnhancingLanding({ userProfile }) {
const navigate = useNavigate(); const navigate = useNavigate();
const socCode = userProfile?.socCode;
const stateName = userProfile?.state;
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6"> <div className="min-h-screen bg-gray-50 py-8 px-4">
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8"> <div className="max-w-4xl mx-auto space-y-10">
<h1 className="text-3xl font-bold mb-4 text-center">
Enhancing Your Career {/* Section 1: Current Career Status */}
</h1> <section className="bg-white shadow rounded-lg p-6">
<p className="text-gray-600 mb-6 text-center"> <h2 className="text-2xl font-semibold mb-4">📌 Where Am I Now?</h2>
AptivaAI helps you advance your career. Plan career milestones, enhance your skill set, optimize your resume, and prepare for promotions or transitions. <EconomicProjections socCode={socCode} stateName={stateName} />
</p> </section>
<div className="grid grid-cols-1 gap-4">
<Button onClick={() => navigate('/resume-optimizer')}>Optimize Resume</Button> {/* Section 2: Actionable Next Steps */}
<Button onClick={() => navigate('/milestone-tracker')}>Set Career Milestones</Button> <section className="bg-white shadow rounded-lg p-6">
</div> <h2 className="text-2xl font-semibold mb-4">🚩 What's Next For Me?</h2>
<p className="text-gray-600 mb-4">
Identify your next career milestones with AI-driven recommendations and start advancing your career today.
</p>
<Button onClick={() => navigate('/milestone-tracker')}>Go to Milestone Tracker</Button>
</section>
{/* Section 3: Interactive Planning & Resume Optimization */}
<section className="bg-white shadow rounded-lg p-6">
<h2 className="text-2xl font-semibold mb-4">🚀 How Do I Get There?</h2>
<p className="text-gray-600 mb-4">
Use our comprehensive planning tools to visualize career paths, optimize your resume, and explore financial scenarios.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Button onClick={() => navigate('/resume-optimizer')}>Optimize Resume</Button>
<Button onClick={() => navigate('/milestone-tracker')}>Plan Career & Finances</Button>
</div>
</section>
</div> </div>
</div> </div>
); );

View File

@ -4,12 +4,12 @@ import React, { useEffect, useState, useCallback } from 'react';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
/** /**
* Renders a simple vertical list of milestones for the given careerPathId. * Renders a simple vertical list of milestones for the given careerProfileId.
* Also includes Task CRUD (create/edit/delete) for each milestone, * Also includes Task CRUD (create/edit/delete) for each milestone,
* plus a small "copy milestone" wizard, "financial impacts" form, etc. * plus a small "copy milestone" wizard, "financial impacts" form, etc.
*/ */
export default function MilestoneTimeline({ export default function MilestoneTimeline({
careerPathId, careerProfileId,
authFetch, authFetch,
activeView, // 'Career' or 'Financial' activeView, // 'Career' or 'Financial'
setActiveView, // optional, if you need to switch between views setActiveView, // optional, if you need to switch between views
@ -81,9 +81,9 @@ export default function MilestoneTimeline({
// 2) Fetch milestones => store in "milestones[Career]" / "milestones[Financial]" // 2) Fetch milestones => store in "milestones[Career]" / "milestones[Financial]"
// ------------------------------------------------------------------ // ------------------------------------------------------------------
const fetchMilestones = useCallback(async () => { const fetchMilestones = useCallback(async () => {
if (!careerPathId) return; if (!careerProfileId) return;
try { try {
const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); const res = await authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`);
if (!res.ok) { if (!res.ok) {
console.error('Failed to fetch milestones. Status:', res.status); console.error('Failed to fetch milestones. Status:', res.status);
return; return;
@ -107,7 +107,7 @@ export default function MilestoneTimeline({
} catch (err) { } catch (err) {
console.error('Failed to fetch milestones:', err); console.error('Failed to fetch milestones:', err);
} }
}, [careerPathId, authFetch]); }, [careerProfileId, authFetch]);
useEffect(() => { useEffect(() => {
fetchMilestones(); fetchMilestones();
@ -122,7 +122,7 @@ export default function MilestoneTimeline({
const res = await authFetch('/api/premium/career-profile/all'); const res = await authFetch('/api/premium/career-profile/all');
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setScenarios(data.careerPaths || []); setScenarios(data.careerProfiles || []);
} }
} catch (err) { } catch (err) {
console.error('Error loading scenarios for copy wizard:', err); console.error('Error loading scenarios for copy wizard:', err);
@ -185,7 +185,7 @@ export default function MilestoneTimeline({
title: newMilestone.title, title: newMilestone.title,
description: newMilestone.description, description: newMilestone.description,
date: newMilestone.date, date: newMilestone.date,
career_path_id: careerPathId, career_profile_id: careerProfileId,
progress: newMilestone.progress, progress: newMilestone.progress,
status: newMilestone.progress >= 100 ? 'completed' : 'planned', status: newMilestone.progress >= 100 ? 'completed' : 'planned',
new_salary: new_salary:

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { import {
Chart as ChartJS, Chart as ChartJS,
@ -12,18 +12,20 @@ import {
} from 'chart.js'; } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import { Filler } from 'chart.js'; import { Filler } from 'chart.js';
import { Button } from './ui/button.js';
import authFetch from '../utils/authFetch.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 CareerSelectDropdown from './CareerSelectDropdown.js';
import CareerSearch from './CareerSearch.js'; import CareerSearch from './CareerSearch.js';
import MilestoneTimeline from './MilestoneTimeline.js';
import MilestoneTimeline from './MilestoneTimeline.js'; // Key: This handles Milestone & Task CRUD
import AISuggestedMilestones from './AISuggestedMilestones.js'; import AISuggestedMilestones from './AISuggestedMilestones.js';
import ScenarioEditModal from './ScenarioEditModal.js'; import ScenarioEditModal from './ScenarioEditModal.js';
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
import './MilestoneTracker.css'; import './MilestoneTracker.css';
import './MilestoneTimeline.css'; import './MilestoneTimeline.css';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
// Register Chart + annotation plugin // Register Chart + annotation plugin
ChartJS.register( ChartJS.register(
@ -39,100 +41,140 @@ ChartJS.register(
const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const apiURL = process.env.REACT_APP_API_URL; const apiURL = process.env.REACT_APP_API_URL;
// -------------------------------------------------- // --------------------------------------------------
// State // State
// -------------------------------------------------- // --------------------------------------------------
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); // User and Financial Profile Data
const [careerPathId, setCareerPathId] = useState(null); const [userProfile, setUserProfile] = useState(null);
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
const [activeView, setActiveView] = useState('Career');
const [financialProfile, setFinancialProfile] = 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 [scenarioRow, setScenarioRow] = useState(null);
const [collegeProfile, setCollegeProfile] = useState(null); const [collegeProfile, setCollegeProfile] = useState(null);
const [scenarioMilestones, setScenarioMilestones] = useState([]); // for annotation // Milestones & Simulation
const [scenarioMilestones, setScenarioMilestones] = useState([]);
const [projectionData, setProjectionData] = useState([]); const [projectionData, setProjectionData] = useState([]);
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const [simulationYearsInput, setSimulationYearsInput] = useState('20');
const simulationYears = parseInt(simulationYearsInput, 10) || 20; const simulationYears = parseInt(simulationYearsInput, 10) || 20;
// Show/hide scenario edit modal // Salary Data & Economic Projections
const [salaryData, setSalaryData] = useState(null);
const [economicProjections, setEconomicProjections] = useState(null);
// UI Toggles
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
const [showAISuggestions, setShowAISuggestions] = useState(false);
// If coming from location.state
const { const {
projectionData: initialProjectionData = [], projectionData: initialProjectionData = [],
loanPayoffMonth: initialLoanPayoffMonth = null loanPayoffMonth: initialLoanPayoffMonth = null
} = location.state || {}; } = location.state || {};
// -------------------------------------------------- // --------------------------------------------------
// 1) Fetch users scenario list + financialProfile // 1) Fetch User Profile & Financial Profile
// -------------------------------------------------- // --------------------------------------------------
useEffect(() => { useEffect(() => {
const fetchCareerPaths = async () => { 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 users Career Profiles => set initial scenario
// --------------------------------------------------
useEffect(() => {
const fetchCareerProfiles = async () => {
const res = await authFetch(`${apiURL}/premium/career-profile/all`); const res = await authFetch(`${apiURL}/premium/career-profile/all`);
if (!res || !res.ok) return; if (!res || !res.ok) return;
const data = await res.json(); const data = await res.json();
setExistingCareerPaths(data.careerPaths); setExistingCareerProfiles(data.careerProfiles);
// If there's a career in location.state, pick that
const fromPopout = location.state?.selectedCareer; const fromPopout = location.state?.selectedCareer;
if (fromPopout) { if (fromPopout) {
setSelectedCareer(fromPopout); setSelectedCareer(fromPopout);
setCareerPathId(fromPopout.career_path_id); setCareerProfileId(fromPopout.career_profile_id);
} else { } else {
const storedCareerPathId = localStorage.getItem('lastSelectedCareerPathId'); // Else try localStorage
if (storedCareerPathId) { const storedCareerProfileId = localStorage.getItem('lastSelectedCareerProfileId');
const matchingCareer = data.careerPaths.find(p => p.id === storedCareerPathId); if (storedCareerProfileId) {
const matchingCareer = data.careerProfiles.find((p) => p.id === storedCareerProfileId);
if (matchingCareer) { if (matchingCareer) {
setSelectedCareer(matchingCareer); setSelectedCareer(matchingCareer);
setCareerPathId(storedCareerPathId); setCareerProfileId(storedCareerProfileId);
return; return;
} }
} }
// fallback to latest scenario if no stored ID or not found // Fallback to the "latest" scenario
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`); const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
if (latest && latest.ok) { if (latest && latest.ok) {
const latestData = await latest.json(); const latestData = await latest.json();
if (latestData?.id) { if (latestData?.id) {
setSelectedCareer(latestData); setSelectedCareer(latestData);
setCareerPathId(latestData.id); setCareerProfileId(latestData.id);
} }
} }
} }
}; };
const fetchFinancialProfile = async () => { fetchCareerProfiles();
const res = await authFetch(`${apiURL}/premium/financial-profile`);
if (res?.ok) {
const data = await res.json();
setFinancialProfile(data);
}
};
fetchCareerPaths();
fetchFinancialProfile();
}, [apiURL, location.state]); }, [apiURL, location.state]);
// -------------------------------------------------- // --------------------------------------------------
// 2) When careerPathId changes => fetch scenarioRow + collegeProfile // 3) Fetch scenarioRow + collegeProfile for chosen careerProfileId
// -------------------------------------------------- // --------------------------------------------------
useEffect(() => { useEffect(() => {
if (!careerPathId) { if (!careerProfileId) {
setScenarioRow(null); setScenarioRow(null);
setCollegeProfile(null); setCollegeProfile(null);
setScenarioMilestones([]); setScenarioMilestones([]);
return; return;
} }
async function fetchScenario() { localStorage.setItem('lastSelectedCareerProfileId', careerProfileId);
const scenRes = await authFetch(`${apiURL}/premium/career-profile/${careerPathId}`);
const fetchScenario = async () => {
const scenRes = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`);
if (scenRes.ok) { if (scenRes.ok) {
const data = await scenRes.json(); const data = await scenRes.json();
setScenarioRow(data); setScenarioRow(data);
@ -140,148 +182,278 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
console.error('Failed to fetch scenario row:', scenRes.status); console.error('Failed to fetch scenario row:', scenRes.status);
setScenarioRow(null); setScenarioRow(null);
} }
} };
async function fetchCollege() { const fetchCollege = async () => {
const colRes = await authFetch( const colRes = await authFetch(
`${apiURL}/premium/college-profile?careerPathId=${careerPathId}` `${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`
); );
if (!colRes?.ok) { if (colRes.ok) {
const data = await colRes.json();
setCollegeProfile(data);
} else {
setCollegeProfile(null); setCollegeProfile(null);
return;
} }
const data = await colRes.json(); };
setCollegeProfile(data);
}
fetchScenario(); fetchScenario();
fetchCollege(); fetchCollege();
}, [careerPathId, apiURL]); }, [careerProfileId, apiURL]);
// --------------------------------------------------
// 4) Fetch Salary Data for selectedCareer + userLocation
// --------------------------------------------------
useEffect(() => { useEffect(() => {
if (careerPathId) { if (!selectedCareer?.soc_code) {
localStorage.setItem('lastSelectedCareerPathId', careerPathId); setSalaryData(null);
return;
} }
}, [careerPathId]);
// -------------------------------------------------- const areaParam = userLocation || 'U.S.';
// 3) Once scenarioRow + collegeProfile + financialProfile => run simulation
// + fetch milestones for annotation lines
// --------------------------------------------------
useEffect(() => {
if (!financialProfile || !scenarioRow || !collegeProfile) return;
(async () => { const fetchSalaryData = async () => {
try { try {
// fetch milestones for this scenario const queryParams = new URLSearchParams({
const milRes = await authFetch( socCode: selectedCareer.soc_code,
`${apiURL}/premium/milestones?careerPathId=${careerPathId}` area: areaParam
); }).toString();
if (!milRes.ok) {
console.error('Failed to fetch milestones for scenario', careerPathId); const res = await fetch(`/api/salary?${queryParams}`);
if (!res.ok) {
console.error('Error fetching salary data:', res.status);
setSalaryData(null);
return; return;
} }
const milestonesData = await milRes.json();
const allMilestones = milestonesData.milestones || [];
setScenarioMilestones(allMilestones);
// fetch impacts for each milestone const data = await res.json();
const impactPromises = allMilestones.map((m) => if (data.error) {
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) console.log('No salary data found for these params:', data.error);
.then((r) => (r.ok ? r.json() : null)) }
.then((data) => data?.impacts || []) setSalaryData(data);
.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
const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts);
// Build 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) { } catch (err) {
console.error('Error in scenario simulation:', err); console.error('Exception fetching salary data:', err);
setSalaryData(null);
} }
})(); };
}, [financialProfile, scenarioRow, collegeProfile, careerPathId, apiURL, simulationYears]);
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 handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value);
const handleSimulationYearsBlur = () => { const handleSimulationYearsBlur = () => {
if (!simulationYearsInput.trim()) { if (!simulationYearsInput.trim()) {
@ -300,7 +472,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const month = String(d.getUTCMonth() + 1).padStart(2, '0'); const month = String(d.getUTCMonth() + 1).padStart(2, '0');
const short = `${year}-${month}`; const short = `${year}-${month}`;
// check if we have data for that month
if (!projectionData.some((p) => p.month === short)) return; if (!projectionData.some((p) => p.month === short)) return;
milestoneAnnotationLines[`milestone_${m.id}`] = { milestoneAnnotationLines[`milestone_${m.id}`] = {
@ -318,7 +489,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
}; };
}); });
// If we also want a line for payoff: // Loan payoff line
const annotationConfig = {}; const annotationConfig = {};
if (loanPayoffMonth) { if (loanPayoffMonth) {
annotationConfig.loanPaidOffLine = { annotationConfig.loanPaidOffLine = {
@ -342,38 +513,168 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
} }
const allAnnotations = { ...milestoneAnnotationLines, ...annotationConfig }; 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 ( return (
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-6"> <div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-6">
{/* 1) Career dropdown */} {/* 1) Career dropdown */}
<CareerSelectDropdown <CareerSelectDropdown
existingCareerPaths={existingCareerPaths} existingCareerProfiles={existingCareerProfiles}
selectedCareer={selectedCareer} selectedCareer={selectedCareer}
onChange={(selected) => { onChange={(selected) => {
setSelectedCareer(selected); setSelectedCareer(selected);
setCareerPathId(selected?.id || null); setCareerProfileId(selected?.id || null);
}} }}
loading={!existingCareerPaths.length} loading={!existingCareerProfiles.length}
authFetch={authFetch} authFetch={authFetch}
/> />
{/* 2) MilestoneTimeline for Milestone & Task CRUD */} {/* 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 <MilestoneTimeline
careerPathId={careerPathId} careerProfileId={careerProfileId}
authFetch={authFetch} authFetch={authFetch}
activeView="Career" activeView="Career"
onMilestoneUpdated={() => {}} onMilestoneUpdated={() => {}}
/> />
{/* 3) AI-Suggested Milestones */} {/* 4) AI Suggestions Button */}
<AISuggestedMilestones {!showAISuggestions && (
career={selectedCareer?.career_name} <Button
careerPathId={careerPathId} onClick={() => setShowAISuggestions(true)}
authFetch={authFetch} className="bg-green-500 hover:bg-green-600 text-white font-semibold px-4 py-2 rounded"
activeView={activeView} >
projectionData={projectionData} Show AI Suggestions
/> </Button>
)}
{/* 4) The main chart with annotation lines */} {/* 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 && ( {projectionData.length > 0 && (
<div className="bg-white p-4 rounded shadow space-y-4"> <div className="bg-white p-4 rounded shadow space-y-4">
<h3 className="text-lg font-semibold">Financial Projection</h3> <h3 className="text-lg font-semibold">Financial Projection</h3>
@ -440,7 +741,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
</div> </div>
)} )}
{/* 5) Simulation length + "Edit" Button => open ScenarioEditModal */} {/* 7) Simulation length + "Edit" => open ScenarioEditModal */}
<div className="space-x-2"> <div className="space-x-2">
<label className="font-medium">Simulation Length (years):</label> <label className="font-medium">Simulation Length (years):</label>
<input <input
@ -455,30 +756,48 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
</Button> </Button>
</div> </div>
{/* 6) Career Search, scenario edit modal, etc. */} {/* 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 <CareerSearch
onCareerSelected={(careerObj) => { onCareerSelected={(careerObj) => {
setPendingCareerForModal(careerObj.title); setPendingCareerForModal(careerObj.title);
}} }}
/> />
{pendingCareerForModal && ( {pendingCareerForModal && (
<Button <Button
onClick={() => { onClick={() => {
console.log('User confirmed new career path:', pendingCareerForModal); console.log('User confirmed new career path:', pendingCareerForModal);
setPendingCareerForModal(null); setPendingCareerForModal(null);
}} }}
className="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded" className="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded mt-2"
> >
Confirm Career Change to {pendingCareerForModal} Confirm Career Change to {pendingCareerForModal}
</Button> </Button>
)} )}
{/* 10) Scenario Edit Modal */}
<ScenarioEditModal <ScenarioEditModal
show={showEditModal} show={showEditModal}
onClose={() => { onClose={() => {
setShowEditModal(false); setShowEditModal(false);
// optionally reload to see scenario changes
window.location.reload(); window.location.reload();
}} }}
scenario={scenarioRow} scenario={scenarioRow}

View File

@ -9,7 +9,7 @@ import { Button } from './ui/button.js';
* MultiScenarioView * MultiScenarioView
* ----------------- * -----------------
* - Loads the users global financialProfile * - Loads the users global financialProfile
* - Loads all scenarios from `career_paths` * - Loads all scenarios from `career_profiles`
* - Renders a <ScenarioContainer> for each scenario * - Renders a <ScenarioContainer> for each scenario
* - Handles "Add Scenario", "Clone Scenario" (including college_profile), "Remove Scenario" * - Handles "Add Scenario", "Clone Scenario" (including college_profile), "Remove Scenario"
*/ */
@ -20,7 +20,7 @@ export default function MultiScenarioView() {
// The users single overall financial profile // The users single overall financial profile
const [financialProfile, setFinancialProfile] = useState(null); const [financialProfile, setFinancialProfile] = useState(null);
// The list of scenario "headers" (rows from career_paths) // The list of scenario "headers" (rows from career_profiles)
const [scenarios, setScenarios] = useState([]); const [scenarios, setScenarios] = useState([]);
useEffect(() => { useEffect(() => {
@ -46,7 +46,7 @@ export default function MultiScenarioView() {
const scenData = await scenRes.json(); const scenData = await scenRes.json();
setFinancialProfile(finData); setFinancialProfile(finData);
setScenarios(scenData.careerPaths || []); setScenarios(scenData.careerProfiles || []);
} catch (err) { } catch (err) {
console.error('MultiScenarioView load error:', err); console.error('MultiScenarioView load error:', err);
setError(err.message || 'Failed to load multi-scenarios'); setError(err.message || 'Failed to load multi-scenarios');
@ -122,7 +122,7 @@ export default function MultiScenarioView() {
// parse the newly created scenario_id // parse the newly created scenario_id
const newScenarioData = await res.json(); const newScenarioData = await res.json();
const newScenarioId = newScenarioData.career_path_id; const newScenarioId = newScenarioData.career_profile_id;
// 2) Clone the old scenarios college_profile => new scenario // 2) Clone the old scenarios college_profile => new scenario
await cloneCollegeProfile(oldScenario.id, newScenarioId); await cloneCollegeProfile(oldScenario.id, newScenarioId);
@ -141,7 +141,7 @@ export default function MultiScenarioView() {
try { try {
// fetch old scenarios college_profile // fetch old scenarios college_profile
const getRes = await authFetch( const getRes = await authFetch(
`/api/premium/college-profile?careerPathId=${oldScenarioId}` `/api/premium/college-profile?careerProfileId=${oldScenarioId}`
); );
if (!getRes.ok) { if (!getRes.ok) {
console.warn( console.warn(
@ -162,7 +162,7 @@ export default function MultiScenarioView() {
// build new payload // build new payload
const clonePayload = { const clonePayload = {
career_path_id: newScenarioId, career_profile_id: newScenarioId,
selected_school: oldCollegeData.selected_school, selected_school: oldCollegeData.selected_school,
selected_program: oldCollegeData.selected_program, selected_program: oldCollegeData.selected_program,

View File

@ -24,7 +24,7 @@ const Paywall = () => {
}); });
if (response.ok) { if (response.ok) {
navigate('/PremiumOnboarding', { state: { selectedCareer } }); navigate('/premium-onboarding', { state: { selectedCareer } });
} else if (response.status === 401) { } else if (response.status === 401) {
navigate('/GettingStarted', { state: { selectedCareer } }); navigate('/GettingStarted', { state: { selectedCareer } });
} else { } else {

View File

@ -116,7 +116,7 @@ function PopoutPanel({
} }
try { try {
// 1) Fetch existing career profiles (a.k.a. "careerPaths") // 1) Fetch existing career profiles (a.k.a. "careerProfiles")
const allPathsResponse = await fetch( const allPathsResponse = await fetch(
`${process.env.REACT_APP_API_URL}/premium/career-profile/all`, `${process.env.REACT_APP_API_URL}/premium/career-profile/all`,
{ {
@ -132,11 +132,11 @@ function PopoutPanel({
throw new Error(`HTTP error ${allPathsResponse.status}`); throw new Error(`HTTP error ${allPathsResponse.status}`);
} }
// The server returns { careerPaths: [...] } // The server returns { careerProfiles: [...] }
const { careerPaths } = await allPathsResponse.json(); const { careerProfiles } = await allPathsResponse.json();
// 2) Check if there's already a career path with the same name // 2) Check if there's already a career path with the same name
const match = careerPaths.find((cp) => cp.career_name === data.title); const match = careerProfiles.find((cp) => cp.career_name === data.title);
if (match) { if (match) {
// If a path already exists for this career, confirm with the user // If a path already exists for this career, confirm with the user
@ -149,7 +149,7 @@ function PopoutPanel({
navigate("/paywall", { navigate("/paywall", {
state: { state: {
selectedCareer: { selectedCareer: {
career_path_id: match.id, // 'id' is the primary key from the DB career_profile_id: match.id, // 'id' is the primary key from the DB
career_name: data.title, career_name: data.title,
}, },
}, },
@ -179,15 +179,15 @@ function PopoutPanel({
throw new Error("Failed to create new career path."); throw new Error("Failed to create new career path.");
} }
// The server returns something like { message: 'Career profile upserted.', career_path_id: 'xxx-xxx' } // The server returns something like { message: 'Career profile upserted.', career_profile_id: 'xxx-xxx' }
const result = await newResponse.json(); const result = await newResponse.json();
const newlyCreatedId = result?.career_path_id; const newlyCreatedId = result?.career_profile_id;
// 4) Navigate to /paywall, passing the newly created career_path_id // 4) Navigate to /paywall, passing the newly created career_profile_id
navigate("/paywall", { navigate("/paywall", {
state: { state: {
selectedCareer: { selectedCareer: {
career_path_id: newlyCreatedId, career_profile_id: newlyCreatedId,
career_name: data.title, career_name: data.title,
}, },
}, },

View File

@ -4,104 +4,60 @@ import axios from 'axios';
import { Input } from '../ui/input.js'; // Ensure path matches your structure import { Input } from '../ui/input.js'; // Ensure path matches your structure
import authFetch from '../../utils/authFetch.js'; import authFetch from '../../utils/authFetch.js';
// 1) Import your CareerSearch component
import CareerSearch from '../CareerSearch.js'; // adjust path as necessary
const apiURL = process.env.REACT_APP_API_URL; const apiURL = process.env.REACT_APP_API_URL;
const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => { const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
const [userId, setUserId] = useState(null); // We store local state for “are you working,” “selectedCareer,” etc.
const [currentlyWorking, setCurrentlyWorking] = useState(''); const [currentlyWorking, setCurrentlyWorking] = useState('');
const [selectedCareer, setSelectedCareer] = useState(''); const [selectedCareer, setSelectedCareer] = useState('');
const [careerPathId, setCareerPathId] = useState(null);
const [collegeEnrollmentStatus, setCollegeEnrollmentStatus] = useState(''); const [collegeEnrollmentStatus, setCollegeEnrollmentStatus] = useState('');
const [careers, setCareers] = useState([]); // On mount, if data already has these fields, set them
const [searchInput, setSearchInput] = useState('');
useEffect(() => { useEffect(() => {
const storedUserId = localStorage.getItem('userId'); if (data.currently_working) setCurrentlyWorking(data.currently_working);
if (storedUserId) { if (data.career_name) setSelectedCareer(data.career_name);
setUserId(storedUserId); if (data.college_enrollment_status) setCollegeEnrollmentStatus(data.college_enrollment_status);
} else { }, [data]);
console.error('User ID not found in localStorage');
}
}, []);
useEffect(() => {
const fetchCareerTitles = async () => {
try {
const response = await fetch('/career_clusters.json');
const data = await response.json();
const careerTitlesSet = new Set();
const clusters = Object.keys(data);
for (let i = 0; i < clusters.length; i++) {
const cluster = clusters[i];
const subdivisions = Object.keys(data[cluster]);
for (let j = 0; j < subdivisions.length; j++) {
const subdivision = subdivisions[j];
const careersArray = data[cluster][subdivision];
for (let k = 0; k < careersArray.length; k++) {
const careerObj = careersArray[k];
if (careerObj.title) {
careerTitlesSet.add(careerObj.title);
}
}
}
}
setCareers([...careerTitlesSet]);
} catch (error) {
console.error("Error fetching or processing career_clusters.json:", error);
}
};
fetchCareerTitles();
}, []);
useEffect(() => {
if (careers.includes(searchInput)) {
setSelectedCareer(searchInput);
setData(prev => ({ ...prev, career_name: searchInput }));
}
}, [searchInput, careers, setData]);
// Called whenever other <inputs> change
const handleChange = (e) => { const handleChange = (e) => {
setData(prev => ({ ...prev, [e.target.name]: e.target.value })); setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
}; };
const handleCareerInputChange = (e) => { // Called when user picks a career from CareerSearch and confirms it
const inputValue = e.target.value; const handleCareerSelected = (careerObj) => {
setSearchInput(inputValue); // e.g. { title, soc_code, cip_code, ... }
setSelectedCareer(careerObj.title);
if (careers.includes(inputValue)) { setData(prev => ({
setSelectedCareer(inputValue); ...prev,
setData(prev => ({ ...prev, career_name: inputValue })); career_name: careerObj.title,
} soc_code: careerObj.soc_code || '' // store SOC if needed
}));
}; };
const handleSubmit = () => { const handleSubmit = () => {
if (!selectedCareer || !currentlyWorking || !collegeEnrollmentStatus) { if (!selectedCareer || !currentlyWorking || !collegeEnrollmentStatus) {
alert("Please complete all required fields before continuing."); alert('Please complete all required fields before continuing.');
return; return;
} }
const isInCollege = ( const isInCollege =
collegeEnrollmentStatus === 'currently_enrolled' || collegeEnrollmentStatus === 'currently_enrolled' ||
collegeEnrollmentStatus === 'prospective_student' collegeEnrollmentStatus === 'prospective_student';
);
// Merge local state into parent data
setData(prevData => ({ setData(prevData => ({
...prevData, ...prevData,
career_name: selectedCareer, career_name: selectedCareer,
college_enrollment_status: collegeEnrollmentStatus, college_enrollment_status: collegeEnrollmentStatus,
currently_working: currentlyWorking, currently_working: currentlyWorking,
inCollege: isInCollege, inCollege: isInCollege,
// fallback defaults, or use user-provided
status: prevData.status || 'planned', status: prevData.status || 'planned',
start_date: prevData.start_date || new Date().toISOString(), start_date: prevData.start_date || new Date().toISOString().slice(0, 10),
projected_end_date: prevData.projected_end_date || null, projected_end_date: prevData.projected_end_date || null
user_id: userId
})); }));
nextStep(); nextStep();
@ -117,7 +73,10 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
</label> </label>
<select <select
value={currentlyWorking} value={currentlyWorking}
onChange={(e) => setCurrentlyWorking(e.target.value)} onChange={(e) => {
setCurrentlyWorking(e.target.value);
setData(prev => ({ ...prev, currently_working: e.target.value }));
}}
className="w-full border rounded p-2" className="w-full border rounded p-2"
> >
<option value="">Select one</option> <option value="">Select one</option>
@ -126,20 +85,10 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
</select> </select>
</div> </div>
{/* 2) Replace old local “Search for Career” with <CareerSearch/> */}
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-medium">Search for Career</h3> <h3 className="font-medium">Search for Career</h3>
<input <CareerSearch onCareerSelected={handleCareerSelected} />
value={searchInput}
onChange={handleCareerInputChange}
placeholder="Start typing a career..."
list="career-titles"
className="w-full border rounded p-2"
/>
<datalist id="career-titles">
{careers.map((career, index) => (
<option key={index} value={career} />
))}
</datalist>
</div> </div>
{selectedCareer && ( {selectedCareer && (
@ -157,9 +106,9 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
className="w-full border rounded p-2" className="w-full border rounded p-2"
> >
<option value="">Select Status</option> <option value="">Select Status</option>
<option value="Planned">Planned</option> <option value="planned">Planned</option>
<option value="Current">Current</option> <option value="current">Current</option>
<option value="Exploring">Exploring</option> <option value="exploring">Exploring</option>
</select> </select>
</div> </div>
@ -191,13 +140,16 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
</label> </label>
<select <select
value={collegeEnrollmentStatus} value={collegeEnrollmentStatus}
onChange={(e) => setCollegeEnrollmentStatus(e.target.value)} onChange={(e) => {
setCollegeEnrollmentStatus(e.target.value);
setData(prev => ({ ...prev, college_enrollment_status: e.target.value }));
}}
className="w-full border rounded p-2" className="w-full border rounded p-2"
> >
<option value="">Select one</option> <option value="">Select one</option>
<option value="not_enrolled">Not Enrolled / Not Planning</option> <option value="not_enrolled">Not Enrolled / Not Planning</option>
<option value="currently_enrolled">Currently Enrolled</option> <option value="currently_enrolled">Currently Enrolled</option>
<option value="prospective_student">Planning to Enroll (Prospective Student)</option> <option value="prospective_student">Planning to Enroll (Prospective)</option>
</select> </select>
</div> </div>

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import authFetch from '../../utils/authFetch.js'; import authFetch from '../../utils/authFetch.js';
function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId }) { function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId }) {
// CIP / iPEDS local states (purely for CIP data and suggestions) // CIP / iPEDS local states (purely for CIP data and suggestions)
const [schoolData, setSchoolData] = useState([]); const [schoolData, setSchoolData] = useState([]);
const [icTuitionData, setIcTuitionData] = useState([]); const [icTuitionData, setIcTuitionData] = useState([]);

View File

@ -1,13 +1,14 @@
// OnboardingContainer.js // OnboardingContainer.js
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import PremiumWelcome from './PremiumWelcome.js'; import PremiumWelcome from './PremiumWelcome.js';
import CareerOnboarding from './CareerOnboarding.js'; import CareerOnboarding from './CareerOnboarding.js';
import FinancialOnboarding from './FinancialOnboarding.js'; import FinancialOnboarding from './FinancialOnboarding.js';
import CollegeOnboarding from './CollegeOnboarding.js'; import CollegeOnboarding from './CollegeOnboarding.js';
import authFetch from '../../utils/authFetch.js';
import { useNavigate } from 'react-router-dom';
import ReviewPage from './ReviewPage.js'; import ReviewPage from './ReviewPage.js';
import authFetch from '../../utils/authFetch.js';
const OnboardingContainer = () => { const OnboardingContainer = () => {
console.log('OnboardingContainer MOUNT'); console.log('OnboardingContainer MOUNT');
@ -15,28 +16,29 @@ const OnboardingContainer = () => {
const [careerData, setCareerData] = useState({}); const [careerData, setCareerData] = useState({});
const [financialData, setFinancialData] = useState({}); const [financialData, setFinancialData] = useState({});
const [collegeData, setCollegeData] = useState({}); const [collegeData, setCollegeData] = useState({});
const navigate = useNavigate(); const navigate = useNavigate();
const nextStep = () => setStep(step + 1); const nextStep = () => setStep(step + 1);
const prevStep = () => setStep(step - 1); const prevStep = () => setStep(step - 1);
console.log("Final collegeData in OnboardingContainer:", collegeData); console.log('Final collegeData in OnboardingContainer:', collegeData);
// Now we do the final “all done” submission when the user finishes the last step // Final “all done” submission when user finishes the last step
const handleFinalSubmit = async () => { const handleFinalSubmit = async () => {
try { try {
// Build a scenarioPayload that includes the optional planned_* fields. // Build a scenarioPayload that includes optional planned_* fields:
// We parseFloat them to avoid sending strings, and default to 0 if empty.
const scenarioPayload = { const scenarioPayload = {
...careerData, ...careerData,
planned_monthly_expenses: parseFloat(careerData.planned_monthly_expenses) || 0, planned_monthly_expenses: parseFloat(careerData.planned_monthly_expenses) || 0,
planned_monthly_debt_payments: parseFloat(careerData.planned_monthly_debt_payments) || 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_retirement_contribution:
planned_monthly_emergency_contribution: parseFloat(careerData.planned_monthly_emergency_contribution) || 0, 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_emergency_pct: parseFloat(careerData.planned_surplus_emergency_pct) || 0,
planned_surplus_retirement_pct: parseFloat(careerData.planned_surplus_retirement_pct) || 0, planned_surplus_retirement_pct:
planned_additional_income: parseFloat(careerData.planned_additional_income) || 0 parseFloat(careerData.planned_surplus_retirement_pct) || 0,
planned_additional_income: parseFloat(careerData.planned_additional_income) || 0,
}; };
// 1) POST career-profile (scenario) // 1) POST career-profile (scenario)
@ -47,8 +49,10 @@ const OnboardingContainer = () => {
}); });
if (!careerRes.ok) throw new Error('Failed to save career profile'); if (!careerRes.ok) throw new Error('Failed to save career profile');
const careerJson = await careerRes.json(); const careerJson = await careerRes.json();
const { career_path_id } = careerJson; const { career_profile_id } = careerJson; // <-- Renamed from career_profile_id
if (!career_path_id) throw new Error('No career_path_id returned by server'); if (!career_profile_id) {
throw new Error('No career_profile_id returned by server');
}
// 2) POST financial-profile // 2) POST financial-profile
const financialRes = await authFetch('/api/premium/financial-profile', { const financialRes = await authFetch('/api/premium/financial-profile', {
@ -58,11 +62,11 @@ const OnboardingContainer = () => {
}); });
if (!financialRes.ok) throw new Error('Failed to save financial profile'); if (!financialRes.ok) throw new Error('Failed to save financial profile');
// 3) POST college-profile (include career_path_id) // 3) POST college-profile (now uses career_profile_id)
const mergedCollege = { const mergedCollege = {
...collegeData, ...collegeData,
career_path_id, career_profile_id,
college_enrollment_status: careerData.college_enrollment_status college_enrollment_status: careerData.college_enrollment_status,
}; };
const collegeRes = await authFetch('/api/premium/college-profile', { const collegeRes = await authFetch('/api/premium/college-profile', {
method: 'POST', method: 'POST',
@ -71,7 +75,7 @@ const OnboardingContainer = () => {
}); });
if (!collegeRes.ok) throw new Error('Failed to save college profile'); if (!collegeRes.ok) throw new Error('Failed to save college profile');
// Done => navigate away // All done → navigate away
navigate('/milestone-tracker'); navigate('/milestone-tracker');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -79,7 +83,6 @@ const OnboardingContainer = () => {
} }
}; };
const onboardingSteps = [ const onboardingSteps = [
<PremiumWelcome nextStep={nextStep} />, <PremiumWelcome nextStep={nextStep} />,
@ -104,12 +107,12 @@ const OnboardingContainer = () => {
nextStep={nextStep} nextStep={nextStep}
data={{ data={{
...collegeData, ...collegeData,
// ensure we keep the enrollment status from career if that matters: // keep enrollment status from careerData if relevant:
college_enrollment_status: careerData.college_enrollment_status college_enrollment_status: careerData.college_enrollment_status,
}} }}
setData={setCollegeData} setData={setCollegeData}
/>, />,
// Add a final "Review & Submit" step or just automatically call handleFinalSubmit on step 4
<ReviewPage <ReviewPage
careerData={careerData} careerData={careerData}
financialData={financialData} financialData={financialData}

View File

@ -33,7 +33,7 @@ export default function ScenarioContainer({
throw new Error(`Failed fetching scenario list: ${res.status}`); throw new Error(`Failed fetching scenario list: ${res.status}`);
} }
const data = await res.json(); const data = await res.json();
setAllScenarios(data.careerPaths || []); setAllScenarios(data.careerProfiles || []);
} catch (err) { } catch (err) {
console.error('Error loading allScenarios for dropdown:', err); console.error('Error loading allScenarios for dropdown:', err);
} }
@ -72,7 +72,7 @@ export default function ScenarioContainer({
} }
async function loadCollegeProfile() { async function loadCollegeProfile() {
try { try {
const url = `/api/premium/college-profile?careerPathId=${localScenario.id}`; const url = `/api/premium/college-profile?careerProfileId=${localScenario.id}`;
const res = await authFetch(url); const res = await authFetch(url);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
@ -97,7 +97,7 @@ export default function ScenarioContainer({
} }
try { try {
const res = await authFetch( const res = await authFetch(
`/api/premium/milestones?careerPathId=${localScenario.id}` `/api/premium/milestones?careerProfileId=${localScenario.id}`
); );
if (!res.ok) { if (!res.ok) {
console.error('Failed fetching milestones. Status:', res.status); console.error('Failed fetching milestones. Status:', res.status);
@ -446,7 +446,7 @@ export default function ScenarioContainer({
title: newMilestone.title, title: newMilestone.title,
description: newMilestone.description, description: newMilestone.description,
date: newMilestone.date, date: newMilestone.date,
career_path_id: localScenario.id, career_profile_id: localScenario.id,
progress: newMilestone.progress, progress: newMilestone.progress,
status: newMilestone.progress >= 100 ? 'completed' : 'planned', status: newMilestone.progress >= 100 ? 'completed' : 'planned',
new_salary: newMilestone.newSalary new_salary: newMilestone.newSalary
@ -791,7 +791,7 @@ export default function ScenarioContainer({
career={ career={
localScenario.career_name || localScenario.scenario_title || '' localScenario.career_name || localScenario.scenario_title || ''
} }
careerPathId={localScenario.id} careerProfileId={localScenario.id}
authFetch={authFetch} authFetch={authFetch}
activeView="Financial" activeView="Financial"
projectionData={projectionData} projectionData={projectionData}

View File

@ -533,11 +533,11 @@ export default function ScenarioEditModal({
throw new Error(`Scenario upsert failed: ${msg}`); throw new Error(`Scenario upsert failed: ${msg}`);
} }
const scenData = await scenRes.json(); const scenData = await scenRes.json();
const updatedScenarioId = scenData.career_path_id; const updatedScenarioId = scenData.career_profile_id;
// 2) Build college payload // 2) Build college payload
const collegePayload = { const collegePayload = {
career_path_id: updatedScenarioId, career_profile_id: updatedScenarioId,
college_enrollment_status: finalCollegeStatus, college_enrollment_status: finalCollegeStatus,
is_in_state: formData.is_in_state ? 1 : 0, is_in_state: formData.is_in_state ? 1 : 0,
is_in_district: formData.is_in_district ? 1 : 0, is_in_district: formData.is_in_district ? 1 : 0,
@ -621,7 +621,7 @@ export default function ScenarioEditModal({
const [scenResp2, colResp2, finResp] = await Promise.all([ const [scenResp2, colResp2, finResp] = await Promise.all([
authFetch(`/api/premium/career-profile/${updatedScenarioId}`), authFetch(`/api/premium/career-profile/${updatedScenarioId}`),
authFetch( authFetch(
`/api/premium/college-profile?careerPathId=${updatedScenarioId}` `/api/premium/college-profile?careerProfileId=${updatedScenarioId}`
), ),
authFetch(`/api/premium/financial-profile`) authFetch(`/api/premium/financial-profile`)
]); ]);

View File

@ -32,7 +32,7 @@ export default function ScenarioEditWizard({
const [scenRes, finRes, colRes] = await Promise.all([ const [scenRes, finRes, colRes] = await Promise.all([
authFetch(`/api/premium/career-profile/${scenarioId}`), authFetch(`/api/premium/career-profile/${scenarioId}`),
authFetch(`/api/premium/financial-profile`), authFetch(`/api/premium/financial-profile`),
authFetch(`/api/premium/college-profile?careerPathId=${scenarioId}`) authFetch(`/api/premium/college-profile?careerProfileId=${scenarioId}`)
]); ]);
if (!scenRes.ok || !finRes.ok || !colRes.ok) { if (!scenRes.ok || !finRes.ok || !colRes.ok) {
throw new Error('Failed fetching existing scenario or financial or college.'); throw new Error('Failed fetching existing scenario or financial or college.');
@ -59,7 +59,7 @@ export default function ScenarioEditWizard({
planned_surplus_emergency_pct: scenData.planned_surplus_emergency_pct, planned_surplus_emergency_pct: scenData.planned_surplus_emergency_pct,
planned_surplus_retirement_pct: scenData.planned_surplus_retirement_pct, planned_surplus_retirement_pct: scenData.planned_surplus_retirement_pct,
planned_additional_income: scenData.planned_additional_income, planned_additional_income: scenData.planned_additional_income,
user_id: scenData.user_id, id: scenData.id,
// etc... // etc...
}); });

View File

@ -33,11 +33,11 @@ function SignIn({ setIsAuthenticated, setUser }) {
const data = await response.json(); const data = await response.json();
// Destructure user, which includes is_premium, etc. // Destructure user, which includes is_premium, etc.
const { token, userId, user } = data; const { token, id, user } = data;
// Store token & userId in localStorage // Store token & id in localStorage
localStorage.setItem('token', token); localStorage.setItem('token', token);
localStorage.setItem('userId', userId); localStorage.setItem('id', id);
// Mark user as authenticated // Mark user as authenticated
setIsAuthenticated(true); setIsAuthenticated(true);

View File

@ -417,6 +417,7 @@ export function simulateFinancialProjection(userProfile) {
netMonthlyIncome: +netMonthlyIncome.toFixed(2), netMonthlyIncome: +netMonthlyIncome.toFixed(2),
totalExpenses: +actualExpensesPaid.toFixed(2), totalExpenses: +actualExpensesPaid.toFixed(2),
effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2), effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2),
effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2), effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2),
@ -442,6 +443,7 @@ export function simulateFinancialProjection(userProfile) {
wasInDeferral = (stillInCollege && loanDeferralUntilGraduation); wasInDeferral = (stillInCollege && loanDeferralUntilGraduation);
} }
// final loanPaidOffMonth if never set // final loanPaidOffMonth if never set
if (loanBalance <= 0 && !loanPaidOffMonth) { if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = scenarioStartClamped.clone().add(maxMonths, 'months').format('YYYY-MM'); loanPaidOffMonth = scenarioStartClamped.clone().add(maxMonths, 'months').format('YYYY-MM');

View File

@ -0,0 +1,5 @@
// parseFloatOrZero.js
export default function parseFloatOrZero(val, defaultVal = 0) {
const num = parseFloat(val);
return isNaN(num) ? defaultVal : num;
}

View File

@ -14,7 +14,7 @@ export function buildMilestonePromptData({
isCollegeMode isCollegeMode
}) { }) {
return { return {
careerPath: { careerProfile: {
name: selectedCareer?.career_name, name: selectedCareer?.career_name,
socCode: selectedCareer?.soc_code, socCode: selectedCareer?.soc_code,
cluster: careerCluster, cluster: careerCluster,

Binary file not shown.