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 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 {

View File

@ -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 =>

View File

@ -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="$"
/>
</>

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 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
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,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));
}

Binary file not shown.