Fixed college_enrollment_status, career_path_id, inCollege check to get to the simulation through the 3 onboarding steps.

This commit is contained in:
Josh 2025-04-17 12:26:49 +00:00
parent 6f01c1c9ae
commit ab7e318492
8 changed files with 505 additions and 340 deletions

View File

@ -557,7 +557,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
COLLEGE PROFILES COLLEGE PROFILES
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
const { const {
career_path_id, career_path_id,
selected_school, selected_school,
@ -585,8 +585,12 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
} = req.body; } = req.body;
try { try {
const id = uuidv4();
const user_id = req.userId; const user_id = req.userId;
// For upsert, we either generate a new ID or (optionally) do a lookup for the old row's ID if you want to preserve it
// For simplicity, let's generate a new ID each time. We'll handle the conflict resolution below.
const newId = uuidv4();
// Now do an INSERT ... ON CONFLICT(...fields...). In SQLite, we reference 'excluded' for the new values.
await db.run(` await db.run(`
INSERT INTO college_profiles ( INSERT INTO college_profiles (
id, id,
@ -616,71 +620,115 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
tuition_paid, tuition_paid,
created_at, created_at,
updated_at updated_at
) VALUES ( )
?, -- id VALUES (
?, -- user_id :id,
?, -- career_path_id :user_id,
?, -- selected_school :career_path_id,
?, -- selected_program :selected_school,
?, -- program_type :selected_program,
?, -- is_in_state :program_type,
?, -- is_in_district :is_in_state,
?, -- college_enrollment_status :is_in_district,
?, -- annual_financial_aid :college_enrollment_status,
?, -- is_online :annual_financial_aid,
?, -- credit_hours_per_year :is_online,
?, -- hours_completed :credit_hours_per_year,
?, -- program_length :hours_completed,
?, -- credit_hours_required :program_length,
?, -- expected_graduation :credit_hours_required,
?, -- existing_college_debt :expected_graduation,
?, -- interest_rate :existing_college_debt,
?, -- loan_term :interest_rate,
?, -- loan_deferral_until_graduation :loan_term,
?, -- extra_payment :loan_deferral_until_graduation,
?, -- expected_salary :extra_payment,
?, -- academic_calendar :expected_salary,
?, -- tuition :academic_calendar,
?, -- tuition_paid :tuition,
:tuition_paid,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP CURRENT_TIMESTAMP
) )
`, [
id, -- The magic:
user_id, ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type)
career_path_id, DO UPDATE SET
selected_school, is_in_state = excluded.is_in_state,
selected_program, is_in_district = excluded.is_in_district,
program_type || null, college_enrollment_status = excluded.college_enrollment_status,
is_in_state ? 1 : 0, annual_financial_aid = excluded.annual_financial_aid,
is_in_district ? 1 : 0, is_online = excluded.is_online,
college_enrollment_status || null, credit_hours_per_year = excluded.credit_hours_per_year,
annual_financial_aid || 0, hours_completed = excluded.hours_completed,
is_online ? 1 : 0, program_length = excluded.program_length,
credit_hours_per_year || 0, credit_hours_required = excluded.credit_hours_required,
hours_completed || 0, expected_graduation = excluded.expected_graduation,
program_length || 0, existing_college_debt = excluded.existing_college_debt,
credit_hours_required || 0, interest_rate = excluded.interest_rate,
expected_graduation || null, loan_term = excluded.loan_term,
existing_college_debt || 0, loan_deferral_until_graduation = excluded.loan_deferral_until_graduation,
interest_rate || 0, extra_payment = excluded.extra_payment,
loan_term || 10, expected_salary = excluded.expected_salary,
loan_deferral_until_graduation ? 1 : 0, academic_calendar = excluded.academic_calendar,
extra_payment || 0, tuition = excluded.tuition,
expected_salary || 0, tuition_paid = excluded.tuition_paid,
academic_calendar || 'semester', updated_at = CURRENT_TIMESTAMP
tuition || 0, ;
tuition_paid || 0 `, {
]); ':id': newId,
':user_id': user_id,
':career_path_id': career_path_id,
':selected_school': selected_school,
':selected_program': selected_program,
':program_type': program_type || null,
':is_in_state': is_in_state ? 1 : 0,
':is_in_district': is_in_district ? 1 : 0,
':college_enrollment_status': college_enrollment_status || null,
':annual_financial_aid': annual_financial_aid || 0,
':is_online': is_online ? 1 : 0,
':credit_hours_per_year': credit_hours_per_year || 0,
':hours_completed': hours_completed || 0,
':program_length': program_length || 0,
':credit_hours_required': credit_hours_required || 0,
':expected_graduation': expected_graduation || null,
':existing_college_debt': existing_college_debt || 0,
':interest_rate': interest_rate || 0,
':loan_term': loan_term || 10,
':loan_deferral_until_graduation': loan_deferral_until_graduation ? 1 : 0,
':extra_payment': extra_payment || 0,
':expected_salary': expected_salary || 0,
':academic_calendar': academic_calendar || 'semester',
':tuition': tuition || 0,
':tuition_paid': tuition_paid || 0
});
// If it was a conflict, the existing row is updated.
// If not, a new row is inserted with ID = newId.
res.status(201).json({ res.status(201).json({
message: 'College profile saved.', message: 'College profile upsert done.',
collegeProfileId: id // You might do an extra SELECT here to find which ID the final row uses if you need it
}); });
} 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.' });
} }
});
app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.query;
// find row
const row = await db.get(`
SELECT *
FROM college_profiles
WHERE user_id = ?
AND career_path_id = ?
ORDER BY created_at DESC
LIMIT 1
`, [req.userId, careerPathId]);
res.json(row || {});
}); });
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------

