Fixed UUID issues between onboarding and simulator, new issues with college-profile saving from onboarding.

This commit is contained in:
Josh 2025-06-03 17:58:59 +00:00
parent 287737fa8b
commit f9177fbf38
7 changed files with 256 additions and 86 deletions

View File

@ -165,7 +165,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
}
try {
const newId = uuidv4();
const finalId = req.body.id || uuidv4();
// 1) Insert includes career_goals
const sql = `
@ -207,7 +207,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
`;
await pool.query(sql, [
newId,
finalId,
req.id,
scenario_title || null,
career_name,
@ -231,12 +231,12 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
`SELECT id
FROM career_profiles
WHERE id = ?`,
[newId]
[finalId]
);
return res.status(200).json({
message: 'Career profile upserted.',
career_profile_id: rows[0]?.id || newId
career_profile_id: finalId
});
} catch (error) {
console.error('Error upserting career profile:', error);
@ -1383,6 +1383,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
const {
id, // <-- Accept this in body
career_profile_id,
selected_school,
selected_program,
@ -1395,6 +1396,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
credit_hours_required,
hours_completed,
program_length,
enrollment_date,
expected_graduation,
existing_college_debt,
interest_rate,
@ -1409,7 +1411,8 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
} = req.body;
try {
const newId = uuidv4();
// If the request includes an existing id, use it; otherwise generate a new one
const finalId = id || uuidv4();
const sql = `
INSERT INTO college_profiles (
@ -1428,6 +1431,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
hours_completed,
program_length,
credit_hours_required,
enrollment_date,
expected_graduation,
existing_college_debt,
interest_rate,
@ -1444,7 +1448,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?
?, ?, ?, ?, ?
)
ON DUPLICATE KEY UPDATE
is_in_state = VALUES(is_in_state),
@ -1456,6 +1460,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
hours_completed = VALUES(hours_completed),
program_length = VALUES(program_length),
credit_hours_required = VALUES(credit_hours_required),
enrollment_date = VALUES(enrollment_date),
expected_graduation = VALUES(expected_graduation),
existing_college_debt = VALUES(existing_college_debt),
interest_rate = VALUES(interest_rate),
@ -1470,8 +1475,8 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
`;
await pool.query(sql, [
newId,
req.id,
finalId,
req.id, // user_id
career_profile_id,
selected_school,
selected_program,
@ -1485,6 +1490,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
hours_completed || 0,
program_length || 0,
credit_hours_required || 0,
enrollment_date || null,
expected_graduation || null,
existing_college_debt || 0,
interest_rate || 0,
@ -1497,7 +1503,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
tuition_paid || 0
]);
res.status(201).json({ message: 'College profile upsert done.' });
res.status(201).json({ message: 'College profile upsert done.', id: finalId });
} catch (error) {
console.error('Error saving college profile:', error);
res.status(500).json({ error: 'Failed to save college profile.' });

View File

@ -20,8 +20,6 @@ import parseFloatOrZero from '../utils/ParseFloatorZero.js';
import { getFullStateName } from '../utils/stateUtils.js';
import { Button } from './ui/button.js';
import CareerSelectDropdown from './CareerSelectDropdown.js';
import MilestoneTimeline from './MilestoneTimeline.js';
import ScenarioEditModal from './ScenarioEditModal.js';
import './CareerRoadmap.css';
@ -634,6 +632,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
annualFinancialAid: collegeData.annualFinancialAid,
calculatedTuition: collegeData.calculatedTuition,
extraPayment: collegeData.extraPayment,
enrollmentDate: collegeProfile.enrollmentDate || null,
inCollege: collegeData.inCollege,
gradDate: collegeData.gradDate,
programType: collegeData.programType,
@ -653,6 +652,8 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
randomRangeMax
};
console.log('Merged profile to simulate =>', mergedProfile);
const { projectionData: pData, loanPaidOffMonth } =
simulateFinancialProjection(mergedProfile);

View File

@ -23,6 +23,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
annual_financial_aid = '',
is_online = false,
existing_college_debt = '',
enrollment_date = '',
expected_graduation = '',
interest_rate = 5.5,
loan_term = 10,
@ -494,11 +495,24 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
/>
</div>
{college_enrollment_status === 'prospective_student' && (
<div className="space-y-1">
<label className="block font-medium">Intended Start Date</label>
<input
type="date"
name="enrollment_date"
value={enrollment_date || ''}
onChange={handleParentFieldChange}
className="w-full border rounded p-2"
/>
</div>
)}
{/* If "currently_enrolled" show Hours Completed + Program Length */}
{college_enrollment_status === 'currently_enrolled' && (
<>
<div className="space-y-1">
<label className="block font-medium">Hours Completed</label>
<label className="block font-medium">Completed Credit Hours (that count towards program completion)</label>
<input
type="number"
name="hours_completed"

View File

@ -113,7 +113,7 @@ const OnboardingContainer = () => {
localStorage.removeItem('premiumOnboardingState');
// 5) Navigate away
navigate('/milestone-tracker');
navigate('/career-roadmap');
} catch (err) {
console.error(err);
// Optionally show error to user

View File

@ -39,7 +39,7 @@ function ReviewPage({
<h3 className="text-xl font-semibold">Career Info</h3>
<div><strong>Career Name:</strong> {careerData.career_name || 'N/A'}</div>
<div><strong>Currently Working:</strong> {careerData.currently_working || 'N/A'}</div>
<div><strong>Enrollment Status:</strong> {careerData.college_enrollment_status || 'N/A'}</div>
<div><strong>College enrollment Status:</strong> {careerData.college_enrollment_status || 'N/A'}</div>
<div><strong>Status:</strong> {careerData.status || 'N/A'}</div>
<div><strong>Start Date:</strong> {careerData.start_date || 'N/A'}</div>
<div><strong>Projected End Date:</strong> {careerData.projected_end_date || 'N/A'}</div>
@ -90,21 +90,21 @@ function ReviewPage({
<div className="p-4 border rounded-md space-y-2">
<h3 className="text-xl font-semibold">College Info</h3>
<div><strong>College Name</strong></div>
<div><strong>Major</strong></div>
<div><strong>Program Type</strong></div>
<div><strong>Tuition (calculated)</strong></div>
<div><strong>Program Length (years)</strong></div>
<div><strong>Credit Hours Per Year</strong></div>
<div><strong>Credit Hours Required</strong></div>
<div><strong>Hours Completed</strong></div>
<div><strong>Is In State?</strong></div>
<div><strong>Loan Deferral Until Graduation?</strong></div>
<div><strong>Annual Financial Aid</strong></div>
<div><strong>Existing College Debt</strong></div>
<div><strong>Extra Monthly Payment</strong></div>
<div><strong>Expected Graduation</strong></div>
<div><strong>Expected Salary</strong></div>
<div><strong>College Name:</strong> {formatNum(collegeData.selected_school)}</div>
<div><strong>Major</strong> {formatNum(collegeData.selected_program)}</div>
<div><strong>Program Type</strong> {formatNum(collegeData.program_type)}</div>
<div><strong>Yearly Tuition</strong> {formatNum(collegeData.tuition)}</div>
<div><strong>Program Length (years)</strong> {formatNum(collegeData.program_length)}</div>
<div><strong>Credit Hours Per Year</strong> {formatNum(collegeData.credit_hours_per_year)}</div>
<div><strong>Credit Hours Required</strong> {formatNum(collegeData.credit_hours_required)}</div>
<div><strong>Hours Completed</strong> {formatNum(collegeData.hours_completed)}</div>
<div><strong>Is In State?</strong> {formatNum(collegeData.is_in_state)}</div>
<div><strong>Loan Deferral Until Graduation?</strong> {formatNum(collegeData.loan_deferral_until_graduation)}</div>
<div><strong>Annual Financial Aid</strong> {formatNum(collegeData.annual_financial_aid)}</div>
<div><strong>Existing College Debt</strong> {formatNum(collegeData.existing_college_debt)}</div>
<div><strong>Extra Monthly Payment</strong> {formatNum(collegeData.extra_payment)}</div>
<div><strong>Expected Graduation</strong> {formatNum(collegeData.expected_graduation)}</div>
<div><strong>Expected Salary</strong> {formatNum(collegeData.expected_salary)}</div>
</div>
)}

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import authFetch from '../utils/authFetch.js';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
import { Button } from './ui/button.js';
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
// Data paths
const CIP_URL = '/cip_institution_mapping_new.json';
@ -137,17 +138,18 @@ export default function ScenarioEditModal({
college_enrollment_status: s.college_enrollment_status || 'not_enrolled',
currently_working: s.currently_working || 'no',
planned_monthly_expenses: s.planned_monthly_expenses ?? '',
planned_monthly_debt_payments: s.planned_monthly_debt_payments ?? '',
planned_monthly_expenses: s.planned_monthly_expenses ?? null,
planned_monthly_debt_payments: s.planned_monthly_debt_payments ?? null,
planned_monthly_retirement_contribution:
s.planned_monthly_retirement_contribution ?? '',
s.planned_monthly_retirement_contribution ?? null,
planned_monthly_emergency_contribution:
s.planned_monthly_emergency_contribution ?? '',
planned_surplus_emergency_pct: s.planned_surplus_emergency_pct ?? '',
planned_surplus_retirement_pct: s.planned_surplus_retirement_pct ?? '',
planned_additional_income: s.planned_additional_income ?? '',
s.planned_monthly_emergency_contribution ?? null,
planned_surplus_emergency_pct: s.planned_surplus_emergency_pct ?? null,
planned_surplus_retirement_pct: s.planned_surplus_retirement_pct ?? null,
planned_additional_income: s.planned_additional_income ?? null,
// college portion
college_profile_id: c.id || null,
selected_school: c.selected_school || '',
selected_program: c.selected_program || '',
program_type: c.program_type || '',
@ -169,7 +171,8 @@ export default function ScenarioEditModal({
hours_completed: c.hours_completed ?? '',
program_length: c.program_length ?? '',
credit_hours_required: c.credit_hours_required ?? '',
expected_graduation: c.expected_graduation || '',
enrollment_date: c.enrollment_date ? c.enrollment_date.substring(0, 10): '',
expected_graduation: c.expected_graduation ? c.expected_graduation.substring(0, 10): '',
expected_salary: c.expected_salary ?? ''
});
@ -415,7 +418,7 @@ export default function ScenarioEditModal({
annualFinancialAid: collegeRow.annual_financial_aid || 0,
calculatedTuition: collegeRow.tuition || 0,
extraPayment: collegeRow.extra_payment || 0,
enrollmentDate: collegeRow.enrollment_date || null,
gradDate: collegeRow.expected_graduation || null,
programType: collegeRow.program_type || null,
hoursCompleted: collegeRow.hours_completed || 0,
@ -467,6 +470,12 @@ export default function ScenarioEditModal({
// Build scenario payload
const scenarioPayload = {};
// If scenario already has an id, include it:
if (scenario?.id) {
scenarioPayload.id = scenario.id;
}
scenarioPayload.college_enrollment_status = finalCollegeStatus;
scenarioPayload.currently_working = formData.currently_working || 'no';
@ -486,8 +495,7 @@ export default function ScenarioEditModal({
formData.projected_end_date &&
formData.projected_end_date.trim() !== ''
) {
scenarioPayload.projected_end_date =
formData.projected_end_date.trim();
scenarioPayload.projected_end_date = formData.projected_end_date.trim();
}
const pme = parseNumberIfGiven(formData.planned_monthly_expenses);
@ -537,6 +545,7 @@ export default function ScenarioEditModal({
// 2) Build college payload
const collegePayload = {
id: formData.college_profile_id || null,
career_profile_id: updatedScenarioId,
college_enrollment_status: finalCollegeStatus,
is_in_state: formData.is_in_state ? 1 : 0,
@ -556,12 +565,16 @@ export default function ScenarioEditModal({
const acCal = parseStringIfGiven(formData.academic_calendar);
if (acCal !== undefined) collegePayload.academic_calendar = acCal;
if (
formData.expected_graduation &&
formData.expected_graduation.trim() !== ''
) {
collegePayload.expected_graduation =
formData.expected_graduation.trim();
if (formData.expected_graduation && formData.expected_graduation.trim() !== '') {
collegePayload.expected_graduation = formData.expected_graduation
.trim()
.substring(0, 10);
}
if (formData.enrollment_date && formData.enrollment_date.trim() !== '') {
collegePayload.enrollment_date = formData.enrollment_date
.trim()
.substring(0, 10);
}
const afa = parseNumberIfGiven(formData.annual_financial_aid);
@ -607,30 +620,31 @@ export default function ScenarioEditModal({
}
// 3) Upsert or skip
if (finalCollegeStatus === 'currently_enrolled' ||
finalCollegeStatus === 'prospective_student')
{
const colRes = await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(collegePayload),
});
if (!colRes.ok) {
const msg2 = await colRes.text();
throw new Error(`College upsert failed: ${msg2}`);
}
} else {
console.log('Skipping college-profile upsert in EditScenarioModal because user not enrolled');
// Optionally: if you want to delete an existing college profile:
// await authFetch(`/api/premium/college-profile/delete/${updatedScenarioId}`, { method: 'DELETE' });
if (
finalCollegeStatus === 'currently_enrolled' ||
finalCollegeStatus === 'prospective_student'
) {
const colRes = await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(collegePayload)
});
if (!colRes.ok) {
const msg2 = await colRes.text();
throw new Error(`College upsert failed: ${msg2}`);
}
} else {
console.log(
'Skipping college-profile upsert in EditScenarioModal because user not enrolled'
);
// Optionally: if you want to delete an existing college profile:
// await authFetch(`/api/premium/college-profile/delete/${updatedScenarioId}`, { method: 'DELETE' });
}
// 4) Re-fetch scenario, college, financial => aggregator => simulate
const [scenResp2, colResp2, finResp] = await Promise.all([
authFetch(`/api/premium/career-profile/${updatedScenarioId}`),
authFetch(
`/api/premium/college-profile?careerProfileId=${updatedScenarioId}`
),
authFetch(`/api/premium/college-profile?careerProfileId=${updatedScenarioId}`),
authFetch(`/api/premium/financial-profile`)
]);
if (!scenResp2.ok || !colResp2.ok || !finResp.ok) {
@ -643,24 +657,139 @@ export default function ScenarioEditModal({
return;
}
const [finalScenarioRow, finalCollegeRaw, finalFinancial] =
await Promise.all([scenResp2.json(), colResp2.json(), finResp.json()]);
const [finalScenarioRow, finalCollegeRaw, finalFinancial] = await Promise.all([
scenResp2.json(),
colResp2.json(),
finResp.json()
]);
let finalCollegeRow = Array.isArray(finalCollegeRaw)
? finalCollegeRaw[0] || {}
: finalCollegeRaw;
// 5) Simulate
// -------------------------------------------
// 5) Before simulate: parse numeric fields
// to avoid .toFixed errors
// -------------------------------------------
// scenario planned_ fields
if (finalScenarioRow.planned_monthly_expenses != null) {
finalScenarioRow.planned_monthly_expenses = parseFloatOrZero(
finalScenarioRow.planned_monthly_expenses,
null
);
}
if (finalScenarioRow.planned_monthly_debt_payments != null) {
finalScenarioRow.planned_monthly_debt_payments = parseFloatOrZero(
finalScenarioRow.planned_monthly_debt_payments,
null
);
}
if (finalScenarioRow.planned_monthly_retirement_contribution != null) {
finalScenarioRow.planned_monthly_retirement_contribution = parseFloatOrZero(
finalScenarioRow.planned_monthly_retirement_contribution,
null
);
}
if (finalScenarioRow.planned_monthly_emergency_contribution != null) {
finalScenarioRow.planned_monthly_emergency_contribution = parseFloatOrZero(
finalScenarioRow.planned_monthly_emergency_contribution,
null
);
}
if (finalScenarioRow.planned_surplus_emergency_pct != null) {
finalScenarioRow.planned_surplus_emergency_pct = parseFloatOrZero(
finalScenarioRow.planned_surplus_emergency_pct,
null
);
}
if (finalScenarioRow.planned_surplus_retirement_pct != null) {
finalScenarioRow.planned_surplus_retirement_pct = parseFloatOrZero(
finalScenarioRow.planned_surplus_retirement_pct,
null
);
}
if (finalScenarioRow.planned_additional_income != null) {
finalScenarioRow.planned_additional_income = parseFloatOrZero(
finalScenarioRow.planned_additional_income,
null
);
}
// college numeric fields (force all to numbers or 0)
const numericFields = [
'existing_college_debt',
'extra_payment',
'tuition',
'tuition_paid',
'interest_rate',
'loan_term',
'credit_hours_per_year',
'hours_completed',
'program_length',
'expected_salary',
'annual_financial_aid',
'credit_hours_required'
];
for (const field of numericFields) {
if (finalCollegeRow[field] != null) {
finalCollegeRow[field] = parseFloatOrZero(finalCollegeRow[field], 0);
} else {
finalCollegeRow[field] = 0;
}
}
// Also ensure all scenario/financial fields used in buildMergedUserProfile are numbers
const scenarioNumericFields = [
'planned_monthly_expenses',
'planned_monthly_debt_payments',
'planned_monthly_retirement_contribution',
'planned_monthly_emergency_contribution',
'planned_surplus_emergency_pct',
'planned_surplus_retirement_pct',
'planned_additional_income'
];
for (const field of scenarioNumericFields) {
if (finalScenarioRow[field] != null) {
finalScenarioRow[field] = parseFloatOrZero(finalScenarioRow[field], 0);
} else {
finalScenarioRow[field] = 0;
}
}
if (finalFinancial) {
const financialNumericFields = [
'current_salary',
'monthly_expenses',
'monthly_debt_payments',
'additional_income',
'emergency_fund',
'retirement_savings',
'retirement_contribution',
'emergency_contribution',
'extra_cash_emergency_pct',
'extra_cash_retirement_pct'
];
for (const field of financialNumericFields) {
if (finalFinancial[field] != null) {
finalFinancial[field] = parseFloatOrZero(finalFinancial[field], 0);
} else {
finalFinancial[field] = 0;
}
}
}
// 6) Now simulate
const userProfile = buildMergedUserProfile(
finalScenarioRow,
finalCollegeRow,
finalFinancial
);
console.log('UserProfile from Modal:', userProfile);
const results = simulateFinancialProjection(userProfile);
setProjectionData(results.projectionData);
setLoanPayoffMonth(results.loanPaidOffMonth);
// 6) Close or reload
// 7) Close or reload
onClose();
window.location.reload();
} catch (err) {
@ -797,7 +926,7 @@ export default function ScenarioEditModal({
{/* College Enrollment Status */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
College Enrollment Status (scenario row)
College Enrollment Status
</label>
<select
name="college_enrollment_status"
@ -947,7 +1076,7 @@ export default function ScenarioEditModal({
<input
type="checkbox"
name="is_online"
checked={!!formData.is_online}
checked={!!formData.is_in_online}
onChange={handleFormChange}
className="mr-1"
/>
@ -1169,6 +1298,20 @@ export default function ScenarioEditModal({
</>
)}
{/* Enrollment Date */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Enrollment Date
</label>
<input
type="date"
name="enrollment_date"
value={formData.enrollment_date || ''}
onChange={handleFormChange}
className="border border-gray-300 rounded p-2 w-full"
/>
</div>
{/* Expected Graduation */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">

View File

@ -109,6 +109,7 @@ export function simulateFinancialProjection(userProfile) {
hoursCompleted = 0,
creditHoursPerYear,
calculatedTuition,
enrollmentDate,
gradDate,
startDate,
academicCalendar = 'monthly',
@ -174,6 +175,7 @@ function getMonthlyInterestRate() {
/***************************************************
* 2) CLAMP THE SCENARIO START TO MONTH-BEGIN
***************************************************/
const scenarioStartClamped = moment(startDate || new Date()).startOf('month');
/***************************************************
@ -267,26 +269,30 @@ function getMonthlyInterestRate() {
// If there's a gradDate, let's see if we pass it:
const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null;
const enrollmentDateObj = enrollmentDate
? moment(enrollmentDate).startOf('month')
: scenarioStartClamped.clone(); // fallback
/***************************************************
* 7) THE MONTHLY LOOP
***************************************************/
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
// Check if loan is fully paid
if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = currentSimDate.format('YYYY-MM');
}
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
// Are we still in college for this month?
let stillInCollege = false;
if (inCollege) {
if (graduationDateObj) {
stillInCollege = currentSimDate.isBefore(graduationDateObj, 'month');
} else {
stillInCollege = (monthIndex < totalAcademicMonths);
}
}
// figure out if we are in the college window
let stillInCollege = false;
if (inCollege && enrollmentDateObj && graduationDateObj) {
stillInCollege = currentSimDate.isSameOrAfter(enrollmentDateObj)
&& currentSimDate.isBefore(graduationDateObj);
if (inCollege && gradDate) {
stillInCollege =
currentSimDate.isSameOrAfter(enrollmentDateObj) &&
currentSimDate.isBefore(graduationDateObj);
}
}
/************************************************
* 7.1 TUITION lumps