Projection fixes, and added sample Paywall.js

This commit is contained in:
Josh 2025-04-07 13:36:58 +00:00
parent 16816d74b3
commit fa26c4a31b
7 changed files with 448 additions and 179 deletions

View File

@ -9,6 +9,8 @@ import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; 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) => { app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
const { const {
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
retirementSavings, retirementContribution, emergencyFund, retirementSavings, retirementContribution, emergencyFund,
inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, 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; } = req.body;
try { 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]); const existing = await db.get(`SELECT id FROM financial_profile WHERE user_id = ?`, [req.userId]);
if (existing) { if (existing) {
// Updating existing profile
await db.run(` await db.run(`
UPDATE financial_profile SET UPDATE financial_profile SET
current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?, current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?,
retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?, retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?,
in_college = ?, expected_graduation = ?, part_time_income = ?, tuition_paid = ?, college_loan_total = ?, 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 updated_at = CURRENT_TIMESTAMP
WHERE user_id = ? WHERE user_id = ?`,
`, [ [
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
retirementSavings, retirementContribution, emergencyFund, retirementSavings, retirementContribution, emergencyFund,
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, 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 req.userId
]); ]
);
} else { } else {
// Insert a new profile
await db.run(` await db.run(`
INSERT INTO financial_profile ( INSERT INTO financial_profile (
id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments, id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments,
retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation, retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation,
part_time_income, tuition_paid, college_loan_total part_time_income, tuition_paid, college_loan_total, selected_school, selected_program, program_type,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 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, uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
retirementSavings, retirementContribution, emergencyFund, retirementSavings, retirementContribution, emergencyFund,
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal 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, // Return the financial simulation results (calculated projection data) to the frontend
monthlyExpenses, res.status(200).json({
monthlyDebtPayments, message: 'Financial profile saved.',
retirementSavings, projectionData,
retirementContribution, loanPaidOffMonth,
emergencySavings: emergencyFund, emergencyFund: emergencyFund // explicitly add the emergency fund here
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 res.status(200).json({ message: 'Financial profile saved.', projectionData }); console.log("Request body:", req.body);
} catch (error) { } catch (error) {
console.error('Error saving financial profile:', error); console.error('Error saving financial profile:', error);
@ -450,6 +482,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
// Retrieve career history // Retrieve career history
app.get('/api/premium/career-history', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/career-history', authenticatePremiumUser, async (req, res) => {
try { try {

View File

@ -18,14 +18,46 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
useEffect(() => { useEffect(() => {
if (!career) return; if (!career || !Array.isArray(projectionData)) return;
setSuggestedMilestones([
{ title: `Entry-Level ${career}`, date: '2025-06-01', progress: 0 }, // Dynamically suggest milestones based on projection data
{ title: `Mid-Level ${career}`, date: '2027-01-01', progress: 0 }, const suggested = [];
{ title: `Senior-Level ${career}`, date: '2030-01-01', progress: 0 },
]); // 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([]); setSelected([]);
}, [career]); }, [career, projectionData]);
const toggleSelect = (index) => { const toggleSelect = (index) => {
setSelected(prev => setSelected(prev =>

View File

@ -30,6 +30,16 @@ function FinancialProfileForm() {
const [selectedSchool, setSelectedSchool] = useState(""); const [selectedSchool, setSelectedSchool] = useState("");
const [selectedProgram, setSelectedProgram] = useState(""); const [selectedProgram, setSelectedProgram] = useState("");
const [manualTuition, setManualTuition] = 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 [schoolData, setSchoolData] = useState([]);
const [schoolSuggestions, setSchoolSuggestions] = useState([]); const [schoolSuggestions, setSchoolSuggestions] = useState([]);
@ -38,6 +48,7 @@ function FinancialProfileForm() {
const [icTuitionData, setIcTuitionData] = useState([]); const [icTuitionData, setIcTuitionData] = useState([]);
const [calculatedTuition, setCalculatedTuition] = useState(0); const [calculatedTuition, setCalculatedTuition] = useState(0);
const [selectedSchoolUnitId, setSelectedSchoolUnitId] = useState(null); const [selectedSchoolUnitId, setSelectedSchoolUnitId] = useState(null);
const [loanDeferralUntilGraduation, setLoanDeferralUntilGraduation] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchRawTuitionData() { async function fetchRawTuitionData() {
@ -60,18 +71,6 @@ function FinancialProfileForm() {
} }
}, [selectedSchool, schoolData]); }, [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(() => { useEffect(() => {
if (selectedSchool && programType && creditHoursPerYear && icTuitionData.length > 0) { if (selectedSchool && programType && creditHoursPerYear && icTuitionData.length > 0) {
// Find the selected school from tuition data // Find the selected school from tuition data
@ -137,6 +136,7 @@ function FinancialProfileForm() {
headers: { "Authorization": `Bearer ${localStorage.getItem('token')}` } headers: { "Authorization": `Bearer ${localStorage.getItem('token')}` }
}); });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
if (data && Object.keys(data).length > 0) { if (data && Object.keys(data).length > 0) {
@ -155,10 +155,15 @@ function FinancialProfileForm() {
setExistingCollegeDebt(data.existing_college_debt || ""); setExistingCollegeDebt(data.existing_college_debt || "");
setCreditHoursPerYear(data.credit_hours_per_year || ""); setCreditHoursPerYear(data.credit_hours_per_year || "");
setProgramType(data.program_type || ""); setProgramType(data.program_type || "");
setIsFullyOnline(!!data.is_fully_online); setIsFullyOnline(!!data.is_online); // Correct the name to 'is_online'
setSelectedSchool(data.selected_school || ""); setSelectedSchool(data.selected_school || "");
setSelectedProgram(data.selected_program || ""); 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) { } catch (err) {
@ -170,16 +175,15 @@ function FinancialProfileForm() {
}, [userId]); }, [userId]);
useEffect(() => { useEffect(() => {
if (selectedSchool && schoolData.length > 0) { if (selectedSchool && schoolData.length > 0 && !selectedProgram) {
// Filter programs for the selected school and display them as suggestions
const programs = schoolData const programs = schoolData
.filter(s => s.INSTNM.toLowerCase() === selectedSchool.toLowerCase()) .filter(s => s.INSTNM.toLowerCase() === selectedSchool.toLowerCase())
.map(s => s.CIPDESC); .map(s => s.CIPDESC);
// Filter unique programs and show the first 10
setProgramSuggestions([...new Set(programs)].slice(0, 10)); setProgramSuggestions([...new Set(programs)].slice(0, 10));
} }
}, [selectedSchool, schoolData]); }, [selectedSchool, schoolData, selectedProgram]);
useEffect(() => { useEffect(() => {
if (selectedProgram && selectedSchool && schoolData.length > 0) { if (selectedProgram && selectedSchool && schoolData.length > 0) {
@ -220,6 +224,16 @@ function FinancialProfileForm() {
setProgramSuggestions([]); 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 handleProgramChange = (e) => {
const value = e.target.value; const value = e.target.value;
setSelectedProgram(value); setSelectedProgram(value);
@ -244,14 +258,62 @@ function FinancialProfileForm() {
setAvailableProgramTypes([...new Set(filteredTypes)]); 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) => { const handleProgramTypeSelect = (e) => {
setProgramType(e.target.value); setProgramType(e.target.value);
setCreditHoursRequired(""); // Reset if the user changes program type
setProgramLength(""); // Recalculate when the program type changes
}; };
const handleTuitionInput = (e) => { const handleTuitionInput = (e) => {
setManualTuition(e.target.value); 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) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
const formData = { const formData = {
@ -273,21 +335,36 @@ function FinancialProfileForm() {
isFullyOnline, isFullyOnline,
selectedSchool, selectedSchool,
selectedProgram, selectedProgram,
calculatedTuition, tuition: manualTuition || calculatedTuition,
manualTuition, hoursCompleted: hoursCompleted ? parseInt(hoursCompleted, 10) : 0,
finalTuition: manualTuition || calculatedTuition programLength: parseFloat(programLength),
creditHoursRequired: parseFloat(creditHoursRequired),
loanDeferralUntilGraduation,
interestRate: parseFloat(interestRate),
loanTerm: parseInt(loanTerm, 10),
extraPayment: parseFloat(extraPayment),
expectedSalary: parseFloat(expectedSalary),
}; };
try { try {
const res = await authFetch("/api/premium/financial-profile", { const res = await authFetch("/api/premium/financial-profile", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, ...formData }) body: JSON.stringify({ user_id: userId, ...formData }),
}); });
if (res.ok) { if (res.ok) {
const data = await res.json();
setProjectionData(data.projectionData); // Store projection data
setLoanPayoffMonth(data.loanPaidOffMonth); // Store loan payoff month
navigate('/milestone-tracker', { navigate('/milestone-tracker', {
state: { selectedCareer } state: {
selectedCareer,
projectionData: data.projectionData,
loanPayoffMonth: data.loanPaidOffMonth
}
}); });
} }
} catch (err) { } catch (err) {
@ -307,6 +384,9 @@ function FinancialProfileForm() {
<label className="block font-medium">Additional Monthly Income</label> <label className="block font-medium">Additional Monthly Income</label>
<input type="number" value={additionalIncome} onChange={handleInput(setAdditionalIncome)} className="w-full border rounded p-2" placeholder="$" /> <input type="number" value={additionalIncome} onChange={handleInput(setAdditionalIncome)} className="w-full border rounded p-2" placeholder="$" />
<label className="block font-medium">Existing College Loan Debt</label>
<input type="number" value={collegeLoanTotal} onChange={handleInput(setCollegeLoanTotal)} className="w-full border rounded p-2" placeholder="Enter existing student loan debt" />
<label className="block font-medium">Monthly Living Expenses</label> <label className="block font-medium">Monthly Living Expenses</label>
<input type="number" value={monthlyExpenses} onChange={handleInput(setMonthlyExpenses)} className="w-full border rounded p-2" placeholder="$" /> <input type="number" value={monthlyExpenses} onChange={handleInput(setMonthlyExpenses)} className="w-full border rounded p-2" placeholder="$" />
@ -361,20 +441,12 @@ function FinancialProfileForm() {
className="w-full border rounded p-2" className="w-full border rounded p-2"
placeholder="Search for a Program" placeholder="Search for a Program"
/> />
{selectedProgram.length > 0 && programSuggestions.length > 0 && ( {programSuggestions.length > 0 && (
<ul className="border rounded bg-white max-h-40 overflow-y-auto shadow-md"> <ul className="border rounded bg-white max-h-40 overflow-y-auto shadow-md">
{programSuggestions.map((suggestion, idx) => ( {programSuggestions.map((suggestion, idx) => (
<li <li
key={idx} key={idx}
onClick={() => { onClick={() => handleProgramSelect(suggestion)}
setSelectedProgram(suggestion);
setProgramSuggestions([]);
const filteredTypes = schoolData.filter(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC === suggestion
).map(s => s.CREDDESC);
setAvailableProgramTypes([...new Set(filteredTypes)]);
}}
className="p-2 hover:bg-blue-100 cursor-pointer" className="p-2 hover:bg-blue-100 cursor-pointer"
> >
{suggestion} {suggestion}
@ -397,8 +469,22 @@ function FinancialProfileForm() {
</option> </option>
))} ))}
</select> </select>
{programType && (programType === "Graduate/Professional Certificate" || programType === "First Professional Degree" || programType === "Doctoral Degree") && (
<>
<label className="block font-medium">Credit Hours Required</label>
<input
type="number"
value={creditHoursRequired}
onChange={handleCreditHoursRequired}
className="w-full border rounded p-2"
placeholder="e.g. 30"
/>
</> </>
)} )}
</>
)}
<> <>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input id="isInState" type="checkbox" checked={isInState} onChange={(e) => setIsInState(e.target.checked)} /> <input id="isInState" type="checkbox" checked={isInState} onChange={(e) => setIsInState(e.target.checked)} />
@ -410,6 +496,26 @@ function FinancialProfileForm() {
<label htmlFor="isFullyOnline" className="font-medium">Program is Fully Online</label> <label htmlFor="isFullyOnline" className="font-medium">Program is Fully Online</label>
</div> </div>
<div className="flex items-center space-x-2">
<input
id="loanDeferralUntilGraduation"
type="checkbox"
checked={loanDeferralUntilGraduation}
onChange={(e) => setLoanDeferralUntilGraduation(e.target.checked)}
/>
<label htmlFor="loanDeferralUntilGraduation" className="font-medium">
Loan Payments Deferred Until Graduation?
</label>
</div>
<label className="block font-medium">Hours Completed</label>
<input
type="number"
value={hoursCompleted}
onChange={(e) => setHoursCompleted(e.target.value)}
className="w-full border rounded p-2"
placeholder="e.g. 30"
/>
<label className="block font-medium">Credit Hours Per Year</label> <label className="block font-medium">Credit Hours Per Year</label>
<input <input
type="number" type="number"
@ -427,6 +533,43 @@ function FinancialProfileForm() {
className="w-full border rounded p-2" className="w-full border rounded p-2"
placeholder="Override tuition amount" placeholder="Override tuition amount"
/> />
<label className="block font-medium">Loan Interest Rate (%)</label>
<input
type="number"
value={interestRate}
onChange={(e) => setInterestRate(e.target.value)}
className="w-full border rounded p-2"
placeholder="e.g., 5.5"
/>
<label className="block font-medium">Loan Term (years)</label>
<input
type="number"
value={loanTerm}
onChange={(e) => setLoanTerm(e.target.value)}
className="w-full border rounded p-2"
placeholder="e.g., 10"
/>
<label className="block font-medium">Extra Monthly Payment</label>
<input
type="number"
value={extraPayment}
onChange={(e) => setExtraPayment(e.target.value)}
className="w-full border rounded p-2"
placeholder="e.g., 100 (optional)"
/>
<label className="block font-medium">Expected Salary after Graduation</label>
<input
type="number"
value={expectedSalary}
onChange={(e) => setExpectedSalary(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
</> </>

View File

@ -6,7 +6,7 @@ import { Line } from 'react-chartjs-2';
import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend } from 'chart.js'; import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import { Filler } from 'chart.js'; import { Filler } from 'chart.js';
import authFetch from '../utils/authFetch.js';
import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSelectDropdown from './CareerSelectDropdown.js';
import CareerSearch from './CareerSearch.js'; import CareerSearch from './CareerSearch.js';
import MilestoneTimeline from './MilestoneTimeline.js'; import MilestoneTimeline from './MilestoneTimeline.js';
@ -26,35 +26,19 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
const [careerPathId, setCareerPathId] = useState(null); const [careerPathId, setCareerPathId] = useState(null);
const [existingCareerPaths, setExistingCareerPaths] = useState([]); const [existingCareerPaths, setExistingCareerPaths] = useState([]);
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
const [activeView, setActiveView] = useState("Career"); const [activeView, setActiveView] = useState("Career");
const [projectionData, setProjectionData] = useState(null);
const [financialProfile, setFinancialProfile] = useState(null); // Store the financial profile const [financialProfile, setFinancialProfile] = useState(null); // Store the financial profile
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); const {
projectionData: initialProjectionData = [],
loanPayoffMonth: initialLoanPayoffMonth = null,
} = location.state || {};
const [loanPayoffMonth, setLoanPayoffMonth] = useState(initialLoanPayoffMonth);
const [projectionData, setProjectionData] = useState(initialProjectionData);
const apiURL = process.env.REACT_APP_API_URL; const apiURL = process.env.REACT_APP_API_URL;
const authFetch = async (url, options = {}) => {
const token = localStorage.getItem('token');
if (!token) {
setShowSessionExpiredModal(true);
return null;
}
const res = await fetch(url, {
...options,
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers },
});
if ([401, 403].includes(res.status)) {
setShowSessionExpiredModal(true);
return null;
}
return res;
};
useEffect(() => { useEffect(() => {
const fetchCareerPaths = async () => { const fetchCareerPaths = async () => {
const res = await authFetch(`${apiURL}/premium/planned-path/all`); const res = await authFetch(`${apiURL}/premium/planned-path/all`);
@ -100,29 +84,56 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
fetchFinancialProfile(); fetchFinancialProfile();
}, []); }, []);
// Calculate financial projection based on the profile
useEffect(() => { useEffect(() => {
if (financialProfile && selectedCareer) { if (financialProfile && selectedCareer) {
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({ const { projectionData, loanPaidOffMonth, emergencySavings } = simulateFinancialProjection({
currentSalary: financialProfile.current_salary, currentSalary: financialProfile.current_salary,
monthlyExpenses: financialProfile.monthly_expenses, monthlyExpenses: financialProfile.monthly_expenses,
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
studentLoanAmount: financialProfile.college_loan_total, studentLoanAmount: financialProfile.college_loan_total,
studentLoanAPR: 5.5, // Default rate
loanTermYears: 10, // Default term 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, emergencySavings: financialProfile.emergency_fund,
retirementSavings: financialProfile.retirement_savings, retirementSavings: financialProfile.retirement_savings,
monthlyRetirementContribution: financialProfile.retirement_contribution, monthlyRetirementContribution: financialProfile.retirement_contribution,
monthlyEmergencyContribution: 0, // Add emergency savings contribution if available monthlyEmergencyContribution: 0,
gradDate: financialProfile.expected_graduation, gradDate: financialProfile.expected_graduation,
fullTimeCollegeStudent: financialProfile.in_college, fullTimeCollegeStudent: financialProfile.in_college,
partTimeIncome: financialProfile.part_time_income, partTimeIncome: financialProfile.part_time_income,
startDate: new Date(), 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,
}); });
setProjectionData(projectionData);
setLoanPayoffMonth(loanPaidOffMonth); // Set the projection data 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]); }, [financialProfile, selectedCareer]);
const handleCareerChange = (selected) => { const handleCareerChange = (selected) => {
if (selected && selected.id && selected.career_name) { if (selected && selected.id && selected.career_name) {
setSelectedCareer(selected); setSelectedCareer(selected);
@ -132,7 +143,11 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
} }
}; };
console.log("📊 projectionData sample:", projectionData?.slice(0, 5)); console.log(
'First 5 items of projectionData:',
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
);
const handleConfirmCareerSelection = async () => { const handleConfirmCareerSelection = async () => {
const newId = uuidv4(); const newId = uuidv4();
@ -146,14 +161,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
return ( return (
<div className="milestone-tracker"> <div className="milestone-tracker">
{showSessionExpiredModal && (
<div className="modal-overlay">
<div className="modal">
<h3>Session Expired</h3>
<button onClick={() => navigate('/signin')}>Go to Sign In</button>
</div>
</div>
)}
<CareerSelectDropdown <CareerSelectDropdown
existingCareerPaths={existingCareerPaths} existingCareerPaths={existingCareerPaths}
@ -176,8 +183,8 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
labels: projectionData.map(p => p.month), labels: projectionData.map(p => p.month),
datasets: [ datasets: [
{ {
label: 'Net Savings', label: 'Total Savings', // ✅ Changed label to clarify
data: projectionData.map(p => p.netSavings), 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,
@ -227,7 +234,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
backgroundColor: 'rgba(255, 206, 86, 0.8)', backgroundColor: 'rgba(255, 206, 86, 0.8)',
color: '#000', color: '#000',
font: { font: {
style: 'bold',
size: 12 size: 12
}, },
rotation: 0, rotation: 0,
@ -240,7 +246,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
}, },
scales: { scales: {
y: { y: {
beginAtZero: true, beginAtZero: false,
ticks: { ticks: {
callback: (value) => `$${value.toLocaleString()}` callback: (value) => `$${value.toLocaleString()}`
} }

28
src/components/Paywall.js Normal file
View File

@ -0,0 +1,28 @@
// Paywall.js
import React from 'react';
import { useNavigate } from 'react-router-dom';
const Paywall = () => {
const navigate = useNavigate();
const handleSubscribe = () => {
// Implement subscription logic here (Stripe, etc.)
alert('Subscription logic placeholder!');
};
return (
<div className="paywall">
<h2>Unlock AptivaAI Premium</h2>
<ul>
<li> Personalized Career Milestone Planning</li>
<li> Comprehensive Financial Projections</li>
<li> Detailed College Guidance & Analysis</li>
</ul>
<button onClick={handleSubscribe}>Subscribe Now</button>
<button onClick={() => navigate(-1)}>Cancel / Go Back</button>
</div>
);
};
export default Paywall;

View File

@ -7,91 +7,117 @@ export function simulateFinancialProjection(userProfile) {
const { const {
currentSalary, currentSalary,
monthlyExpenses, monthlyExpenses,
monthlyDebtPayments,
studentLoanAmount, studentLoanAmount,
studentLoanAPR, interestRate, // ✅ Corrected
loanTermYears, loanTerm, // ✅ Corrected
milestones = [], extraPayment,
expectedSalary,
emergencySavings, emergencySavings,
retirementSavings, retirementSavings,
monthlyRetirementContribution, monthlyRetirementContribution,
monthlyEmergencyContribution, monthlyEmergencyContribution,
gradDate, gradDate,
fullTimeCollegeStudent, fullTimeCollegeStudent: inCollege,
partTimeIncome = 0, partTimeIncome,
startDate = new Date() startDate,
programType,
isFullyOnline,
creditHoursPerYear,
calculatedTuition,
hoursCompleted,
loanDeferralUntilGraduation,
programLength
} = userProfile; } = userProfile;
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, studentLoanAPR, loanTermYears); const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
let totalEmergencySavings = emergencySavings; let totalEmergencySavings = emergencySavings;
let totalRetirementSavings = retirementSavings; let totalRetirementSavings = retirementSavings;
let loanBalance = studentLoanAmount; let loanBalance = studentLoanAmount;
let monthlyIncome = currentSalary;
let projectionData = []; let projectionData = [];
const graduationDate = gradDate ? new Date(gradDate) : null; const graduationDate = gradDate ? new Date(gradDate) : null;
let milestoneIndex = 0; let milestoneIndex = 0;
let loanPaidOffMonth = null; let loanPaidOffMonth = null;
// Dynamic credit hours based on the program type
let requiredCreditHours;
switch (programType) {
case "Associate Degree":
requiredCreditHours = 60;
break;
case "Bachelor's Degree":
requiredCreditHours = 120;
break;
case "Master's Degree":
requiredCreditHours = 30;
break;
case "Doctoral Degree":
requiredCreditHours = 60;
break;
default:
requiredCreditHours = 120;
}
const remainingCreditHours = requiredCreditHours - hoursCompleted;
const programDuration = Math.ceil(remainingCreditHours / creditHoursPerYear);
const tuitionCost = calculatedTuition;
const totalTuitionCost = tuitionCost * programDuration;
const date = new Date(startDate); const date = new Date(startDate);
for (let month = 0; month < 240; month++) { for (let month = 0; month < 240; month++) {
// ✅ Advance date forward by one month
date.setMonth(date.getMonth() + 1); date.setMonth(date.getMonth() + 1);
if (loanBalance <= 0 && !loanPaidOffMonth) { if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
} }
// Check for milestone let tuitionCostThisMonth = 0;
if ( if (inCollege && !loanDeferralUntilGraduation) {
milestoneIndex < milestones.length && tuitionCostThisMonth = totalTuitionCost / programDuration / 12;
new Date(milestones[milestoneIndex].date) <= date
) {
const milestone = milestones[milestoneIndex];
if (milestone.type === 'salary') {
monthlyIncome = milestone.newSalary;
}
milestoneIndex++;
}
// If in college before graduation, adjust income
if (graduationDate && date < graduationDate) {
monthlyIncome = fullTimeCollegeStudent ? 0 : partTimeIncome;
} }
let thisMonthLoanPayment = 0; let thisMonthLoanPayment = 0;
if (loanBalance > 0) { if (loanDeferralUntilGraduation && graduationDate && date < graduationDate) {
const interestForMonth = loanBalance * (studentLoanAPR / 100 / 12); const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here
const principalForMonth = Math.min(loanBalance, monthlyLoanPayment - interestForMonth); loanBalance += interestForMonth;
} else if (loanBalance > 0) {
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here
const principalForMonth = Math.min(loanBalance, monthlyLoanPayment + extraPayment - interestForMonth);
loanBalance -= principalForMonth; loanBalance -= principalForMonth;
loanBalance = Math.max(loanBalance, 0); loanBalance = Math.max(loanBalance, 0);
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
thisMonthLoanPayment = monthlyLoanPayment;
} }
let extraCash = monthlyLoanPayment - thisMonthLoanPayment; const salaryNow = graduationDate && date >= graduationDate ? expectedSalary : currentSalary;
totalEmergencySavings += monthlyEmergencyContribution + (extraCash * 0.3); // 30% redirect const totalMonthlyExpenses = monthlyExpenses
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7); // 70% redirect + tuitionCostThisMonth
totalRetirementSavings *= (1 + 0.07 / 12); // compound growth + monthlyDebtPayments
+ thisMonthLoanPayment;
const monthlyIncome = salaryNow / 12;
let extraCash = monthlyIncome - totalMonthlyExpenses - monthlyRetirementContribution - monthlyEmergencyContribution;
extraCash = Math.max(extraCash, 0);
// update savings explicitly with contributions first
totalEmergencySavings += monthlyEmergencyContribution + (extraCash * 0.3);
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7);
totalRetirementSavings *= (1 + 0.07 / 12);
// netSavings calculation fixed
projectionData.push({ projectionData.push({
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`, month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
salary: monthlyIncome, salary: salaryNow,
monthlyIncome: monthlyIncome / 12, monthlyIncome: monthlyIncome,
expenses: monthlyExpenses, expenses: totalMonthlyExpenses,
loanPayment: monthlyLoanPayment, loanPayment: thisMonthLoanPayment,
retirementContribution: monthlyRetirementContribution, retirementContribution: monthlyRetirementContribution,
emergencyContribution: monthlyEmergencyContribution, emergencyContribution: monthlyEmergencyContribution,
netSavings: netSavings: monthlyIncome - totalMonthlyExpenses, // Exclude contributions here explicitly!
monthlyIncome -
(monthlyExpenses +
thisMonthLoanPayment +
monthlyRetirementContribution +
monthlyEmergencyContribution),
totalEmergencySavings, totalEmergencySavings,
totalRetirementSavings, totalRetirementSavings,
loanBalance loanBalance
@ -99,12 +125,13 @@ export function simulateFinancialProjection(userProfile) {
} }
return { projectionData, loanPaidOffMonth };
} return { projectionData, loanPaidOffMonth, emergencySavings };
}
function calculateLoanPayment(principal, annualRate, years) { function calculateLoanPayment(principal, annualRate, years) {
const monthlyRate = annualRate / 100 / 12; const monthlyRate = annualRate / 100 / 12;
const numPayments = years * 12; const numPayments = years * 12;
return (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -numPayments)); return (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -numPayments));
} }

Binary file not shown.