Fixed MilestoneTracker.js to be the Where Am I now section.

This commit is contained in:
Josh 2025-05-27 14:47:40 +00:00
parent 98661b1c5a
commit 0ea62392dd
6 changed files with 748 additions and 581 deletions

View File

@ -71,14 +71,17 @@ const authenticatePremiumUser = (req, res, next) => {
// GET the latest selected career profile // GET the latest selected career profile
app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => {
try { try {
const [rows] = await pool.query(` const sql = `
SELECT * SELECT
*,
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles FROM career_profiles
WHERE user_id = ? WHERE user_id = ?
ORDER BY start_date DESC ORDER BY start_date DESC
LIMIT 1 LIMIT 1
`, [req.id]); `;
const [rows] = await pool.query(sql, [req.id]);
res.json(rows[0] || {}); res.json(rows[0] || {});
} catch (error) { } catch (error) {
console.error('Error fetching latest career profile:', error); console.error('Error fetching latest career profile:', error);
@ -89,13 +92,16 @@ app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (re
// GET all career profiles for the user // GET all career profiles for the user
app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => {
try { try {
const [rows] = await pool.query(` const sql = `
SELECT * SELECT
*,
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles FROM career_profiles
WHERE user_id = ? WHERE user_id = ?
ORDER BY start_date ASC ORDER BY start_date ASC
`, [req.id]); `;
const [rows] = await pool.query(sql, [req.id]);
res.json({ careerProfiles: rows }); res.json({ careerProfiles: rows });
} catch (error) { } catch (error) {
console.error('Error fetching career profiles:', error); console.error('Error fetching career profiles:', error);
@ -107,13 +113,18 @@ app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req,
app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => {
const { careerProfileId } = req.params; const { careerProfileId } = req.params;
try { try {
const [rows] = await pool.query(` const sql = `
SELECT * SELECT
*,
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles FROM career_profiles
WHERE id = ? WHERE id = ?
AND user_id = ? AND user_id = ?
`, [careerProfileId, req.id]); LIMIT 1
`;
const [rows] = await pool.query(sql, [careerProfileId, req.id]);
if (!rows[0]) { if (!rows[0]) {
return res.status(404).json({ error: 'Career profile not found or not yours.' }); return res.status(404).json({ error: 'Career profile not found or not yours.' });
} }
@ -124,6 +135,7 @@ app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser,
} }
}); });
// POST a new career profile (upsert) // POST a new career profile (upsert)
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const { const {

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,16 @@ const OnboardingContainer = () => {
const nextStep = () => setStep(step + 1); const nextStep = () => setStep(step + 1);
const prevStep = () => setStep(step - 1); const prevStep = () => setStep(step - 1);
function parseFloatOrNull(value) {
// If user left it blank ("" or undefined), treat it as NULL.
if (value == null || value === '') {
return null;
}
const parsed = parseFloat(value);
// If parseFloat can't parse, also return null
return isNaN(parsed) ? null : parsed;
}
console.log('Final collegeData in OnboardingContainer:', collegeData); console.log('Final collegeData in OnboardingContainer:', collegeData);
// Final “all done” submission when user finishes the last step // Final “all done” submission when user finishes the last step
@ -29,17 +39,14 @@ const OnboardingContainer = () => {
// Build a scenarioPayload that includes optional planned_* fields: // Build a scenarioPayload that includes optional planned_* fields:
const scenarioPayload = { const scenarioPayload = {
...careerData, ...careerData,
planned_monthly_expenses: parseFloat(careerData.planned_monthly_expenses) || 0, planned_monthly_expenses: parseFloatOrNull(careerData.planned_monthly_expenses),
planned_monthly_debt_payments: parseFloat(careerData.planned_monthly_debt_payments) || 0, planned_monthly_debt_payments: parseFloatOrNull(careerData.planned_monthly_debt_payments),
planned_monthly_retirement_contribution: planned_monthly_retirement_contribution: parseFloatOrNull(careerData.planned_monthly_retirement_contribution),
parseFloat(careerData.planned_monthly_retirement_contribution) || 0, planned_monthly_emergency_contribution: parseFloatOrNull(careerData.planned_monthly_emergency_contribution),
planned_monthly_emergency_contribution: planned_surplus_emergency_pct: parseFloatOrNull(careerData.planned_surplus_emergency_pct),
parseFloat(careerData.planned_monthly_emergency_contribution) || 0, planned_surplus_retirement_pct: parseFloatOrNull(careerData.planned_surplus_retirement_pct),
planned_surplus_emergency_pct: parseFloat(careerData.planned_surplus_emergency_pct) || 0, planned_additional_income: parseFloatOrNull(careerData.planned_additional_income),
planned_surplus_retirement_pct: };
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)
const careerRes = await authFetch('/api/premium/career-profile', { const careerRes = await authFetch('/api/premium/career-profile', {
@ -62,21 +69,28 @@ const OnboardingContainer = () => {
}); });
if (!financialRes.ok) throw new Error('Failed to save financial profile'); if (!financialRes.ok) throw new Error('Failed to save financial profile');
// 3) POST college-profile (now uses career_profile_id) // 3) Only do college-profile if user is "currently_enrolled" or "prospective_student"
const mergedCollege = { if (
...collegeData, careerData.college_enrollment_status === 'currently_enrolled' ||
career_profile_id, careerData.college_enrollment_status === 'prospective_student'
college_enrollment_status: careerData.college_enrollment_status, ) {
}; const mergedCollege = {
const collegeRes = await authFetch('/api/premium/college-profile', { ...collegeData,
method: 'POST', career_profile_id,
headers: { 'Content-Type': 'application/json' }, college_enrollment_status: careerData.college_enrollment_status,
body: JSON.stringify(mergedCollege), };
}); const collegeRes = await authFetch('/api/premium/college-profile', {
if (!collegeRes.ok) throw new Error('Failed to save college profile'); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mergedCollege),
});
if (!collegeRes.ok) throw new Error('Failed to save college profile');
} else {
console.log('Skipping college-profile upsert because user is not enrolled/planning.');
}
// All done → navigate away // Done => navigate
navigate('/milestone-tracker'); navigate('/milestone-tracker');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
// (optionally show error to user) // (optionally show error to user)

View File

@ -606,16 +606,24 @@ export default function ScenarioEditModal({
collegePayload.loan_deferral_until_graduation = 1; collegePayload.loan_deferral_until_graduation = 1;
} }
// 3) Upsert college // 3) Upsert or skip
const colRes = await authFetch('/api/premium/college-profile', { if (finalCollegeStatus === 'currently_enrolled' ||
method: 'POST', finalCollegeStatus === 'prospective_student')
headers: { 'Content-Type': 'application/json' }, {
body: JSON.stringify(collegePayload) const colRes = await authFetch('/api/premium/college-profile', {
}); method: 'POST',
if (!colRes.ok) { headers: { 'Content-Type': 'application/json' },
const msg2 = await colRes.text(); body: JSON.stringify(collegePayload),
throw new Error(`College upsert failed: ${msg2}`); });
} if (!colRes.ok) {
const msg2 = await colRes.text();
throw new Error(`College upsert failed: ${msg2}`);
}
} else {
console.log('Skipping college-profile upsert in EditScenarioModal because user not enrolled');
// Optionally: if you want to delete an existing college profile:
// await authFetch(`/api/premium/college-profile/delete/${updatedScenarioId}`, { method: 'DELETE' });
}
// 4) Re-fetch scenario, college, financial => aggregator => simulate // 4) Re-fetch scenario, college, financial => aggregator => simulate
const [scenResp2, colResp2, finResp] = await Promise.all([ const [scenResp2, colResp2, finResp] = await Promise.all([

View File

@ -433,6 +433,7 @@ export function simulateFinancialProjection(userProfile) {
loanBalance: +loanBalance.toFixed(2), loanBalance: +loanBalance.toFixed(2),
loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2), loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2),
totalSavings: (currentEmergencySavings + currentRetirementSavings).toFixed(2),
fedYTDgross: +fedYTDgross.toFixed(2), fedYTDgross: +fedYTDgross.toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2), fedYTDtax: +fedYTDtax.toFixed(2),

View File

@ -0,0 +1,22 @@
// utils/fetchCareerEnrichment.js
import axios from 'axios';
export async function fetchCareerEnrichment(apiUrl, socCode, area) {
// strippedSoc = remove decimals from e.g. "15-1132.00" => "15-1132"
const strippedSoc = socCode.includes('.') ? socCode.split('.')[0] : socCode;
const [cipData, jobDetailsData, economicData, salaryData] = await Promise.all([
axios.get(`${apiUrl}/cip/${socCode}`).catch(() => null),
axios.get(`${apiUrl}/onet/career-description/${socCode}`).catch(() => null),
axios.get(`${apiUrl}/projections/${strippedSoc}`, { params: { area } }).catch(() => null),
axios.get(`${apiUrl}/salary`, { params: { socCode: strippedSoc, area } }).catch(() => null),
]);
return {
cip: cipData?.data || null,
jobDetails: jobDetailsData?.data || null,
economic: economicData?.data || null,
salary: salaryData?.data || null,
};
}