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 { try {
const newId = uuidv4(); const finalId = req.body.id || uuidv4();
// 1) Insert includes career_goals // 1) Insert includes career_goals
const sql = ` const sql = `
@ -207,7 +207,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
`; `;
await pool.query(sql, [ await pool.query(sql, [
newId, finalId,
req.id, req.id,
scenario_title || null, scenario_title || null,
career_name, career_name,
@ -231,12 +231,12 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
`SELECT id `SELECT id
FROM career_profiles FROM career_profiles
WHERE id = ?`, WHERE id = ?`,
[newId] [finalId]
); );
return res.status(200).json({ return res.status(200).json({
message: 'Career profile upserted.', message: 'Career profile upserted.',
career_profile_id: rows[0]?.id || newId career_profile_id: finalId
}); });
} catch (error) { } catch (error) {
console.error('Error upserting career profile:', 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) => { app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
const { const {
id, // <-- Accept this in body
career_profile_id, career_profile_id,
selected_school, selected_school,
selected_program, selected_program,
@ -1395,6 +1396,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
credit_hours_required, credit_hours_required,
hours_completed, hours_completed,
program_length, program_length,
enrollment_date,
expected_graduation, expected_graduation,
existing_college_debt, existing_college_debt,
interest_rate, interest_rate,
@ -1409,7 +1411,8 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
} = req.body; } = req.body;
try { try {
const newId = uuidv4(); // If the request includes an existing id, use it; otherwise generate a new one
const finalId = id || uuidv4();
const sql = ` const sql = `
INSERT INTO college_profiles ( INSERT INTO college_profiles (
@ -1428,6 +1431,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
hours_completed, hours_completed,
program_length, program_length,
credit_hours_required, credit_hours_required,
enrollment_date,
expected_graduation, expected_graduation,
existing_college_debt, existing_college_debt,
interest_rate, interest_rate,
@ -1444,7 +1448,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ? ?, ?, ?, ?, ?
) )
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
is_in_state = VALUES(is_in_state), 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), hours_completed = VALUES(hours_completed),
program_length = VALUES(program_length), program_length = VALUES(program_length),
credit_hours_required = VALUES(credit_hours_required), credit_hours_required = VALUES(credit_hours_required),
enrollment_date = VALUES(enrollment_date),
expected_graduation = VALUES(expected_graduation), expected_graduation = VALUES(expected_graduation),
existing_college_debt = VALUES(existing_college_debt), existing_college_debt = VALUES(existing_college_debt),
interest_rate = VALUES(interest_rate), interest_rate = VALUES(interest_rate),
@ -1470,8 +1475,8 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
`; `;
await pool.query(sql, [ await pool.query(sql, [
newId, finalId,
req.id, req.id, // user_id
career_profile_id, career_profile_id,
selected_school, selected_school,
selected_program, selected_program,
@ -1485,6 +1490,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
hours_completed || 0, hours_completed || 0,
program_length || 0, program_length || 0,
credit_hours_required || 0, credit_hours_required || 0,
enrollment_date || null,
expected_graduation || null, expected_graduation || null,
existing_college_debt || 0, existing_college_debt || 0,
interest_rate || 0, interest_rate || 0,
@ -1497,7 +1503,7 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
tuition_paid || 0 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) { } catch (error) {
console.error('Error saving college profile:', error); console.error('Error saving college profile:', error);
res.status(500).json({ error: 'Failed to save college profile.' }); 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 { getFullStateName } from '../utils/stateUtils.js';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
import CareerSelectDropdown from './CareerSelectDropdown.js';
import MilestoneTimeline from './MilestoneTimeline.js';
import ScenarioEditModal from './ScenarioEditModal.js'; import ScenarioEditModal from './ScenarioEditModal.js';
import './CareerRoadmap.css'; import './CareerRoadmap.css';
@ -634,6 +632,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
annualFinancialAid: collegeData.annualFinancialAid, annualFinancialAid: collegeData.annualFinancialAid,
calculatedTuition: collegeData.calculatedTuition, calculatedTuition: collegeData.calculatedTuition,
extraPayment: collegeData.extraPayment, extraPayment: collegeData.extraPayment,
enrollmentDate: collegeProfile.enrollmentDate || null,
inCollege: collegeData.inCollege, inCollege: collegeData.inCollege,
gradDate: collegeData.gradDate, gradDate: collegeData.gradDate,
programType: collegeData.programType, programType: collegeData.programType,
@ -653,6 +652,8 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
randomRangeMax randomRangeMax
}; };
console.log('Merged profile to simulate =>', mergedProfile);
const { projectionData: pData, loanPaidOffMonth } = const { projectionData: pData, loanPaidOffMonth } =
simulateFinancialProjection(mergedProfile); simulateFinancialProjection(mergedProfile);

View File

@ -23,6 +23,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
annual_financial_aid = '', annual_financial_aid = '',
is_online = false, is_online = false,
existing_college_debt = '', existing_college_debt = '',
enrollment_date = '',
expected_graduation = '', expected_graduation = '',
interest_rate = 5.5, interest_rate = 5.5,
loan_term = 10, loan_term = 10,
@ -494,11 +495,24 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
/> />
</div> </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 */} {/* If "currently_enrolled" show Hours Completed + Program Length */}
{college_enrollment_status === 'currently_enrolled' && ( {college_enrollment_status === 'currently_enrolled' && (
<> <>
<div className="space-y-1"> <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 <input
type="number" type="number"
name="hours_completed" name="hours_completed"

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import authFetch from '../utils/authFetch.js'; import authFetch from '../utils/authFetch.js';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
// Data paths // Data paths
const CIP_URL = '/cip_institution_mapping_new.json'; 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', college_enrollment_status: s.college_enrollment_status || 'not_enrolled',
currently_working: s.currently_working || 'no', currently_working: s.currently_working || 'no',
planned_monthly_expenses: s.planned_monthly_expenses ?? '', planned_monthly_expenses: s.planned_monthly_expenses ?? null,
planned_monthly_debt_payments: s.planned_monthly_debt_payments ?? '', planned_monthly_debt_payments: s.planned_monthly_debt_payments ?? null,
planned_monthly_retirement_contribution: planned_monthly_retirement_contribution:
s.planned_monthly_retirement_contribution ?? '', s.planned_monthly_retirement_contribution ?? null,
planned_monthly_emergency_contribution: planned_monthly_emergency_contribution:
s.planned_monthly_emergency_contribution ?? '', s.planned_monthly_emergency_contribution ?? null,
planned_surplus_emergency_pct: s.planned_surplus_emergency_pct ?? '', planned_surplus_emergency_pct: s.planned_surplus_emergency_pct ?? null,
planned_surplus_retirement_pct: s.planned_surplus_retirement_pct ?? '', planned_surplus_retirement_pct: s.planned_surplus_retirement_pct ?? null,
planned_additional_income: s.planned_additional_income ?? '', planned_additional_income: s.planned_additional_income ?? null,
// college portion // college portion
college_profile_id: c.id || null,
selected_school: c.selected_school || '', selected_school: c.selected_school || '',
selected_program: c.selected_program || '', selected_program: c.selected_program || '',
program_type: c.program_type || '', program_type: c.program_type || '',
@ -169,7 +171,8 @@ export default function ScenarioEditModal({
hours_completed: c.hours_completed ?? '', hours_completed: c.hours_completed ?? '',
program_length: c.program_length ?? '', program_length: c.program_length ?? '',
credit_hours_required: c.credit_hours_required ?? '', 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 ?? '' expected_salary: c.expected_salary ?? ''
}); });
@ -415,7 +418,7 @@ export default function ScenarioEditModal({
annualFinancialAid: collegeRow.annual_financial_aid || 0, annualFinancialAid: collegeRow.annual_financial_aid || 0,
calculatedTuition: collegeRow.tuition || 0, calculatedTuition: collegeRow.tuition || 0,
extraPayment: collegeRow.extra_payment || 0, extraPayment: collegeRow.extra_payment || 0,
enrollmentDate: collegeRow.enrollment_date || null,
gradDate: collegeRow.expected_graduation || null, gradDate: collegeRow.expected_graduation || null,
programType: collegeRow.program_type || null, programType: collegeRow.program_type || null,
hoursCompleted: collegeRow.hours_completed || 0, hoursCompleted: collegeRow.hours_completed || 0,
@ -467,6 +470,12 @@ export default function ScenarioEditModal({
// Build scenario payload // Build scenario payload
const scenarioPayload = {}; const scenarioPayload = {};
// If scenario already has an id, include it:
if (scenario?.id) {
scenarioPayload.id = scenario.id;
}
scenarioPayload.college_enrollment_status = finalCollegeStatus; scenarioPayload.college_enrollment_status = finalCollegeStatus;
scenarioPayload.currently_working = formData.currently_working || 'no'; scenarioPayload.currently_working = formData.currently_working || 'no';
@ -486,8 +495,7 @@ export default function ScenarioEditModal({
formData.projected_end_date && formData.projected_end_date &&
formData.projected_end_date.trim() !== '' formData.projected_end_date.trim() !== ''
) { ) {
scenarioPayload.projected_end_date = scenarioPayload.projected_end_date = formData.projected_end_date.trim();
formData.projected_end_date.trim();
} }
const pme = parseNumberIfGiven(formData.planned_monthly_expenses); const pme = parseNumberIfGiven(formData.planned_monthly_expenses);
@ -537,6 +545,7 @@ export default function ScenarioEditModal({
// 2) Build college payload // 2) Build college payload
const collegePayload = { const collegePayload = {
id: formData.college_profile_id || null,
career_profile_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,
@ -556,12 +565,16 @@ export default function ScenarioEditModal({
const acCal = parseStringIfGiven(formData.academic_calendar); const acCal = parseStringIfGiven(formData.academic_calendar);
if (acCal !== undefined) collegePayload.academic_calendar = acCal; if (acCal !== undefined) collegePayload.academic_calendar = acCal;
if ( if (formData.expected_graduation && formData.expected_graduation.trim() !== '') {
formData.expected_graduation && collegePayload.expected_graduation = formData.expected_graduation
formData.expected_graduation.trim() !== '' .trim()
) { .substring(0, 10);
collegePayload.expected_graduation = }
formData.expected_graduation.trim();
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); const afa = parseNumberIfGiven(formData.annual_financial_aid);
@ -607,30 +620,31 @@ export default function ScenarioEditModal({
} }
// 3) Upsert or skip // 3) Upsert or skip
if (finalCollegeStatus === 'currently_enrolled' || if (
finalCollegeStatus === 'prospective_student') finalCollegeStatus === 'currently_enrolled' ||
{ finalCollegeStatus === 'prospective_student'
const colRes = await authFetch('/api/premium/college-profile', { ) {
method: 'POST', const colRes = await authFetch('/api/premium/college-profile', {
headers: { 'Content-Type': 'application/json' }, method: 'POST',
body: JSON.stringify(collegePayload), headers: { 'Content-Type': 'application/json' },
}); body: JSON.stringify(collegePayload)
if (!colRes.ok) { });
const msg2 = await colRes.text(); if (!colRes.ok) {
throw new Error(`College upsert failed: ${msg2}`); 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' });
} }
} 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([
authFetch(`/api/premium/career-profile/${updatedScenarioId}`), authFetch(`/api/premium/career-profile/${updatedScenarioId}`),
authFetch( authFetch(`/api/premium/college-profile?careerProfileId=${updatedScenarioId}`),
`/api/premium/college-profile?careerProfileId=${updatedScenarioId}`
),
authFetch(`/api/premium/financial-profile`) authFetch(`/api/premium/financial-profile`)
]); ]);
if (!scenResp2.ok || !colResp2.ok || !finResp.ok) { if (!scenResp2.ok || !colResp2.ok || !finResp.ok) {
@ -643,24 +657,139 @@ export default function ScenarioEditModal({
return; return;
} }
const [finalScenarioRow, finalCollegeRaw, finalFinancial] = const [finalScenarioRow, finalCollegeRaw, finalFinancial] = await Promise.all([
await Promise.all([scenResp2.json(), colResp2.json(), finResp.json()]); scenResp2.json(),
colResp2.json(),
finResp.json()
]);
let finalCollegeRow = Array.isArray(finalCollegeRaw) let finalCollegeRow = Array.isArray(finalCollegeRaw)
? finalCollegeRaw[0] || {} ? finalCollegeRaw[0] || {}
: finalCollegeRaw; : 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( const userProfile = buildMergedUserProfile(
finalScenarioRow, finalScenarioRow,
finalCollegeRow, finalCollegeRow,
finalFinancial finalFinancial
); );
console.log('UserProfile from Modal:', userProfile);
const results = simulateFinancialProjection(userProfile); const results = simulateFinancialProjection(userProfile);
setProjectionData(results.projectionData); setProjectionData(results.projectionData);
setLoanPayoffMonth(results.loanPaidOffMonth); setLoanPayoffMonth(results.loanPaidOffMonth);
// 6) Close or reload // 7) Close or reload
onClose(); onClose();
window.location.reload(); window.location.reload();
} catch (err) { } catch (err) {
@ -797,7 +926,7 @@ export default function ScenarioEditModal({
{/* College Enrollment Status */} {/* College Enrollment Status */}
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
College Enrollment Status (scenario row) College Enrollment Status
</label> </label>
<select <select
name="college_enrollment_status" name="college_enrollment_status"
@ -947,7 +1076,7 @@ export default function ScenarioEditModal({
<input <input
type="checkbox" type="checkbox"
name="is_online" name="is_online"
checked={!!formData.is_online} checked={!!formData.is_in_online}
onChange={handleFormChange} onChange={handleFormChange}
className="mr-1" 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 */} {/* Expected Graduation */}
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">

View File

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