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:
parent
1e9af5a13d
commit
98661b1c5a
@ -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,
|
||||||
|
1122
backend/server3.js
1122
backend/server3.js
File diff suppressed because it is too large
Load Diff
0
schema.sql
Normal file
0
schema.sql
Normal file
28
src/App.js
28
src/App.js
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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');
|
||||||
}}
|
}}
|
||||||
|
@ -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} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Section 2: Actionable Next Steps */}
|
||||||
|
<section className="bg-white shadow rounded-lg p-6">
|
||||||
|
<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>
|
</p>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<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('/resume-optimizer')}>Optimize Resume</Button>
|
||||||
<Button onClick={() => navigate('/milestone-tracker')}>Set Career Milestones</Button>
|
<Button onClick={() => navigate('/milestone-tracker')}>Plan Career & Finances</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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:
|
||||||
|
@ -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 user’s 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 user’s 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,52 +182,109 @@ 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) {
|
||||||
setCollegeProfile(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await colRes.json();
|
const data = await colRes.json();
|
||||||
setCollegeProfile(data);
|
setCollegeProfile(data);
|
||||||
|
} else {
|
||||||
|
setCollegeProfile(null);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchScenario();
|
fetchScenario();
|
||||||
fetchCollege();
|
fetchCollege();
|
||||||
}, [careerPathId, apiURL]);
|
}, [careerProfileId, apiURL]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (careerPathId) {
|
|
||||||
localStorage.setItem('lastSelectedCareerPathId', careerPathId);
|
|
||||||
}
|
|
||||||
}, [careerPathId]);
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// 3) Once scenarioRow + collegeProfile + financialProfile => run simulation
|
// 4) Fetch Salary Data for selectedCareer + userLocation
|
||||||
// + fetch milestones for annotation lines
|
// --------------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedCareer?.soc_code) {
|
||||||
|
setSalaryData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaParam = userLocation || 'U.S.';
|
||||||
|
|
||||||
|
const fetchSalaryData = async () => {
|
||||||
|
try {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
socCode: selectedCareer.soc_code,
|
||||||
|
area: areaParam
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
const res = await fetch(`/api/salary?${queryParams}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Error fetching salary data:', res.status);
|
||||||
|
setSalaryData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) {
|
||||||
|
console.log('No salary data found for these params:', data.error);
|
||||||
|
}
|
||||||
|
setSalaryData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Exception fetching salary data:', err);
|
||||||
|
setSalaryData(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSalaryData();
|
||||||
|
}, [selectedCareer, userLocation]);
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// 5) (Optional) Fetch Economic Projections
|
||||||
|
// --------------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedCareer?.career_name) {
|
||||||
|
setEconomicProjections(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchEconomicProjections = async () => {
|
||||||
|
try {
|
||||||
|
const encodedCareer = encodeURIComponent(selectedCareer.career_name);
|
||||||
|
const res = await authFetch('/api/projections/:socCode');
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setEconomicProjections(data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching economic projections:', err);
|
||||||
|
setEconomicProjections(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEconomicProjections();
|
||||||
|
}, [selectedCareer, apiURL]);
|
||||||
|
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// 6) Once we have scenario + financial + college => run simulation
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!financialProfile || !scenarioRow || !collegeProfile) return;
|
if (!financialProfile || !scenarioRow || !collegeProfile) return;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// fetch milestones for this scenario
|
// 1) Fetch milestones for this scenario
|
||||||
const milRes = await authFetch(
|
const milRes = await authFetch(`${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`);
|
||||||
`${apiURL}/premium/milestones?careerPathId=${careerPathId}`
|
|
||||||
);
|
|
||||||
if (!milRes.ok) {
|
if (!milRes.ok) {
|
||||||
console.error('Failed to fetch milestones for scenario', careerPathId);
|
console.error('Failed to fetch milestones for scenario', careerProfileId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const milestonesData = await milRes.json();
|
const milestonesData = await milRes.json();
|
||||||
const allMilestones = milestonesData.milestones || [];
|
const allMilestones = milestonesData.milestones || [];
|
||||||
setScenarioMilestones(allMilestones);
|
setScenarioMilestones(allMilestones);
|
||||||
|
|
||||||
// fetch impacts for each milestone
|
// 2) Fetch impacts for each milestone
|
||||||
const impactPromises = allMilestones.map((m) =>
|
const impactPromises = allMilestones.map((m) =>
|
||||||
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
||||||
.then((r) => (r.ok ? r.json() : null))
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
@ -196,78 +295,141 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
const impactsForEach = await Promise.all(impactPromises);
|
const impactsForEach = await Promise.all(impactPromises);
|
||||||
const milestonesWithImpacts = allMilestones.map((m, i) => ({
|
|
||||||
|
// Flatten all milestone impacts
|
||||||
|
const allImpacts = allMilestones.map((m, i) => ({
|
||||||
...m,
|
...m,
|
||||||
impacts: impactsForEach[i] || []
|
impacts: impactsForEach[i] || [],
|
||||||
}));
|
})).flatMap((m) => m.impacts);
|
||||||
|
|
||||||
// flatten
|
/*******************************************************
|
||||||
const allImpacts = milestonesWithImpacts.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),
|
||||||
|
};
|
||||||
|
|
||||||
// Build mergedProfile
|
/*******************************************************
|
||||||
const mergedProfile = {
|
* B) Parse scenario overrides from "scenarioRow"
|
||||||
currentSalary: financialProfile.current_salary || 0,
|
*******************************************************/
|
||||||
monthlyExpenses:
|
const scenarioOverrides = {
|
||||||
scenarioRow.planned_monthly_expenses ??
|
monthlyExpenses: parseFloatOrZero(
|
||||||
financialProfile.monthly_expenses ??
|
scenarioRow.planned_monthly_expenses,
|
||||||
0,
|
financialBase.monthlyExpenses
|
||||||
monthlyDebtPayments:
|
),
|
||||||
scenarioRow.planned_monthly_debt_payments ??
|
monthlyDebtPayments: parseFloatOrZero(
|
||||||
financialProfile.monthly_debt_payments ??
|
scenarioRow.planned_monthly_debt_payments,
|
||||||
0,
|
financialBase.monthlyDebtPayments
|
||||||
retirementSavings: financialProfile.retirement_savings ?? 0,
|
),
|
||||||
emergencySavings: financialProfile.emergency_fund ?? 0,
|
monthlyRetirementContribution: parseFloatOrZero(
|
||||||
monthlyRetirementContribution:
|
scenarioRow.planned_monthly_retirement_contribution,
|
||||||
scenarioRow.planned_monthly_retirement_contribution ??
|
financialBase.retirementContribution
|
||||||
financialProfile.retirement_contribution ??
|
),
|
||||||
0,
|
monthlyEmergencyContribution: parseFloatOrZero(
|
||||||
monthlyEmergencyContribution:
|
scenarioRow.planned_monthly_emergency_contribution,
|
||||||
scenarioRow.planned_monthly_emergency_contribution ??
|
financialBase.emergencyContribution
|
||||||
financialProfile.emergency_contribution ??
|
),
|
||||||
0,
|
surplusEmergencyAllocation: parseFloatOrZero(
|
||||||
surplusEmergencyAllocation:
|
scenarioRow.planned_surplus_emergency_pct,
|
||||||
scenarioRow.planned_surplus_emergency_pct ??
|
financialBase.extraCashEmergencyPct
|
||||||
financialProfile.extra_cash_emergency_pct ??
|
),
|
||||||
50,
|
surplusRetirementAllocation: parseFloatOrZero(
|
||||||
surplusRetirementAllocation:
|
scenarioRow.planned_surplus_retirement_pct,
|
||||||
scenarioRow.planned_surplus_retirement_pct ??
|
financialBase.extraCashRetirementPct
|
||||||
financialProfile.extra_cash_retirement_pct ??
|
),
|
||||||
50,
|
additionalIncome: parseFloatOrZero(
|
||||||
additionalIncome:
|
scenarioRow.planned_additional_income,
|
||||||
scenarioRow.planned_additional_income ??
|
financialBase.additionalIncome
|
||||||
financialProfile.additional_income ??
|
),
|
||||||
0,
|
};
|
||||||
|
|
||||||
// college
|
/*******************************************************
|
||||||
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
* C) Parse numeric "collegeProfile" fields
|
||||||
interestRate: collegeProfile.interest_rate || 5,
|
*******************************************************/
|
||||||
loanTerm: collegeProfile.loan_term || 10,
|
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,
|
loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation,
|
||||||
academicCalendar: collegeProfile.academic_calendar || 'monthly',
|
academicCalendar: collegeProfile.academic_calendar || 'monthly',
|
||||||
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
|
annualFinancialAid: parseFloatOrZero(collegeProfile.annual_financial_aid, 0),
|
||||||
calculatedTuition: collegeProfile.tuition || 0,
|
calculatedTuition: parseFloatOrZero(collegeProfile.tuition, 0),
|
||||||
extraPayment: collegeProfile.extra_payment || 0,
|
extraPayment: parseFloatOrZero(collegeProfile.extra_payment, 0),
|
||||||
inCollege:
|
inCollege:
|
||||||
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||||
collegeProfile.college_enrollment_status === 'prospective_student',
|
collegeProfile.college_enrollment_status === 'prospective_student',
|
||||||
gradDate: collegeProfile.expected_graduation || null,
|
gradDate: collegeProfile.expected_graduation || null,
|
||||||
programType: collegeProfile.program_type || null,
|
programType: collegeProfile.program_type || null,
|
||||||
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
|
creditHoursPerYear: parseFloatOrZero(collegeProfile.credit_hours_per_year, 0),
|
||||||
hoursCompleted: collegeProfile.hours_completed || 0,
|
hoursCompleted: parseFloatOrZero(collegeProfile.hours_completed, 0),
|
||||||
programLength: collegeProfile.program_length || 0,
|
programLength: parseFloatOrZero(collegeProfile.program_length, 0),
|
||||||
expectedSalary:
|
expectedSalary:
|
||||||
collegeProfile.expected_salary || financialProfile.current_salary || 0,
|
parseFloatOrZero(collegeProfile.expected_salary) ||
|
||||||
|
parseFloatOrZero(financialProfile.current_salary, 0),
|
||||||
|
};
|
||||||
|
|
||||||
// scenario horizon
|
/*******************************************************
|
||||||
|
* 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(),
|
startDate: new Date().toISOString(),
|
||||||
simulationYears,
|
simulationYears,
|
||||||
|
|
||||||
milestoneImpacts: allImpacts
|
milestoneImpacts: allImpacts
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 3) Run the simulation
|
||||||
const { projectionData: pData, loanPaidOffMonth: payoff } =
|
const { projectionData: pData, loanPaidOffMonth: payoff } =
|
||||||
simulateFinancialProjection(mergedProfile);
|
simulateFinancialProjection(mergedProfile);
|
||||||
|
|
||||||
|
// 4) Add cumulative net savings
|
||||||
let cumu = mergedProfile.emergencySavings || 0;
|
let cumu = mergedProfile.emergencySavings || 0;
|
||||||
const finalData = pData.map((mo) => {
|
const finalData = pData.map((mo) => {
|
||||||
cumu += mo.netSavings || 0;
|
cumu += mo.netSavings || 0;
|
||||||
@ -280,8 +442,18 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
console.error('Error in scenario simulation:', err);
|
console.error('Error in scenario simulation:', err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [financialProfile, scenarioRow, collegeProfile, careerPathId, apiURL, simulationYears]);
|
}, [
|
||||||
|
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 */}
|
||||||
|
{!showAISuggestions && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowAISuggestions(true)}
|
||||||
|
className="bg-green-500 hover:bg-green-600 text-white font-semibold px-4 py-2 rounded"
|
||||||
|
>
|
||||||
|
Show AI Suggestions
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 5) AI-Suggested Milestones */}
|
||||||
|
{showAISuggestions && (
|
||||||
<AISuggestedMilestones
|
<AISuggestedMilestones
|
||||||
career={selectedCareer?.career_name}
|
career={selectedCareer?.career_name}
|
||||||
careerPathId={careerPathId}
|
careerProfileId={careerProfileId}
|
||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
activeView={activeView}
|
activeView="Career"
|
||||||
projectionData={projectionData}
|
projectionData={projectionData}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 4) The main chart with annotation lines */}
|
{/* 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}
|
||||||
|
@ -9,7 +9,7 @@ import { Button } from './ui/button.js';
|
|||||||
* MultiScenarioView
|
* MultiScenarioView
|
||||||
* -----------------
|
* -----------------
|
||||||
* - Loads the user’s global financialProfile
|
* - Loads the user’s 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 user’s single overall financial profile
|
// The user’s 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 scenario’s college_profile => new scenario
|
// 2) Clone the old scenario’s 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 scenario’s college_profile
|
// fetch old scenario’s 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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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([]);
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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`)
|
||||||
]);
|
]);
|
||||||
|
@ -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...
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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');
|
||||||
|
5
src/utils/ParseFloatorZero.js
Normal file
5
src/utils/ParseFloatorZero.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// parseFloatOrZero.js
|
||||||
|
export default function parseFloatOrZero(val, defaultVal = 0) {
|
||||||
|
const num = parseFloat(val);
|
||||||
|
return isNaN(num) ? defaultVal : num;
|
||||||
|
}
|
@ -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,
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user