From fa26c4a31bdec1ba06f7d08cf33d7ed73d2af863 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 7 Apr 2025 13:36:58 +0000 Subject: [PATCH] Projection fixes, and added sample Paywall.js --- backend/server3.js | 119 ++++++++----- src/components/AISuggestedMilestones.js | 46 ++++- src/components/FinancialProfileForm.js | 213 ++++++++++++++++++++---- src/components/MilestoneTracker.js | 92 +++++----- src/components/Paywall.js | 28 ++++ src/utils/FinancialProjectionService.js | 129 ++++++++------ user_profile.db | Bin 57344 -> 57344 bytes 7 files changed, 448 insertions(+), 179 deletions(-) create mode 100644 src/components/Paywall.js diff --git a/backend/server3.js b/backend/server3.js index 34d2a4b..7a3d478 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -9,6 +9,8 @@ import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import { fileURLToPath } from 'url'; +import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js'; + @@ -373,74 +375,104 @@ app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, r } }); +// Backend code (server3.js) + app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { const { currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, retirementSavings, retirementContribution, emergencyFund, inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, - careerPathId // ✅ Required to run simulation + selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, + careerPathId, loanDeferralUntilGraduation, tuition, programLength, interestRate, loanTerm, extraPayment, expectedSalary } = req.body; try { + // **Call the simulateFinancialProjection function here** with all the incoming data + const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({ + currentSalary: req.body.currentSalary + (req.body.additionalIncome || 0), + monthlyExpenses: req.body.monthlyExpenses, + monthlyDebtPayments: req.body.monthlyDebtPayments || 0, + studentLoanAmount: req.body.collegeLoanTotal, + + // ✅ UPDATED to dynamic fields from frontend + interestRate: req.body.interestRate, + loanTerm: req.body.loanTerm, + extraPayment: req.body.extraPayment || 0, + expectedSalary: req.body.expectedSalary, + + emergencySavings: req.body.emergencyFund, + retirementSavings: req.body.retirementSavings, + monthlyRetirementContribution: req.body.retirementContribution, + monthlyEmergencyContribution: 0, + gradDate: req.body.expectedGraduation, + fullTimeCollegeStudent: req.body.inCollege, + partTimeIncome: req.body.partTimeIncome, + startDate: new Date(), + programType: req.body.programType, + isFullyOnline: req.body.isFullyOnline, + creditHoursPerYear: req.body.creditHoursPerYear, + calculatedTuition: req.body.tuition, + manualTuition: 0, + hoursCompleted: req.body.hoursCompleted, + loanDeferralUntilGraduation: req.body.loanDeferralUntilGraduation, + programLength: req.body.programLength + }); + // Now you can save the profile or update the database with the new data const existing = await db.get(`SELECT id FROM financial_profile WHERE user_id = ?`, [req.userId]); if (existing) { + // Updating existing profile await db.run(` UPDATE financial_profile SET current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?, retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?, in_college = ?, expected_graduation = ?, part_time_income = ?, tuition_paid = ?, college_loan_total = ?, + selected_school = ?, selected_program = ?, program_type = ?, is_online = ?, credit_hours_per_year = ?, hours_completed = ?, + tuition = ?, loan_deferral_until_graduation = ?, program_length = ?, + interest_rate = ?, loan_term = ?, extra_payment = ?, expected_salary = ?, updated_at = CURRENT_TIMESTAMP - WHERE user_id = ? - `, [ - currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, - retirementSavings, retirementContribution, emergencyFund, - inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, - req.userId - ]); + WHERE user_id = ?`, + [ + currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, + retirementSavings, retirementContribution, emergencyFund, + inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, + selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, + tuition, loanDeferralUntilGraduation, programLength, + interestRate, loanTerm, extraPayment, expectedSalary, // ✅ added new fields + req.userId + ] + ); } else { + // Insert a new profile await db.run(` INSERT INTO financial_profile ( id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation, - part_time_income, tuition_paid, college_loan_total - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, - retirementSavings, retirementContribution, emergencyFund, - inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal - ]); + part_time_income, tuition_paid, college_loan_total, selected_school, selected_program, program_type, + is_online, credit_hours_per_year, calculated_tuition, loan_deferral_until_graduation, hours_completed, tuition, program_length, + interest_rate, loan_term, extra_payment, expected_salary + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, + retirementSavings, retirementContribution, emergencyFund, + inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, + selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, + tuition, loanDeferralUntilGraduation, programLength, + interestRate, loanTerm, extraPayment, expectedSalary // ✅ added new fields + ] + ); + } - // ✅ Run projection only if careerPathId is provided - if (!careerPathId) { - return res.status(200).json({ message: 'Financial profile saved. No projection generated.' }); - } - - const milestones = await db.all( - `SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`, - [req.userId, careerPathId] - ); - - const projectionData = simulateFinancialProjection({ - currentSalary, - additionalIncome, - monthlyExpenses, - monthlyDebtPayments, - retirementSavings, - retirementContribution, - emergencySavings: emergencyFund, - studentLoanAmount: collegeLoanTotal, - studentLoanAPR: 5.5, // placeholder default, can be user-supplied later - loanTermYears: 10, // placeholder default, can be user-supplied later - milestones, - gradDate: expectedGraduation, - fullTimeCollegeStudent: !!inCollege, - partTimeIncome, - startDate: moment() + // Return the financial simulation results (calculated projection data) to the frontend + res.status(200).json({ + message: 'Financial profile saved.', + projectionData, + loanPaidOffMonth, + emergencyFund: emergencyFund // explicitly add the emergency fund here }); - - return res.status(200).json({ message: 'Financial profile saved.', projectionData }); + + console.log("Request body:", req.body); } catch (error) { console.error('Error saving financial profile:', error); @@ -450,6 +482,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, + // Retrieve career history app.get('/api/premium/career-history', authenticatePremiumUser, async (req, res) => { try { diff --git a/src/components/AISuggestedMilestones.js b/src/components/AISuggestedMilestones.js index 3284e8f..63a4f34 100644 --- a/src/components/AISuggestedMilestones.js +++ b/src/components/AISuggestedMilestones.js @@ -18,14 +18,46 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active useEffect(() => { - if (!career) return; - setSuggestedMilestones([ - { title: `Entry-Level ${career}`, date: '2025-06-01', progress: 0 }, - { title: `Mid-Level ${career}`, date: '2027-01-01', progress: 0 }, - { title: `Senior-Level ${career}`, date: '2030-01-01', progress: 0 }, - ]); + if (!career || !Array.isArray(projectionData)) return; + + // Dynamically suggest milestones based on projection data + const suggested = []; + + // Find salary or savings growth points from projectionData: + projectionData.forEach((monthData, index) => { + if (index === 0) return; // Skip first month for comparison + const prevMonth = projectionData[index - 1]; + + // Example logic: suggest milestones when retirement savings hit certain thresholds + if (monthData.totalRetirementSavings >= 50000 && prevMonth.totalRetirementSavings < 50000) { + suggested.push({ + title: `Reach $50k Retirement Savings`, + date: monthData.month + '-01', + progress: 0, + }); + } + + // Milestone when loan is paid off + if (monthData.loanBalance <= 0 && prevMonth.loanBalance > 0) { + suggested.push({ + title: `Student Loans Paid Off`, + date: monthData.month + '-01', + progress: 0, + }); + } + }); + + // Career-based suggestions still possible (add explicitly if desired) + suggested.push( + { title: `Entry-Level ${career}`, date: projectionData[6]?.month + '-01' || '2025-06-01', progress: 0 }, + { title: `Mid-Level ${career}`, date: projectionData[24]?.month + '-01' || '2027-01-01', progress: 0 }, + { title: `Senior-Level ${career}`, date: projectionData[60]?.month + '-01' || '2030-01-01', progress: 0 } + ); + + setSuggestedMilestones(suggested); setSelected([]); - }, [career]); + }, [career, projectionData]); + const toggleSelect = (index) => { setSelected(prev => diff --git a/src/components/FinancialProfileForm.js b/src/components/FinancialProfileForm.js index 860af5e..5c9281d 100644 --- a/src/components/FinancialProfileForm.js +++ b/src/components/FinancialProfileForm.js @@ -30,6 +30,16 @@ function FinancialProfileForm() { const [selectedSchool, setSelectedSchool] = useState(""); const [selectedProgram, setSelectedProgram] = useState(""); const [manualTuition, setManualTuition] = useState(""); + const [hoursCompleted, setHoursCompleted] = useState(""); + const [creditHoursRequired, setCreditHoursRequired] = useState(""); // New field for required credit hours + const [programLength, setProgramLength] = useState(0); + const [projectionData, setProjectionData] = useState(null); + const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); + + const [interestRate, setInterestRate] = useState(5.5); + const [loanTerm, setLoanTerm] = useState(10); + const [extraPayment, setExtraPayment] = useState(0); + const [expectedSalary, setExpectedSalary] = useState(0); const [schoolData, setSchoolData] = useState([]); const [schoolSuggestions, setSchoolSuggestions] = useState([]); @@ -38,6 +48,7 @@ function FinancialProfileForm() { const [icTuitionData, setIcTuitionData] = useState([]); const [calculatedTuition, setCalculatedTuition] = useState(0); const [selectedSchoolUnitId, setSelectedSchoolUnitId] = useState(null); + const [loanDeferralUntilGraduation, setLoanDeferralUntilGraduation] = useState(false); useEffect(() => { async function fetchRawTuitionData() { @@ -60,18 +71,6 @@ function FinancialProfileForm() { } }, [selectedSchool, schoolData]); - useEffect(() => { - async function fetchRawTuitionData() { - const res = await fetch("/ic2023_ay.csv"); - const text = await res.text(); - const rows = text.split("\n").map(line => line.split(',')); - const headers = rows[0]; - const data = rows.slice(1).map(row => Object.fromEntries(row.map((val, idx) => [headers[idx], val]))); - setIcTuitionData(data); - } - fetchRawTuitionData(); - }, []); - useEffect(() => { if (selectedSchool && programType && creditHoursPerYear && icTuitionData.length > 0) { // Find the selected school from tuition data @@ -137,6 +136,7 @@ function FinancialProfileForm() { headers: { "Authorization": `Bearer ${localStorage.getItem('token')}` } }); + if (res.ok) { const data = await res.json(); if (data && Object.keys(data).length > 0) { @@ -155,10 +155,15 @@ function FinancialProfileForm() { setExistingCollegeDebt(data.existing_college_debt || ""); setCreditHoursPerYear(data.credit_hours_per_year || ""); setProgramType(data.program_type || ""); - setIsFullyOnline(!!data.is_fully_online); + setIsFullyOnline(!!data.is_online); // Correct the name to 'is_online' setSelectedSchool(data.selected_school || ""); setSelectedProgram(data.selected_program || ""); - + setHoursCompleted(data.hours_completed || ""); + setLoanDeferralUntilGraduation(!!data.loan_deferral_until_graduation); + setInterestRate(data.interest_rate||""); + setLoanTerm(data.loan_term || ""); + setExtraPayment(data.extra_payment || 0); + setExpectedSalary(data.expected_salary || ""); } } } catch (err) { @@ -170,16 +175,15 @@ function FinancialProfileForm() { }, [userId]); useEffect(() => { - if (selectedSchool && schoolData.length > 0) { - // Filter programs for the selected school and display them as suggestions + if (selectedSchool && schoolData.length > 0 && !selectedProgram) { const programs = schoolData .filter(s => s.INSTNM.toLowerCase() === selectedSchool.toLowerCase()) .map(s => s.CIPDESC); - // Filter unique programs and show the first 10 setProgramSuggestions([...new Set(programs)].slice(0, 10)); } - }, [selectedSchool, schoolData]); + }, [selectedSchool, schoolData, selectedProgram]); + useEffect(() => { if (selectedProgram && selectedSchool && schoolData.length > 0) { @@ -220,6 +224,16 @@ function FinancialProfileForm() { setProgramSuggestions([]); }; + const handleProgramSelect = (suggestion) => { + setSelectedProgram(suggestion); + setProgramSuggestions([]); // Explicitly clear suggestions + const filteredTypes = schoolData.filter(s => + s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() && + s.CIPDESC === suggestion + ).map(s => s.CREDDESC); + setAvailableProgramTypes([...new Set(filteredTypes)]); + }; + const handleProgramChange = (e) => { const value = e.target.value; setSelectedProgram(value); @@ -244,14 +258,62 @@ function FinancialProfileForm() { setAvailableProgramTypes([...new Set(filteredTypes)]); }; + const calculateProgramLength = () => { + let requiredCreditHours = 0; + // Default credit hours per degree + switch (programType) { + case "Associate's Degree": + requiredCreditHours = 60; + break; + case "Bachelor's Degree": + requiredCreditHours = 120; + break; + case "Master's Degree": + requiredCreditHours = 60; + break; + case "Doctoral Degree": + requiredCreditHours = 120; + break; + case "First Professional Degree": + requiredCreditHours = 180; // Typically for professional programs + break; + case "Graduate/Professional Certificate": + requiredCreditHours = parseInt(creditHoursRequired, 10); // User provided input + break; + default: + requiredCreditHours = parseInt(creditHoursRequired, 10); // For other cases + } + + // Deduct completed hours and calculate program length + const remainingCreditHours = requiredCreditHours - parseInt(hoursCompleted, 10); + const calculatedProgramLength = (remainingCreditHours / creditHoursPerYear).toFixed(2); + + setProgramLength(calculatedProgramLength); + }; + + useEffect(() => { + if (programType && hoursCompleted && creditHoursPerYear) { + calculateProgramLength(); // Recalculate when the program type, completed hours, or credit hours per year change + } + }, [programType, hoursCompleted, creditHoursPerYear]); + const handleProgramTypeSelect = (e) => { setProgramType(e.target.value); + setCreditHoursRequired(""); // Reset if the user changes program type + setProgramLength(""); // Recalculate when the program type changes }; const handleTuitionInput = (e) => { setManualTuition(e.target.value); }; + const handleCreditHoursRequired = (e) => { + const value = parseFloat(e.target.value); // Ensure it's parsed as a number + setCreditHoursRequired(value); + const calculatedProgramLength = value / creditHoursPerYear; // Calculate program length + setProgramLength(calculatedProgramLength.toFixed(2)); // Keep two decimal places + }; + const handleSubmit = async (e) => { e.preventDefault(); const formData = { @@ -273,21 +335,36 @@ function FinancialProfileForm() { isFullyOnline, selectedSchool, selectedProgram, - calculatedTuition, - manualTuition, - finalTuition: manualTuition || calculatedTuition + tuition: manualTuition || calculatedTuition, + hoursCompleted: hoursCompleted ? parseInt(hoursCompleted, 10) : 0, + programLength: parseFloat(programLength), + creditHoursRequired: parseFloat(creditHoursRequired), + loanDeferralUntilGraduation, + interestRate: parseFloat(interestRate), + loanTerm: parseInt(loanTerm, 10), + extraPayment: parseFloat(extraPayment), + expectedSalary: parseFloat(expectedSalary), }; + try { const res = await authFetch("/api/premium/financial-profile", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ user_id: userId, ...formData }) + body: JSON.stringify({ user_id: userId, ...formData }), }); - + if (res.ok) { + const data = await res.json(); + setProjectionData(data.projectionData); // Store projection data + setLoanPayoffMonth(data.loanPaidOffMonth); // Store loan payoff month + navigate('/milestone-tracker', { - state: { selectedCareer } + state: { + selectedCareer, + projectionData: data.projectionData, + loanPayoffMonth: data.loanPaidOffMonth + } }); } } catch (err) { @@ -307,6 +384,9 @@ function FinancialProfileForm() { + + + @@ -361,20 +441,12 @@ function FinancialProfileForm() { className="w-full border rounded p-2" placeholder="Search for a Program" /> - {selectedProgram.length > 0 && programSuggestions.length > 0 && ( + {programSuggestions.length > 0 && (