Projection fixes, and added sample Paywall.js
This commit is contained in:
parent
16816d74b3
commit
fa26c4a31b
@ -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,
|
||||||
req.userId
|
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
||||||
]);
|
tuition, loanDeferralUntilGraduation, programLength,
|
||||||
|
interestRate, loanTerm, extraPayment, expectedSalary, // ✅ added new fields
|
||||||
|
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
|
||||||
uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
retirementSavings, retirementContribution, emergencyFund,
|
[
|
||||||
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal
|
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
|
// Return the financial simulation results (calculated projection data) to the frontend
|
||||||
if (!careerPathId) {
|
res.status(200).json({
|
||||||
return res.status(200).json({ message: 'Financial profile saved. No projection generated.' });
|
message: 'Financial profile saved.',
|
||||||
}
|
projectionData,
|
||||||
|
loanPaidOffMonth,
|
||||||
const milestones = await db.all(
|
emergencyFund: emergencyFund // explicitly add the emergency fund here
|
||||||
`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 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 {
|
||||||
|
@ -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 =>
|
||||||
|
@ -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)} />
|
||||||
@ -409,7 +495,27 @@ function FinancialProfileForm() {
|
|||||||
<input id="isFullyOnline" type="checkbox" checked={isFullyOnline} onChange={(e) => setIsFullyOnline(e.target.checked)} />
|
<input id="isFullyOnline" type="checkbox" checked={isFullyOnline} onChange={(e) => setIsFullyOnline(e.target.checked)} />
|
||||||
<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="$"
|
||||||
|
/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,34 +26,18 @@ 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 () => {
|
||||||
@ -100,28 +84,55 @@ 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) {
|
||||||
@ -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
28
src/components/Paywall.js
Normal 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;
|
@ -7,104 +7,131 @@ 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
|
|
||||||
if (
|
|
||||||
milestoneIndex < milestones.length &&
|
|
||||||
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
|
let tuitionCostThisMonth = 0;
|
||||||
if (graduationDate && date < graduationDate) {
|
if (inCollege && !loanDeferralUntilGraduation) {
|
||||||
monthlyIncome = fullTimeCollegeStudent ? 0 : partTimeIncome;
|
tuitionCostThisMonth = totalTuitionCost / programDuration / 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
const salaryNow = graduationDate && date >= graduationDate ? expectedSalary : currentSalary;
|
||||||
}
|
|
||||||
|
|
||||||
let extraCash = monthlyLoanPayment - thisMonthLoanPayment;
|
const totalMonthlyExpenses = monthlyExpenses
|
||||||
|
+ tuitionCostThisMonth
|
||||||
|
+ monthlyDebtPayments
|
||||||
|
+ thisMonthLoanPayment;
|
||||||
|
|
||||||
totalEmergencySavings += monthlyEmergencyContribution + (extraCash * 0.3); // 30% redirect
|
const monthlyIncome = salaryNow / 12;
|
||||||
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7); // 70% redirect
|
|
||||||
totalRetirementSavings *= (1 + 0.07 / 12); // compound growth
|
|
||||||
|
|
||||||
|
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
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user