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 (