diff --git a/backend/server3.js b/backend/server3.js index 6ffb7ef..6509748 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -557,130 +557,178 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, COLLEGE PROFILES ------------------------------------------------------------------ */ -app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { - 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, + app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { + const { career_path_id, selected_school, selected_program, - program_type || null, - is_in_state ? 1 : 0, - is_in_district ? 1 : 0, - college_enrollment_status || null, - annual_financial_aid || 0, - is_online ? 1 : 0, - credit_hours_per_year || 0, - hours_completed || 0, - program_length || 0, - credit_hours_required || 0, - expected_graduation || null, - existing_college_debt || 0, - interest_rate || 0, - loan_term || 10, - loan_deferral_until_graduation ? 1 : 0, - extra_payment || 0, - expected_salary || 0, - academic_calendar || 'semester', - tuition || 0, - tuition_paid || 0 - ]); + 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 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({ - message: 'College profile saved.', - collegeProfileId: id - }); - } catch (error) { - console.error('Error saving college profile:', error); - 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 || {}); }); /* ------------------------------------------------------------------ diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index 58e2266..34e1ddd 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -12,12 +12,10 @@ import CareerSearch from './CareerSearch.js'; import MilestoneTimeline from './MilestoneTimeline.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; 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'; -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 location = useLocation(); @@ -28,24 +26,29 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const [existingCareerPaths, setExistingCareerPaths] = useState([]); const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [activeView, setActiveView] = useState("Career"); - const [financialProfile, setFinancialProfile] = useState(null); // Store the financial profile - const { - projectionData: initialProjectionData = [], - loanPayoffMonth: initialLoanPayoffMonth = null, - } = location.state || {}; - const [loanPayoffMonth, setLoanPayoffMonth] = useState(initialLoanPayoffMonth); - const [projectionData, setProjectionData] = useState(initialProjectionData); + + // Store each profile separately + const [financialProfile, setFinancialProfile] = useState(null); + const [collegeProfile, setCollegeProfile] = useState(null); + + // For the chart + const [projectionData, setProjectionData] = useState([]); + const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); 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(() => { const fetchCareerPaths = async () => { const res = await authFetch(`${apiURL}/premium/career-profile/all`); - if (!res) return; + if (!res || !res.ok) return; const data = await res.json(); - const { careerPaths } = data; - setExistingCareerPaths(careerPaths); + setExistingCareerPaths(data.careerPaths); const fromPopout = location.state?.selectedCareer; if (fromPopout) { @@ -53,7 +56,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { setCareerPathId(fromPopout.career_path_id); } else if (!selectedCareer) { const latest = await authFetch(`${apiURL}/premium/career-profile/latest`); - if (latest) { + if (latest && latest.ok) { const latestData = await latest.json(); if (latestData?.id) { setSelectedCareer(latestData); @@ -67,199 +70,245 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const res = await authFetch(`${apiURL}/premium/financial-profile`); if (res && res.ok) { const data = await res.json(); - setFinancialProfile(data); // Set the financial profile in state + setFinancialProfile(data); } }; fetchCareerPaths(); fetchFinancialProfile(); - }, []); + }, [apiURL, location.state, selectedCareer]); + // ---------------------------- + // 2. Fetch the college profile for the selected careerPathId + // ---------------------------- useEffect(() => { - if (financialProfile && selectedCareer) { - const { projectionData, loanPaidOffMonth, emergencySavings } = simulateFinancialProjection({ - currentSalary: financialProfile.current_salary, - 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); - } + if (!careerPathId) { + setCollegeProfile(null); + return; } - }, [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) => { - if (selected && selected.id && selected.career_name) { - setSelectedCareer(selected); - setCareerPathId(selected.id); - } else { - console.warn('Invalid career object received in handleCareerChange:', selected); + // Merge financial + college data + const mergedProfile = { + // From financialProfile + currentSalary: financialProfile.current_salary || 0, + monthlyExpenses: financialProfile.monthly_expenses || 0, + 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( 'First 5 items of projectionData:', 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 (
- { + setSelectedCareer(selected); + setCareerPathId(selected?.id || null); + }} loading={!existingCareerPaths.length} authFetch={authFetch} /> - - {console.log('Passing careerPathId to MilestoneTimeline:', careerPathId)} + - + - {projectionData && ( -
-

Financial Projection

- p.month), - datasets: [ - { - label: 'Total Savings', // ✅ Changed label to clarify - data: projectionData.map(p => p.cumulativeNetSavings), - borderColor: 'rgba(54, 162, 235, 1)', - backgroundColor: 'rgba(54, 162, 235, 0.2)', - tension: 0.4, - fill: true - }, - { - label: 'Loan Balance', - data: projectionData.map(p => p.loanBalance), - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - tension: 0.4, - fill: { - target: 'origin', - above: 'rgba(255,99,132,0.3)', // loan debt - below: 'transparent' // don't show below 0 - } - }, - { - 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 + {projectionData.length > 0 && ( +
+

Financial Projection

+ p.month), + datasets: [ + { + label: 'Total Savings', + data: projectionData.map(p => p.cumulativeNetSavings), + borderColor: 'rgba(54, 162, 235, 1)', + backgroundColor: 'rgba(54, 162, 235, 0.2)', + tension: 0.4, + fill: true, + }, + { + label: 'Loan Balance', + data: projectionData.map(p => p.loanBalance), + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + tension: 0.4, + fill: { + target: 'origin', + above: 'rgba(255,99,132,0.3)', + below: 'transparent' } - ] - }} - options={{ - responsive: true, - plugins: { - legend: { position: 'bottom' }, - tooltip: { mode: 'index', intersect: false }, - annotation: loanPayoffMonth - ? { - annotations: { - loanPaidOffLine: { - type: 'line', - xMin: loanPayoffMonth, - xMax: loanPayoffMonth, - borderColor: 'rgba(255, 206, 86, 1)', - borderWidth: 2, - borderDash: [6, 6], - label: { - display: true, - content: 'Loan Paid Off', - position: 'end', - backgroundColor: 'rgba(255, 206, 86, 0.8)', - color: '#000', - font: { - size: 12 - }, - rotation: 0, - yAdjust: -10 - } + }, + { + label: 'Retirement Savings', + data: projectionData.map(p => p.retirementSavings), + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + tension: 0.4, + fill: true + } + ] + }} + options={{ + responsive: true, + plugins: { + legend: { position: 'bottom' }, + tooltip: { mode: 'index', intersect: false }, + annotation: loanPayoffMonth + ? { + annotations: { + loanPaidOffLine: { + type: 'line', + xMin: loanPayoffMonth, + xMax: loanPayoffMonth, + borderColor: 'rgba(255, 206, 86, 1)', + borderWidth: 2, + borderDash: [6, 6], + label: { + 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()}` } } - }} - /> -
- )} - - +
+ )} + + setPendingCareerForModal(careerName)} setPendingCareerForModal={setPendingCareerForModal} authFetch={authFetch} /> {pendingCareerForModal && ( - )} @@ -267,4 +316,4 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { ); }; -export default MilestoneTracker; \ No newline at end of file +export default MilestoneTracker; diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js index 3f73703..c21f639 100644 --- a/src/components/PremiumOnboarding/CareerOnboarding.js +++ b/src/components/PremiumOnboarding/CareerOnboarding.js @@ -90,18 +90,23 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => { alert("Please complete all required fields before continuing."); return; } - + const isInCollege = ( + collegeEnrollmentStatus === 'currently_enrolled' || + collegeEnrollmentStatus === 'prospective_student' + ); + setData(prevData => ({ ...prevData, career_name: selectedCareer, college_enrollment_status: collegeEnrollmentStatus, currently_working: currentlyWorking, + inCollege: isInCollege, status: prevData.status || 'planned', start_date: prevData.start_date || new Date().toISOString(), projected_end_date: prevData.projected_end_date || null, user_id: userId })); - + nextStep(); }; diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js index 02c0e4b..109d521 100644 --- a/src/components/PremiumOnboarding/CollegeOnboarding.js +++ b/src/components/PremiumOnboarding/CollegeOnboarding.js @@ -1,6 +1,8 @@ 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) const [schoolData, setSchoolData] = useState([]); const [icTuitionData, setIcTuitionData] = useState([]); @@ -301,20 +303,21 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) { // handleSubmit => merges final chosen values // ------------------------------------------ const handleSubmit = () => { - // If user typed a manual value, we use that. If they left it blank, - // we use the autoTuition. - const chosenTuition = (manualTuition.trim() === '' ? autoTuition : parseFloat(manualTuition)); + const chosenTuition = manualTuition.trim() === '' + ? autoTuition + : parseFloat(manualTuition); + const chosenProgramLength = manualProgramLength.trim() === '' + ? autoProgramLength + : manualProgramLength; - // Same for program length - const chosenProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength); - - // Write them into parent's data + // Update parent’s data (collegeData) setData(prev => ({ ...prev, - tuition: chosenTuition, - program_length: chosenProgramLength + tuition: chosenTuition, // match name used by parent or server + program_length: chosenProgramLength // match name used by parent })); + // Then go to the next step in the parent’s wizard nextStep(); }; @@ -324,6 +327,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) { // The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength) const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength); + return (

College Details

diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js index 1491867..b87353f 100644 --- a/src/components/PremiumOnboarding/OnboardingContainer.js +++ b/src/components/PremiumOnboarding/OnboardingContainer.js @@ -6,6 +6,7 @@ import FinancialOnboarding from './FinancialOnboarding.js'; import CollegeOnboarding from './CollegeOnboarding.js'; import authFetch from '../../utils/authFetch.js'; import { useNavigate } from 'react-router-dom'; +import ReviewPage from './ReviewPage.js'; const OnboardingContainer = () => { console.log('OnboardingContainer MOUNT'); @@ -20,40 +21,54 @@ const OnboardingContainer = () => { const nextStep = () => setStep(step + 1); const prevStep = () => setStep(step - 1); - const submitData = async () => { - await authFetch('/api/premium/career-profile', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(careerData), - }); + console.log("Final collegeData in OnboardingContainer:", collegeData); - await authFetch('/api/premium/financial-profile', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(financialData), - }); + // 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', + 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 isn’t null + college_enrollment_status: careerData.college_enrollment_status, + career_path_id + }; - await authFetch('/api/premium/college-profile', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(collegeData), - }); + // 2) POST financial-profile + const financialRes = await authFetch('/api/premium/financial-profile', { + method: 'POST', + 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); - - 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 + // Done => navigate away + navigate('/milestone-tracker'); + } catch (err) { + console.error(err); + // (optionally show error to user) + } }; const onboardingSteps = [ @@ -76,13 +91,23 @@ const OnboardingContainer = () => { />, + />, + // Add a final "Review & Submit" step or just automatically call handleFinalSubmit on step 4 + , ]; return
{onboardingSteps[step]}
; diff --git a/src/components/PremiumOnboarding/ReviewPage.js b/src/components/PremiumOnboarding/ReviewPage.js new file mode 100644 index 0000000..1ac5bc7 --- /dev/null +++ b/src/components/PremiumOnboarding/ReviewPage.js @@ -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 ( +
+

Review Your Info

+ +

Career Info

+
{JSON.stringify(careerData, null, 2)}
+ +

Financial Info

+
{JSON.stringify(financialData, null, 2)}
+ +

College Info

+
{JSON.stringify(collegeData, null, 2)}
+ + + +
+ ); +} + +export default ReviewPage; diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js index bff7ef0..0295cc0 100644 --- a/src/utils/FinancialProjectionService.js +++ b/src/utils/FinancialProjectionService.js @@ -126,6 +126,7 @@ export function simulateFinancialProjection(userProfile) { for (let month = 0; month < maxMonths; month++) { date.setMonth(date.getMonth() + 1); + // If loan is fully paid, record if not done already if (loanBalance <= 0 && !loanPaidOffMonth) { @@ -145,12 +146,14 @@ export function simulateFinancialProjection(userProfile) { (date.getMonth() - simStart.getMonth()); 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 // We'll find how many academic years have passed since they started let tuitionCostThisMonth = 0; if (stillInCollege && lumpsPerYear > 0) { + const simStart = startDate ? new Date(startDate) : new Date(); const elapsedMonths = (date.getFullYear() - simStart.getFullYear()) * 12 + @@ -160,7 +163,7 @@ export function simulateFinancialProjection(userProfile) { const academicYearIndex = Math.floor(elapsedMonths / 12); // Within that year, which month are we in? (0..11) const monthInYear = elapsedMonths % 12; - + console.log(" lumps logic check: academicYearIndex=", academicYearIndex, "monthInYear=", monthInYear); // If we find monthInYear in lumpsSchedule, then lumps are due if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) { tuitionCostThisMonth = lumpAmount; @@ -170,8 +173,10 @@ export function simulateFinancialProjection(userProfile) { // 7. Decide if user defers or pays out of pocket // If deferring, add lumps to loan if (stillInCollege && loanDeferralUntilGraduation) { + console.log(" deferral is on, lumps => loan?"); // Instead of user paying out of pocket, add to loan if (tuitionCostThisMonth > 0) { + console.log(" tuitionCostThisMonth=", tuitionCostThisMonth); loanBalance += tuitionCostThisMonth; tuitionCostThisMonth = 0; // paid by the loan } @@ -241,15 +246,13 @@ export function simulateFinancialProjection(userProfile) { const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution; const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions; let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay + console.log(" end of month: loanBal=", loanBalance, " shortfall=", shortfall); if (shortfall > 0) { - // We can reduce from emergency savings + console.log(" Breaking out - bankrupt scenario"); const canCover = Math.min(shortfall, currentEmergencySavings); currentEmergencySavings -= canCover; shortfall -= canCover; 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; } } diff --git a/user_profile.db b/user_profile.db index 15489de..a53b042 100644 Binary files a/user_profile.db and b/user_profile.db differ