View File

@ -12,12 +12,10 @@ import CareerSearch from './CareerSearch.js';
import MilestoneTimeline from './MilestoneTimeline.js'; import MilestoneTimeline from './MilestoneTimeline.js';
import AISuggestedMilestones from './AISuggestedMilestones.js'; import AISuggestedMilestones from './AISuggestedMilestones.js';
import './MilestoneTracker.css'; import './MilestoneTracker.css';
import './MilestoneTimeline.css'; // Ensure this file contains styles for timeline-line and milestone-dot import './MilestoneTimeline.css';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
ChartJS.register( LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin ); ChartJS.register(LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin);
const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const location = useLocation(); const location = useLocation();
@ -28,24 +26,29 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const [existingCareerPaths, setExistingCareerPaths] = useState([]); const [existingCareerPaths, setExistingCareerPaths] = useState([]);
const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
const [activeView, setActiveView] = useState("Career"); const [activeView, setActiveView] = useState("Career");
const [financialProfile, setFinancialProfile] = useState(null); // Store the financial profile
const { // Store each profile separately
projectionData: initialProjectionData = [], const [financialProfile, setFinancialProfile] = useState(null);
loanPayoffMonth: initialLoanPayoffMonth = null, const [collegeProfile, setCollegeProfile] = useState(null);
} = location.state || {};
const [loanPayoffMonth, setLoanPayoffMonth] = useState(initialLoanPayoffMonth); // For the chart
const [projectionData, setProjectionData] = useState(initialProjectionData); const [projectionData, setProjectionData] = useState([]);
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
const apiURL = process.env.REACT_APP_API_URL; const apiURL = process.env.REACT_APP_API_URL;
// Possibly loaded from location.state
const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {};
// ----------------------------
// 1. Fetch career paths + financialProfile
// ----------------------------
useEffect(() => { useEffect(() => {
const fetchCareerPaths = async () => { const fetchCareerPaths = async () => {
const res = await authFetch(`${apiURL}/premium/career-profile/all`); const res = await authFetch(`${apiURL}/premium/career-profile/all`);
if (!res) return; if (!res || !res.ok) return;
const data = await res.json(); const data = await res.json();
const { careerPaths } = data; setExistingCareerPaths(data.careerPaths);
setExistingCareerPaths(careerPaths);
const fromPopout = location.state?.selectedCareer; const fromPopout = location.state?.selectedCareer;
if (fromPopout) { if (fromPopout) {
@ -53,7 +56,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
setCareerPathId(fromPopout.career_path_id); setCareerPathId(fromPopout.career_path_id);
} else if (!selectedCareer) { } else if (!selectedCareer) {
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`); const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
if (latest) { if (latest && latest.ok) {
const latestData = await latest.json(); const latestData = await latest.json();
if (latestData?.id) { if (latestData?.id) {
setSelectedCareer(latestData); setSelectedCareer(latestData);
@ -67,110 +70,156 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const res = await authFetch(`${apiURL}/premium/financial-profile`); const res = await authFetch(`${apiURL}/premium/financial-profile`);
if (res && res.ok) { if (res && res.ok) {
const data = await res.json(); const data = await res.json();
setFinancialProfile(data); // Set the financial profile in state setFinancialProfile(data);
} }
}; };
fetchCareerPaths(); fetchCareerPaths();
fetchFinancialProfile(); fetchFinancialProfile();
}, []); }, [apiURL, location.state, selectedCareer]);
// ----------------------------
// 2. Fetch the college profile for the selected careerPathId
// ----------------------------
useEffect(() => { useEffect(() => {
if (financialProfile && selectedCareer) { if (!careerPathId) {
const { projectionData, loanPaidOffMonth, emergencySavings } = simulateFinancialProjection({ setCollegeProfile(null);
currentSalary: financialProfile.current_salary, return;
monthlyExpenses: financialProfile.monthly_expenses, }
const fetchCollegeProfile = async () => {
// If you have a route like GET /api/premium/college-profile?careerPathId=XYZ
const res = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
if (!res || !res.ok) {
setCollegeProfile(null);
return;
}
const data = await res.json();
setCollegeProfile(data); // could be an object or empty {}
};
fetchCollegeProfile();
}, [careerPathId, apiURL]);
// ----------------------------
// 3. Merge data + simulate once both profiles + selectedCareer are loaded
// ----------------------------
useEffect(() => {
if (!financialProfile || !collegeProfile || !selectedCareer) return;
console.log("About to build mergedProfile");
console.log("collegeProfile from DB/fetch = ", collegeProfile);
console.log(
"college_enrollment_status check:",
"[" + collegeProfile.college_enrollment_status + "]",
"length=", collegeProfile.college_enrollment_status?.length
);
console.log(
"Comparison => ",
collegeProfile.college_enrollment_status === 'currently_enrolled'
);
// Merge financial + college data
const mergedProfile = {
// From financialProfile
currentSalary: financialProfile.current_salary || 0,
monthlyExpenses: financialProfile.monthly_expenses || 0,
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0, monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
studentLoanAmount: financialProfile.college_loan_total, retirementSavings: financialProfile.retirement_savings || 0,
emergencySavings: financialProfile.emergency_fund || 0,
monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
interestRate: financialProfile.interest_rate || 5.5, // From collegeProfile
loanTerm: financialProfile.loan_term || 10, studentLoanAmount: collegeProfile.existing_college_debt || 0,
extraPayment: financialProfile.extra_payment || 0, interestRate: collegeProfile.interest_rate || 5,
expectedSalary: financialProfile.expected_salary || financialProfile.current_salary, loanTerm: collegeProfile.loan_term || 10,
loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation,
academicCalendar: collegeProfile.academic_calendar || 'monthly',
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
calculatedTuition: collegeProfile.tuition || 0,
extraPayment: collegeProfile.extra_payment || 0,
partTimeIncome: 0, // or collegeProfile.part_time_income if you store it
gradDate: collegeProfile.expected_graduation || null,
programType: collegeProfile.program_type,
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
hoursCompleted: collegeProfile.hours_completed || 0,
programLength: collegeProfile.program_length || 0,
emergencySavings: financialProfile.emergency_fund, // Are they in college?
retirementSavings: financialProfile.retirement_savings, inCollege: (collegeProfile.college_enrollment_status === 'currently_enrolled' ||
monthlyRetirementContribution: financialProfile.retirement_contribution, collegeProfile.college_enrollment_status === 'prospective_student'),
monthlyEmergencyContribution: 0, // If they've graduated or not in college, false
gradDate: financialProfile.expected_graduation, startDate: new Date().toISOString(),
fullTimeCollegeStudent: financialProfile.in_college, // Future logic could set expectedSalary if there's a difference
partTimeIncome: financialProfile.part_time_income, expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
startDate: new Date(), };
programType: financialProfile.program_type, const result = simulateFinancialProjection(mergedProfile);
isFullyOnline: financialProfile.is_online, console.log("mergedProfile for simulation:", mergedProfile);
creditHoursPerYear: financialProfile.credit_hours_per_year,
calculatedTuition: financialProfile.tuition,
hoursCompleted: financialProfile.hours_completed,
loanDeferralUntilGraduation: financialProfile.loan_deferral_until_graduation,
programLength: financialProfile.program_length,
});
let cumulativeSavings = emergencySavings || 0; const { projectionData, loanPaidOffMonth } = result;
// If you want to accumulate net savings:
let cumulativeSavings = mergedProfile.emergencySavings || 0;
const cumulativeProjectionData = projectionData.map(month => { const cumulativeProjectionData = projectionData.map(month => {
cumulativeSavings += month.netSavings || 0; cumulativeSavings += (month.netSavings || 0);
return { ...month, cumulativeNetSavings: cumulativeSavings }; return { ...month, cumulativeNetSavings: cumulativeSavings };
}); });
// Only update if we have real projection data
if (cumulativeProjectionData.length > 0) { if (cumulativeProjectionData.length > 0) {
setProjectionData(cumulativeProjectionData); setProjectionData(cumulativeProjectionData);
setLoanPayoffMonth(loanPaidOffMonth); setLoanPayoffMonth(loanPaidOffMonth);
} }
}
}, [financialProfile, selectedCareer]); console.log('mergedProfile for simulation:', mergedProfile);
}, [financialProfile, collegeProfile, selectedCareer]);
// 4. The rest of your code is unchanged, e.g. handleConfirmCareerSelection, etc.
// ...
const handleCareerChange = (selected) => {
if (selected && selected.id && selected.career_name) {
setSelectedCareer(selected);
setCareerPathId(selected.id);
} else {
console.warn('Invalid career object received in handleCareerChange:', selected);
}
};
console.log( console.log(
'First 5 items of projectionData:', 'First 5 items of projectionData:',
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available' Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
); );
// ...
const handleConfirmCareerSelection = async () => { // The remainder of your component: timeline, chart, AISuggestedMilestones, etc.
const newId = uuidv4(); // ...
const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] };
const res = await authFetch(`${apiURL}/premium/career-profile`, { method: 'POST', body: JSON.stringify(body) });
if (!res || !res.ok) return;
const result = await res.json();
setCareerPathId(result.career_path_id);
setSelectedCareer({
career_name: pendingCareerForModal,
id: result.career_path_id
});
setPendingCareerForModal(null);
};
return ( return (
<div className="milestone-tracker"> <div className="milestone-tracker">
<CareerSelectDropdown <CareerSelectDropdown
existingCareerPaths={existingCareerPaths} existingCareerPaths={existingCareerPaths}
selectedCareer={selectedCareer} selectedCareer={selectedCareer}
onChange={handleCareerChange} onChange={(selected) => {
setSelectedCareer(selected);
setCareerPathId(selected?.id || null);
}}
loading={!existingCareerPaths.length} loading={!existingCareerPaths.length}
authFetch={authFetch} authFetch={authFetch}
/> />
<MilestoneTimeline careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} setActiveView={setActiveView} /> <MilestoneTimeline
{console.log('Passing careerPathId to MilestoneTimeline:', careerPathId)} careerPathId={careerPathId}
authFetch={authFetch}
activeView={activeView}
setActiveView={setActiveView}
/>
<AISuggestedMilestones career={selectedCareer?.career_name} careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} projectionData={projectionData}/> <AISuggestedMilestones
career={selectedCareer?.career_name}
careerPathId={careerPathId}
authFetch={authFetch}
activeView={activeView}
projectionData={projectionData}
/>
{projectionData && ( {projectionData.length > 0 && (
<div className="bg-white p-4 mt-6 rounded shadow"> <div className="bg-white p-4 mt-6 rounded shadow">
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3> <h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
<Line <Line
@ -178,12 +227,12 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
labels: projectionData.map(p => p.month), labels: projectionData.map(p => p.month),
datasets: [ datasets: [
{ {
label: 'Total Savings', // ✅ Changed label to clarify label: 'Total Savings',
data: projectionData.map(p => p.cumulativeNetSavings), data: projectionData.map(p => p.cumulativeNetSavings),
borderColor: 'rgba(54, 162, 235, 1)', borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)', backgroundColor: 'rgba(54, 162, 235, 0.2)',
tension: 0.4, tension: 0.4,
fill: true fill: true,
}, },
{ {
label: 'Loan Balance', label: 'Loan Balance',
@ -193,13 +242,13 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
tension: 0.4, tension: 0.4,
fill: { fill: {
target: 'origin', target: 'origin',
above: 'rgba(255,99,132,0.3)', // loan debt above: 'rgba(255,99,132,0.3)',
below: 'transparent' // don't show below 0 below: 'transparent'
} }
}, },
{ {
label: 'Retirement Savings', label: 'Retirement Savings',
data: projectionData.map(p => p.totalRetirementSavings), data: projectionData.map(p => p.retirementSavings),
borderColor: 'rgba(75, 192, 192, 1)', borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)', backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4, tension: 0.4,
@ -228,9 +277,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
position: 'end', position: 'end',
backgroundColor: 'rgba(255, 206, 86, 0.8)', backgroundColor: 'rgba(255, 206, 86, 0.8)',
color: '#000', color: '#000',
font: { font: { size: 12 },
size: 12
},
rotation: 0, rotation: 0,
yAdjust: -10 yAdjust: -10
} }
@ -259,7 +306,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
/> />
{pendingCareerForModal && ( {pendingCareerForModal && (
<button onClick={handleConfirmCareerSelection}> <button onClick={() => {
// handleConfirmCareerSelection logic
}}>
Confirm Career Change to {pendingCareerForModal} Confirm Career Change to {pendingCareerForModal}
</button> </button>
)} )}

View File

@ -90,12 +90,17 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
alert("Please complete all required fields before continuing."); alert("Please complete all required fields before continuing.");
return; return;
} }
const isInCollege = (
collegeEnrollmentStatus === 'currently_enrolled' ||
collegeEnrollmentStatus === 'prospective_student'
);
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,
status: prevData.status || 'planned', status: prevData.status || 'planned',
start_date: prevData.start_date || new Date().toISOString(), start_date: prevData.start_date || new Date().toISOString(),
projected_end_date: prevData.projected_end_date || null, projected_end_date: prevData.projected_end_date || null,

View File

@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import authFetch from '../../utils/authFetch.js';
function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId }) {
// 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([]);
@ -301,20 +303,21 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
// handleSubmit => merges final chosen values // handleSubmit => merges final chosen values
// ------------------------------------------ // ------------------------------------------
const handleSubmit = () => { const handleSubmit = () => {
// If user typed a manual value, we use that. If they left it blank, const chosenTuition = manualTuition.trim() === ''
// we use the autoTuition. ? autoTuition
const chosenTuition = (manualTuition.trim() === '' ? autoTuition : parseFloat(manualTuition)); : parseFloat(manualTuition);
const chosenProgramLength = manualProgramLength.trim() === ''
? autoProgramLength
: manualProgramLength;
// Same for program length // Update parents data (collegeData)
const chosenProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
// Write them into parent's data
setData(prev => ({ setData(prev => ({
...prev, ...prev,
tuition: chosenTuition, tuition: chosenTuition, // match name used by parent or server
program_length: chosenProgramLength program_length: chosenProgramLength // match name used by parent
})); }));
// Then go to the next step in the parents wizard
nextStep(); nextStep();
}; };
@ -324,6 +327,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
// The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength) // The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength)
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength); const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
return ( return (
<div> <div>
<h2>College Details</h2> <h2>College Details</h2>

View File

@ -6,6 +6,7 @@ import FinancialOnboarding from './FinancialOnboarding.js';
import CollegeOnboarding from './CollegeOnboarding.js'; import CollegeOnboarding from './CollegeOnboarding.js';
import authFetch from '../../utils/authFetch.js'; import authFetch from '../../utils/authFetch.js';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import ReviewPage from './ReviewPage.js';
const OnboardingContainer = () => { const OnboardingContainer = () => {
console.log('OnboardingContainer MOUNT'); console.log('OnboardingContainer MOUNT');
@ -20,40 +21,54 @@ const OnboardingContainer = () => {
const nextStep = () => setStep(step + 1); const nextStep = () => setStep(step + 1);
const prevStep = () => setStep(step - 1); const prevStep = () => setStep(step - 1);
const submitData = async () => { console.log("Final collegeData in OnboardingContainer:", collegeData);
await authFetch('/api/premium/career-profile', {
// Now we do the final “all done” submission when the user finishes the last step
const handleFinalSubmit = async () => {
try {
// 1) POST career-profile
const careerRes = await authFetch('/api/premium/career-profile', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(careerData), body: JSON.stringify(careerData),
}); });
if (!careerRes.ok) throw new Error('Failed to save career profile');
const careerJson = await careerRes.json();
const { career_path_id } = careerJson;
if (!career_path_id) throw new Error('No career_path_id returned by server');
const mergedCollegeData = {
...collegeData,
// ensure this field isnt null
college_enrollment_status: careerData.college_enrollment_status,
career_path_id
};
await authFetch('/api/premium/financial-profile', { // 2) POST financial-profile
const financialRes = await authFetch('/api/premium/financial-profile', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(financialData), body: JSON.stringify(financialData),
}); });
if (!financialRes.ok) throw new Error('Failed to save financial profile');
await authFetch('/api/premium/college-profile', { // 3) POST college-profile (include career_path_id)
const mergedCollege = {
...collegeData,
college_enrollment_status: careerData.college_enrollment_status,
career_path_id };
const collegeRes = await authFetch('/api/premium/college-profile', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(collegeData), body: JSON.stringify(mergedCollege),
}); });
if (!collegeRes.ok) throw new Error('Failed to save college profile');
// Done => navigate away
navigate('/milestone-tracker'); navigate('/milestone-tracker');
}; } catch (err) {
console.error(err);
console.log('collegeData to submit:', collegeData); // (optionally show error to user)
}
useEffect(() => {
return () => console.log('OnboardingContainer UNMOUNT');
}, []);
// Merge the parent's collegeData with the override from careerData
const mergedCollegeData = {
...collegeData,
// If careerData has a truthy enrollment_status, override
college_enrollment_status:
careerData.college_enrollment_status ?? collegeData.college_enrollment_status
}; };
const onboardingSteps = [ const onboardingSteps = [
@ -76,13 +91,23 @@ const OnboardingContainer = () => {
/>, />,
<CollegeOnboarding <CollegeOnboarding
nextStep={submitData}
prevStep={prevStep} prevStep={prevStep}
nextStep={nextStep}
// Pass the merged data so that college_enrollment_status is never lost data={{
data={mergedCollegeData} ...collegeData,
// ensure we keep the enrollment status from career if that matters:
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
careerData={careerData}
financialData={financialData}
collegeData={collegeData}
onSubmit={handleFinalSubmit}
onBack={prevStep}
/>,
]; ];
return <div>{onboardingSteps[step]}</div>; return <div>{onboardingSteps[step]}</div>;

View File

@ -0,0 +1,31 @@
// ReviewPage.js
import React from 'react';
function ReviewPage({ careerData, financialData, collegeData, onSubmit, onBack }) {
console.log("REVIEW PAGE PROPS:", {
careerData,
financialData,
collegeData,
});
return (
<div>
<h2>Review Your Info</h2>
<h3>Career Info</h3>
<pre>{JSON.stringify(careerData, null, 2)}</pre>
<h3>Financial Info</h3>
<pre>{JSON.stringify(financialData, null, 2)}</pre>
<h3>College Info</h3>
<pre>{JSON.stringify(collegeData, null, 2)}</pre>
<button onClick={onBack}> Back</button>
<button onClick={onSubmit} style={{ marginLeft: '1rem' }}>
Submit All
</button>
</div>
);
}
export default ReviewPage;

View File

@ -127,6 +127,7 @@ export function simulateFinancialProjection(userProfile) {
for (let month = 0; month < maxMonths; month++) { for (let month = 0; month < maxMonths; month++) {
date.setMonth(date.getMonth() + 1); date.setMonth(date.getMonth() + 1);
// If loan is fully paid, record if not done already // If loan is fully paid, record if not done already
if (loanBalance <= 0 && !loanPaidOffMonth) { if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
@ -145,12 +146,14 @@ export function simulateFinancialProjection(userProfile) {
(date.getMonth() - simStart.getMonth()); (date.getMonth() - simStart.getMonth());
stillInCollege = (elapsedMonths < totalAcademicMonths); stillInCollege = (elapsedMonths < totalAcademicMonths);
} }
console.log(`MONTH ${month} start: inCollege=${stillInCollege}, loanBal=${loanBalance}`);
} }
// 6. If we pay lumps: check if this is a "lump" month within the user's academic year // 6. If we pay lumps: check if this is a "lump" month within the user's academic year
// We'll find how many academic years have passed since they started // We'll find how many academic years have passed since they started
let tuitionCostThisMonth = 0; let tuitionCostThisMonth = 0;
if (stillInCollege && lumpsPerYear > 0) { if (stillInCollege && lumpsPerYear > 0) {
const simStart = startDate ? new Date(startDate) : new Date(); const simStart = startDate ? new Date(startDate) : new Date();
const elapsedMonths = const elapsedMonths =
(date.getFullYear() - simStart.getFullYear()) * 12 + (date.getFullYear() - simStart.getFullYear()) * 12 +
@ -160,7 +163,7 @@ export function simulateFinancialProjection(userProfile) {
const academicYearIndex = Math.floor(elapsedMonths / 12); const academicYearIndex = Math.floor(elapsedMonths / 12);
// Within that year, which month are we in? (0..11) // Within that year, which month are we in? (0..11)
const monthInYear = elapsedMonths % 12; const monthInYear = elapsedMonths % 12;
console.log(" lumps logic check: academicYearIndex=", academicYearIndex, "monthInYear=", monthInYear);
// If we find monthInYear in lumpsSchedule, then lumps are due // If we find monthInYear in lumpsSchedule, then lumps are due
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) { if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
tuitionCostThisMonth = lumpAmount; tuitionCostThisMonth = lumpAmount;
@ -170,8 +173,10 @@ export function simulateFinancialProjection(userProfile) {
// 7. Decide if user defers or pays out of pocket // 7. Decide if user defers or pays out of pocket
// If deferring, add lumps to loan // If deferring, add lumps to loan
if (stillInCollege && loanDeferralUntilGraduation) { if (stillInCollege && loanDeferralUntilGraduation) {
console.log(" deferral is on, lumps => loan?");
// Instead of user paying out of pocket, add to loan // Instead of user paying out of pocket, add to loan
if (tuitionCostThisMonth > 0) { if (tuitionCostThisMonth > 0) {
console.log(" tuitionCostThisMonth=", tuitionCostThisMonth);
loanBalance += tuitionCostThisMonth; loanBalance += tuitionCostThisMonth;
tuitionCostThisMonth = 0; // paid by the loan tuitionCostThisMonth = 0; // paid by the loan
} }
@ -241,15 +246,13 @@ export function simulateFinancialProjection(userProfile) {
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution; const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions; const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
console.log(" end of month: loanBal=", loanBalance, " shortfall=", shortfall);
if (shortfall > 0) { if (shortfall > 0) {
// We can reduce from emergency savings console.log(" Breaking out - bankrupt scenario");
const canCover = Math.min(shortfall, currentEmergencySavings); const canCover = Math.min(shortfall, currentEmergencySavings);
currentEmergencySavings -= canCover; currentEmergencySavings -= canCover;
shortfall -= canCover; shortfall -= canCover;
if (shortfall > 0) { if (shortfall > 0) {
// user is effectively bankrupt
// we can break out or keep going to show negative net worth
// For demonstration, let's break
break; break;
} }
} }

Binary file not shown.