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 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 {
|
||||
|
@ -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 =>
|
||||
|
@ -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() {
|
||||
<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="$" />
|
||||
|
||||
<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>
|
||||
<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"
|
||||
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">
|
||||
{programSuggestions.map((suggestion, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setSelectedProgram(suggestion);
|
||||
setProgramSuggestions([]);
|
||||
const filteredTypes = schoolData.filter(s =>
|
||||
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
|
||||
s.CIPDESC === suggestion
|
||||
).map(s => s.CREDDESC);
|
||||
setAvailableProgramTypes([...new Set(filteredTypes)]);
|
||||
}}
|
||||
onClick={() => handleProgramSelect(suggestion)}
|
||||
className="p-2 hover:bg-blue-100 cursor-pointer"
|
||||
>
|
||||
{suggestion}
|
||||
@ -397,8 +469,22 @@ function FinancialProfileForm() {
|
||||
</option>
|
||||
))}
|
||||
</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">
|
||||
<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)} />
|
||||
<label htmlFor="isFullyOnline" className="font-medium">Program is Fully Online</label>
|
||||
</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>
|
||||
<input
|
||||
type="number"
|
||||
@ -427,6 +533,43 @@ function FinancialProfileForm() {
|
||||
className="w-full border rounded p-2"
|
||||
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 annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { Filler } from 'chart.js';
|
||||
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
import CareerSelectDropdown from './CareerSelectDropdown.js';
|
||||
import CareerSearch from './CareerSearch.js';
|
||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
||||
@ -26,34 +26,18 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
|
||||
const [careerPathId, setCareerPathId] = useState(null);
|
||||
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||
const [activeView, setActiveView] = useState("Career");
|
||||
const [projectionData, setProjectionData] = useState(null);
|
||||
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 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(() => {
|
||||
const fetchCareerPaths = async () => {
|
||||
@ -100,28 +84,55 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
fetchFinancialProfile();
|
||||
}, []);
|
||||
|
||||
// Calculate financial projection based on the profile
|
||||
useEffect(() => {
|
||||
if (financialProfile && selectedCareer) {
|
||||
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({
|
||||
const { projectionData, loanPaidOffMonth, emergencySavings } = simulateFinancialProjection({
|
||||
currentSalary: financialProfile.current_salary,
|
||||
monthlyExpenses: financialProfile.monthly_expenses,
|
||||
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
||||
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,
|
||||
retirementSavings: financialProfile.retirement_savings,
|
||||
monthlyRetirementContribution: financialProfile.retirement_contribution,
|
||||
monthlyEmergencyContribution: 0, // Add emergency savings contribution if available
|
||||
monthlyEmergencyContribution: 0,
|
||||
gradDate: financialProfile.expected_graduation,
|
||||
fullTimeCollegeStudent: financialProfile.in_college,
|
||||
partTimeIncome: financialProfile.part_time_income,
|
||||
startDate: new Date(),
|
||||
|
||||
programType: financialProfile.program_type,
|
||||
isFullyOnline: financialProfile.is_online,
|
||||
creditHoursPerYear: financialProfile.credit_hours_per_year,
|
||||
calculatedTuition: financialProfile.tuition,
|
||||
hoursCompleted: financialProfile.hours_completed,
|
||||
loanDeferralUntilGraduation: financialProfile.loan_deferral_until_graduation,
|
||||
programLength: financialProfile.program_length,
|
||||
});
|
||||
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]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleCareerChange = (selected) => {
|
||||
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 newId = uuidv4();
|
||||
@ -146,14 +161,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
|
||||
return (
|
||||
<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
|
||||
existingCareerPaths={existingCareerPaths}
|
||||
@ -176,8 +183,8 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
labels: projectionData.map(p => p.month),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Net Savings',
|
||||
data: projectionData.map(p => p.netSavings),
|
||||
label: 'Total Savings', // ✅ Changed label to clarify
|
||||
data: projectionData.map(p => p.cumulativeNetSavings),
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
tension: 0.4,
|
||||
@ -227,7 +234,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
backgroundColor: 'rgba(255, 206, 86, 0.8)',
|
||||
color: '#000',
|
||||
font: {
|
||||
style: 'bold',
|
||||
size: 12
|
||||
},
|
||||
rotation: 0,
|
||||
@ -240,7 +246,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
beginAtZero: false,
|
||||
ticks: {
|
||||
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 {
|
||||
currentSalary,
|
||||
monthlyExpenses,
|
||||
monthlyDebtPayments,
|
||||
studentLoanAmount,
|
||||
studentLoanAPR,
|
||||
loanTermYears,
|
||||
milestones = [],
|
||||
interestRate, // ✅ Corrected
|
||||
loanTerm, // ✅ Corrected
|
||||
extraPayment,
|
||||
expectedSalary,
|
||||
emergencySavings,
|
||||
retirementSavings,
|
||||
monthlyRetirementContribution,
|
||||
monthlyEmergencyContribution,
|
||||
gradDate,
|
||||
fullTimeCollegeStudent,
|
||||
partTimeIncome = 0,
|
||||
startDate = new Date()
|
||||
fullTimeCollegeStudent: inCollege,
|
||||
partTimeIncome,
|
||||
startDate,
|
||||
programType,
|
||||
isFullyOnline,
|
||||
creditHoursPerYear,
|
||||
calculatedTuition,
|
||||
hoursCompleted,
|
||||
loanDeferralUntilGraduation,
|
||||
programLength
|
||||
} = userProfile;
|
||||
|
||||
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, studentLoanAPR, loanTermYears);
|
||||
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
||||
|
||||
let totalEmergencySavings = emergencySavings;
|
||||
let totalRetirementSavings = retirementSavings;
|
||||
let loanBalance = studentLoanAmount;
|
||||
let monthlyIncome = currentSalary;
|
||||
let projectionData = [];
|
||||
|
||||
const graduationDate = gradDate ? new Date(gradDate) : null;
|
||||
let milestoneIndex = 0;
|
||||
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);
|
||||
for (let month = 0; month < 240; month++) {
|
||||
// ✅ Advance date forward by one month
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
|
||||
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
||||
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
|
||||
if (graduationDate && date < graduationDate) {
|
||||
monthlyIncome = fullTimeCollegeStudent ? 0 : partTimeIncome;
|
||||
let tuitionCostThisMonth = 0;
|
||||
if (inCollege && !loanDeferralUntilGraduation) {
|
||||
tuitionCostThisMonth = totalTuitionCost / programDuration / 12;
|
||||
}
|
||||
|
||||
|
||||
let thisMonthLoanPayment = 0;
|
||||
|
||||
if (loanBalance > 0) {
|
||||
const interestForMonth = loanBalance * (studentLoanAPR / 100 / 12);
|
||||
const principalForMonth = Math.min(loanBalance, monthlyLoanPayment - interestForMonth);
|
||||
|
||||
if (loanDeferralUntilGraduation && graduationDate && date < graduationDate) {
|
||||
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here
|
||||
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 = 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
|
||||
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7); // 70% redirect
|
||||
totalRetirementSavings *= (1 + 0.07 / 12); // compound growth
|
||||
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({
|
||||
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
salary: monthlyIncome,
|
||||
monthlyIncome: monthlyIncome / 12,
|
||||
expenses: monthlyExpenses,
|
||||
loanPayment: monthlyLoanPayment,
|
||||
salary: salaryNow,
|
||||
monthlyIncome: monthlyIncome,
|
||||
expenses: totalMonthlyExpenses,
|
||||
loanPayment: thisMonthLoanPayment,
|
||||
retirementContribution: monthlyRetirementContribution,
|
||||
emergencyContribution: monthlyEmergencyContribution,
|
||||
netSavings:
|
||||
monthlyIncome -
|
||||
(monthlyExpenses +
|
||||
thisMonthLoanPayment +
|
||||
monthlyRetirementContribution +
|
||||
monthlyEmergencyContribution),
|
||||
|
||||
netSavings: monthlyIncome - totalMonthlyExpenses, // Exclude contributions here explicitly!
|
||||
totalEmergencySavings,
|
||||
totalRetirementSavings,
|
||||
loanBalance
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return { projectionData, loanPaidOffMonth };
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { projectionData, loanPaidOffMonth, emergencySavings };
|
||||
}
|
||||
|
||||
function calculateLoanPayment(principal, annualRate, years) {
|
||||
const monthlyRate = annualRate / 100 / 12;
|
||||
const numPayments = years * 12;
|
||||
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