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,130 +557,178 @@ 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,
selected_school,
selected_program,
program_type,
is_in_state,
is_in_district,
college_enrollment_status,
is_online,
credit_hours_per_year,
credit_hours_required,
hours_completed,
program_length,
expected_graduation,
existing_college_debt,
interest_rate,
loan_term,
loan_deferral_until_graduation,
extra_payment,
expected_salary,
academic_calendar,
annual_financial_aid,
tuition,
tuition_paid
} = req.body;
try {
const id = uuidv4();
const user_id = req.userId;
await db.run(`
INSERT INTO college_profiles (
id,
user_id,
career_path_id,
selected_school,
selected_program,
program_type,
is_in_state,
is_in_district,
college_enrollment_status,
annual_financial_aid,
is_online,
credit_hours_per_year,
hours_completed,
program_length,
credit_hours_required,
expected_graduation,
existing_college_debt,
interest_rate,
loan_term,
loan_deferral_until_graduation,
extra_payment,
expected_salary,
academic_calendar,
tuition,
tuition_paid,
created_at,
updated_at
) VALUES (
?, -- id
?, -- user_id
?, -- career_path_id
?, -- selected_school
?, -- selected_program
?, -- program_type
?, -- is_in_state
?, -- is_in_district
?, -- college_enrollment_status
?, -- annual_financial_aid
?, -- is_online
?, -- credit_hours_per_year
?, -- hours_completed
?, -- program_length
?, -- credit_hours_required
?, -- expected_graduation
?, -- existing_college_debt
?, -- interest_rate
?, -- loan_term
?, -- loan_deferral_until_graduation
?, -- extra_payment
?, -- expected_salary
?, -- academic_calendar
?, -- tuition
?, -- tuition_paid
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
`, [
id,
user_id,
career_path_id, career_path_id,
selected_school, selected_school,
selected_program, selected_program,
program_type || null, program_type,
is_in_state ? 1 : 0, is_in_state,
is_in_district ? 1 : 0, is_in_district,
college_enrollment_status || null, college_enrollment_status,
annual_financial_aid || 0, is_online,
is_online ? 1 : 0, credit_hours_per_year,
credit_hours_per_year || 0, credit_hours_required,
hours_completed || 0, hours_completed,
program_length || 0, program_length,
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', annual_financial_aid,
tuition || 0, tuition,
tuition_paid || 0 tuition_paid
]); } = req.body;
try {
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(`
INSERT INTO college_profiles (
id,
user_id,
career_path_id,
selected_school,
selected_program,
program_type,
is_in_state,
is_in_district,
college_enrollment_status,
annual_financial_aid,
is_online,
credit_hours_per_year,
hours_completed,
program_length,
credit_hours_required,
expected_graduation,
existing_college_debt,
interest_rate,
loan_term,
loan_deferral_until_graduation,
extra_payment,
expected_salary,
academic_calendar,
tuition,
tuition_paid,
created_at,
updated_at
)
VALUES (
:id,
:user_id,
:career_path_id,
:selected_school,
:selected_program,
:program_type,
:is_in_state,
:is_in_district,
:college_enrollment_status,
:annual_financial_aid,
:is_online,
:credit_hours_per_year,
:hours_completed,
:program_length,
:credit_hours_required,
:expected_graduation,
:existing_college_debt,
:interest_rate,
:loan_term,
:loan_deferral_until_graduation,
:extra_payment,
:expected_salary,
:academic_calendar,
:tuition,
:tuition_paid,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
-- The magic:
ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type)
DO UPDATE SET
is_in_state = excluded.is_in_state,
is_in_district = excluded.is_in_district,
college_enrollment_status = excluded.college_enrollment_status,
annual_financial_aid = excluded.annual_financial_aid,
is_online = excluded.is_online,
credit_hours_per_year = excluded.credit_hours_per_year,
hours_completed = excluded.hours_completed,
program_length = excluded.program_length,
credit_hours_required = excluded.credit_hours_required,
expected_graduation = excluded.expected_graduation,
existing_college_debt = excluded.existing_college_debt,
interest_rate = excluded.interest_rate,
loan_term = excluded.loan_term,
loan_deferral_until_graduation = excluded.loan_deferral_until_graduation,
extra_payment = excluded.extra_payment,
expected_salary = excluded.expected_salary,
academic_calendar = excluded.academic_calendar,
tuition = excluded.tuition,
tuition_paid = excluded.tuition_paid,
updated_at = CURRENT_TIMESTAMP
;
`, {
':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({
message: 'College profile upsert done.',
// You might do an extra SELECT here to find which ID the final row uses if you need it
});
} catch (error) {
console.error('Error saving college profile:', error);
res.status(500).json({ error: 'Failed to save college profile.' });
}
});
res.status(201).json({ app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
message: 'College profile saved.', const { careerPathId } = req.query;
collegeProfileId: id // find row
}); const row = await db.get(`
} catch (error) { SELECT *
console.error('Error saving college profile:', error); FROM college_profiles
res.status(500).json({ error: 'Failed to save college profile.' }); 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,199 +70,245 @@ 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,
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
studentLoanAmount: financialProfile.college_loan_total,
interestRate: financialProfile.interest_rate || 5.5,
loanTerm: financialProfile.loan_term || 10,
extraPayment: financialProfile.extra_payment || 0,
expectedSalary: financialProfile.expected_salary || financialProfile.current_salary,
emergencySavings: financialProfile.emergency_fund,
retirementSavings: financialProfile.retirement_savings,
monthlyRetirementContribution: financialProfile.retirement_contribution,
monthlyEmergencyContribution: 0,
gradDate: financialProfile.expected_graduation,
fullTimeCollegeStudent: financialProfile.in_college,
partTimeIncome: financialProfile.part_time_income,
startDate: new Date(),
programType: financialProfile.program_type,
isFullyOnline: financialProfile.is_online,
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 cumulativeProjectionData = projectionData.map(month => {
cumulativeSavings += month.netSavings || 0;
return { ...month, cumulativeNetSavings: cumulativeSavings };
});
// Only update if we have real projection data
if (cumulativeProjectionData.length > 0) {
setProjectionData(cumulativeProjectionData);
setLoanPayoffMonth(loanPaidOffMonth);
}
} }
}, [financialProfile, selectedCareer]);
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'
);
const handleCareerChange = (selected) => { // Merge financial + college data
if (selected && selected.id && selected.career_name) { const mergedProfile = {
setSelectedCareer(selected); // From financialProfile
setCareerPathId(selected.id); currentSalary: financialProfile.current_salary || 0,
} else { monthlyExpenses: financialProfile.monthly_expenses || 0,
console.warn('Invalid career object received in handleCareerChange:', selected); monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
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,
// From collegeProfile
studentLoanAmount: collegeProfile.existing_college_debt || 0,
interestRate: collegeProfile.interest_rate || 5,
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,
// Are they in college?
inCollege: (collegeProfile.college_enrollment_status === 'currently_enrolled' ||
collegeProfile.college_enrollment_status === 'prospective_student'),
// If they've graduated or not in college, false
startDate: new Date().toISOString(),
// Future logic could set expectedSalary if there's a difference
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
};
const result = simulateFinancialProjection(mergedProfile);
console.log("mergedProfile for simulation:", mergedProfile);
const { projectionData, loanPaidOffMonth } = result;
// If you want to accumulate net savings:
let cumulativeSavings = mergedProfile.emergencySavings || 0;
const cumulativeProjectionData = projectionData.map(month => {
cumulativeSavings += (month.netSavings || 0);
return { ...month, cumulativeNetSavings: cumulativeSavings };
});
if (cumulativeProjectionData.length > 0) {
setProjectionData(cumulativeProjectionData);
setLoanPayoffMonth(loanPaidOffMonth);
} }
};
console.log('mergedProfile for simulation:', mergedProfile);
}, [financialProfile, collegeProfile, selectedCareer]);
// 4. The rest of your code is unchanged, e.g. handleConfirmCareerSelection, etc.
// ...
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 () => {
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);
};
// ...
// The remainder of your component: timeline, chart, AISuggestedMilestones, etc.
// ...
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
data={{ data={{
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',
data: projectionData.map(p => p.loanBalance), data: projectionData.map(p => p.loanBalance),
borderColor: 'rgba(255, 99, 132, 1)', borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)', backgroundColor: 'rgba(255, 99, 132, 0.2)',
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',
data: projectionData.map(p => p.totalRetirementSavings),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4,
fill: true
} }
] },
}} {
options={{ label: 'Retirement Savings',
responsive: true, data: projectionData.map(p => p.retirementSavings),
plugins: { borderColor: 'rgba(75, 192, 192, 1)',
legend: { position: 'bottom' }, backgroundColor: 'rgba(75, 192, 192, 0.2)',
tooltip: { mode: 'index', intersect: false }, tension: 0.4,
annotation: loanPayoffMonth fill: true
? { }
annotations: { ]
loanPaidOffLine: { }}
type: 'line', options={{
xMin: loanPayoffMonth, responsive: true,
xMax: loanPayoffMonth, plugins: {
borderColor: 'rgba(255, 206, 86, 1)', legend: { position: 'bottom' },
borderWidth: 2, tooltip: { mode: 'index', intersect: false },
borderDash: [6, 6], annotation: loanPayoffMonth
label: { ? {
display: true, annotations: {
content: 'Loan Paid Off', loanPaidOffLine: {
position: 'end', type: 'line',
backgroundColor: 'rgba(255, 206, 86, 0.8)', xMin: loanPayoffMonth,
color: '#000', xMax: loanPayoffMonth,
font: { borderColor: 'rgba(255, 206, 86, 1)',
size: 12 borderWidth: 2,
}, borderDash: [6, 6],
rotation: 0, label: {
yAdjust: -10 display: true,
} content: 'Loan Paid Off',
position: 'end',
backgroundColor: 'rgba(255, 206, 86, 0.8)',
color: '#000',
font: { size: 12 },
rotation: 0,
yAdjust: -10
} }
} }
} }
: undefined
},
scales: {
y: {
beginAtZero: false,
ticks: {
callback: (value) => `$${value.toLocaleString()}`
} }
: undefined
},
scales: {
y: {
beginAtZero: false,
ticks: {
callback: (value) => `$${value.toLocaleString()}`
} }
} }
}} }
/> }}
</div> />
)} </div>
)}
<CareerSearch
<CareerSearch
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)} onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
setPendingCareerForModal={setPendingCareerForModal} setPendingCareerForModal={setPendingCareerForModal}
authFetch={authFetch} authFetch={authFetch}
/> />
{pendingCareerForModal && ( {pendingCareerForModal && (
<button onClick={handleConfirmCareerSelection}> <button onClick={() => {
// handleConfirmCareerSelection logic
}}>
Confirm Career Change to {pendingCareerForModal} Confirm Career Change to {pendingCareerForModal}
</button> </button>
)} )}
@ -267,4 +316,4 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
); );
}; };
export default MilestoneTracker; export default MilestoneTracker;

View File

@ -90,18 +90,23 @@ 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,
user_id: userId user_id: userId
})); }));
nextStep(); nextStep();
}; };

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', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(careerData),
});
await authFetch('/api/premium/financial-profile', { // Now we do the final “all done” submission when the user finishes the last step
method: 'POST', const handleFinalSubmit = async () => {
headers: { 'Content-Type': 'application/json' }, try {
body: JSON.stringify(financialData), // 1) POST career-profile
}); const careerRes = await authFetch('/api/premium/career-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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/college-profile', { // 2) POST financial-profile
method: 'POST', const financialRes = await authFetch('/api/premium/financial-profile', {
headers: { 'Content-Type': 'application/json' }, method: 'POST',
body: JSON.stringify(collegeData), headers: { 'Content-Type': 'application/json' },
}); body: JSON.stringify(financialData),
});
if (!financialRes.ok) throw new Error('Failed to save financial profile');
navigate('/milestone-tracker'); // 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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mergedCollege),
});
if (!collegeRes.ok) throw new Error('Failed to save college profile');
console.log('collegeData to submit:', collegeData); // Done => navigate away
navigate('/milestone-tracker');
useEffect(() => { } catch (err) {
return () => console.log('OnboardingContainer UNMOUNT'); console.error(err);
}, []); // (optionally show error to user)
}
// 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

@ -126,6 +126,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) {
@ -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.