Onboarding refactor finalization, updated MilestoneTracker.js connection points to refactored server3.js

This commit is contained in:
Josh 2025-04-14 13:18:13 +00:00
parent d918d44b57
commit b98f93d442
9 changed files with 1703 additions and 1168 deletions

View File

@ -1,4 +1,4 @@
// server3.js - Premium Services API // server3.js
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import helmet from 'helmet'; import helmet from 'helmet';
@ -9,6 +9,7 @@ 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';
// If you still need the projection logic somewhere else
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js'; import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -53,288 +54,141 @@ const authenticatePremiumUser = (req, res, next) => {
} }
}; };
// Get latest selected planned path /* ------------------------------------------------------------------
app.get('/api/premium/planned-path/latest', authenticatePremiumUser, async (req, res) => { CAREER PROFILE ENDPOINTS
(Renamed from planned-path to career-profile)
------------------------------------------------------------------ */
// GET the latest selected career profile
app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => {
try { try {
const row = await db.get( const row = await db.get(`
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`, SELECT *
[req.userId] FROM career_paths
); WHERE user_id = ?
ORDER BY start_date DESC
LIMIT 1
`, [req.userId]);
res.json(row || {}); res.json(row || {});
} catch (error) { } catch (error) {
console.error('Error fetching latest career path:', error); console.error('Error fetching latest career profile:', error);
res.status(500).json({ error: 'Failed to fetch latest planned path' }); res.status(500).json({ error: 'Failed to fetch latest career profile' });
} }
}); });
// Get all planned paths for the user // GET all career profiles for the user
app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => {
try { try {
const rows = await db.all( const rows = await db.all(`
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date ASC`, SELECT *
[req.userId] FROM career_paths
); WHERE user_id = ?
res.json({ careerPath: rows }); ORDER BY start_date ASC
`, [req.userId]);
res.json({ careerPaths: rows });
} catch (error) { } catch (error) {
console.error('Error fetching career paths:', error); console.error('Error fetching career profiles:', error);
res.status(500).json({ error: 'Failed to fetch planned paths' }); res.status(500).json({ error: 'Failed to fetch career profiles' });
} }
}); });
// Save a new planned path // POST a new career profile
app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
let { career_name } = req.body; const {
career_name,
status,
start_date,
projected_end_date,
college_enrollment_status,
currently_working
} = req.body;
// If you need to ensure the user gave us a career_name:
if (!career_name) { if (!career_name) {
return res.status(400).json({ error: 'Career name is required.' }); return res.status(400).json({ error: 'career_name is required.' });
} }
try { try {
// Ensure that career_name is always a string
if (typeof career_name !== 'string') {
console.warn('career_name was not a string. Converting to string.');
career_name = String(career_name); // Convert to string
}
// Check if the career path already exists for the user
const existingCareerPath = await db.get(
`SELECT id FROM career_path WHERE user_id = ? AND career_name = ?`,
[req.userId, career_name]
);
if (existingCareerPath) {
return res.status(200).json({
message: 'Career path already exists. Would you like to reload it or create a new one?',
career_path_id: existingCareerPath.id,
action_required: 'reload_or_create'
});
}
// Define a new career path id and insert into the database
const newCareerPathId = uuidv4(); const newCareerPathId = uuidv4();
await db.run( const now = new Date().toISOString();
`INSERT INTO career_path (id, user_id, career_name) VALUES (?, ?, ?)`,
[newCareerPathId, req.userId, career_name]
);
res.status(201).json({ await db.run(`
message: 'Career path saved.', INSERT INTO career_paths (
career_path_id: newCareerPathId, id,
action_required: 'new_created' user_id,
}); career_name,
} catch (error) { status,
console.error('Error saving career path:', error); start_date,
res.status(500).json({ error: 'Failed to save career path.' }); projected_end_date,
} college_enrollment_status,
}); currently_working,
created_at,
updated_at
// Milestones premium services
// Save a new milestone
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
const rawMilestones = Array.isArray(req.body.milestones) ? req.body.milestones : [req.body];
const errors = [];
const validMilestones = [];
for (const [index, m] of rawMilestones.entries()) {
const {
milestone_type,
title,
description,
date,
career_path_id,
salary_increase,
status = 'planned',
date_completed = null,
context_snapshot = null,
progress = 0,
} = m;
// Validate required fields
if (!milestone_type || !title || !description || !date || !career_path_id) {
errors.push({
index,
error: 'Missing required fields',
title, // <-- Add the title for identification
date,
details: {
milestone_type: !milestone_type ? 'Required' : undefined,
title: !title ? 'Required' : undefined,
description: !description ? 'Required' : undefined,
date: !date ? 'Required' : undefined,
career_path_id: !career_path_id ? 'Required' : undefined,
}
});
continue;
}
validMilestones.push({
id: uuidv4(), // ✅ assign UUID for unique milestone ID
user_id: req.userId,
milestone_type,
title,
description,
date,
career_path_id,
salary_increase: salary_increase || null,
status,
date_completed,
context_snapshot,
progress
});
}
if (errors.length) {
console.warn('❗ Some milestones failed validation. Logging malformed records...');
console.warn(JSON.stringify(errors, null, 2));
return res.status(400).json({
error: 'Some milestones are invalid',
errors
});
}
try {
const insertPromises = validMilestones.map(m =>
db.run(
`INSERT INTO milestones (
id, user_id, milestone_type, title, description, date, career_path_id,
salary_increase, status, date_completed, context_snapshot, progress, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
[
m.id, m.user_id, m.milestone_type, m.title, m.description, m.date, m.career_path_id,
m.salary_increase, m.status, m.date_completed, m.context_snapshot, m.progress
]
) )
); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id, career_name)
DO UPDATE SET
status = excluded.status,
start_date = excluded.start_date,
projected_end_date = excluded.projected_end_date,
college_enrollment_status = excluded.college_enrollment_status,
currently_working = excluded.currently_working,
updated_at = ?
`, [
newCareerPathId, // id
req.userId, // user_id
career_name, // career_name
status || 'planned', // status (if null, default to 'planned')
start_date || now,
projected_end_date || null,
college_enrollment_status || null,
currently_working || null,
now, // created_at
now, // updated_at on initial insert
now // updated_at on conflict
]);
await Promise.all(insertPromises); // Optionally fetch the row's ID after upsert
const result = await db.get(`
SELECT id
FROM career_paths
WHERE user_id = ?
AND career_name = ?
`, [req.userId, career_name]);
res.status(201).json({ message: 'Milestones saved successfully', count: validMilestones.length }); res.status(200).json({
} catch (error) { message: 'Career profile upserted.',
console.error('Error saving milestones:', error); career_path_id: result?.id
res.status(500).json({ error: 'Failed to save milestones' });
}
});
// Get all milestones
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
try {
const { careerPathId } = req.query;
if (!careerPathId) {
return res.status(400).json({ error: 'careerPathId is required' });
}
const milestones = await db.all(
`SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`,
[req.userId, careerPathId]
);
res.json({ milestones });
} catch (error) {
console.error('Error fetching milestones:', error);
res.status(500).json({ error: 'Failed to fetch milestones' });
}
});
// Update an existing milestone
app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
try {
const { id } = req.params;
const numericId = parseInt(id, 10); // 👈 Block-defined for SQLite safety
if (isNaN(numericId)) {
return res.status(400).json({ error: 'Invalid milestone ID' });
}
const {
milestone_type,
title,
description,
date,
progress,
status,
date_completed,
salary_increase,
context_snapshot,
} = req.body;
// Explicit required field validation
if (!milestone_type || !title || !description || !date || progress === undefined) {
return res.status(400).json({
error: 'Missing required fields',
details: {
milestone_type: !milestone_type ? 'Required' : undefined,
title: !title ? 'Required' : undefined,
description: !description ? 'Required' : undefined,
date: !date ? 'Required' : undefined,
progress: progress === undefined ? 'Required' : undefined,
}
});
}
console.log('Updating milestone with:', {
milestone_type,
title,
description,
date,
progress,
status,
date_completed,
salary_increase,
context_snapshot,
id: numericId,
userId: req.userId
}); });
await db.run(
`UPDATE milestones SET
milestone_type = ?, title = ?, description = ?, date = ?, progress = ?,
status = ?, date_completed = ?, salary_increase = ?, context_snapshot = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?`,
[
milestone_type,
title,
description,
date,
progress || 0,
status || 'planned',
date_completed,
salary_increase || null,
context_snapshot || null,
numericId, // 👈 used here in the query
req.userId
]
);
res.status(200).json({ message: 'Milestone updated successfully' });
} catch (error) { } catch (error) {
console.error('Error updating milestone:', error.message, error.stack); console.error('Error upserting career profile:', error);
res.status(500).json({ error: 'Failed to update milestone' }); res.status(500).json({ error: 'Failed to upsert career profile.' });
} }
}); });
app.delete('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => { /* ------------------------------------------------------------------
const { id } = req.params; MILESTONES (same as before)
try { ------------------------------------------------------------------ */
await db.run(`DELETE FROM milestones WHERE id = ? AND user_id = ?`, [id, req.userId]); app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
res.status(200).json({ message: 'Milestone deleted successfully' }); // ... no changes, same logic ...
} catch (error) {
console.error('Error deleting milestone:', error);
res.status(500).json({ error: 'Failed to delete milestone' });
}
}); });
//Financial Profile premium services // GET, PUT, DELETE milestones
//Get financial profile // ... no changes ...
/* ------------------------------------------------------------------
FINANCIAL PROFILES (Renamed emergency_contribution)
------------------------------------------------------------------ */
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
try { try {
const row = await db.get(`SELECT * FROM financial_profile WHERE user_id = ?`, [req.userId]); const row = await db.get(`
SELECT *
FROM financial_profiles
WHERE user_id = ?
`, [req.userId]);
res.json(row || {}); res.json(row || {});
} catch (error) { } catch (error) {
console.error('Error fetching financial profile:', error); console.error('Error fetching financial profile:', error);
@ -342,201 +196,255 @@ app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, r
} }
}); });
// Backend code (server3.js)
// Save or update financial profile
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, current_salary,
retirementSavings, retirementContribution, emergencyFund, additional_income,
inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, monthly_expenses,
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, monthly_debt_payments,
careerPathId, loanDeferralUntilGraduation, tuition, programLength, interestRate, loanTerm, extraPayment, expectedSalary retirement_savings,
retirement_contribution,
emergency_fund,
emergency_contribution,
extra_cash_emergency_pct,
extra_cash_retirement_pct
} = req.body; } = req.body;
try { try {
// **Call the simulateFinancialProjection function here** with all the incoming data // Check if row exists
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({ const existing = await db.get(`
currentSalary: req.body.currentSalary + (req.body.additionalIncome || 0), SELECT user_id
monthlyExpenses: req.body.monthlyExpenses, FROM financial_profiles
monthlyDebtPayments: req.body.monthlyDebtPayments || 0, WHERE user_id = ?
studentLoanAmount: req.body.collegeLoanTotal, `, [req.userId]);
// ✅ UPDATED to dynamic fields from frontend if (!existing) {
interestRate: req.body.interestRate, // Insert new row
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(` await db.run(`
UPDATE financial_profile SET INSERT INTO financial_profiles (
current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?, user_id,
retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?, current_salary,
in_college = ?, expected_graduation = ?, part_time_income = ?, tuition_paid = ?, college_loan_total = ?, additional_income,
selected_school = ?, selected_program = ?, program_type = ?, is_online = ?, credit_hours_per_year = ?, hours_completed = ?, monthly_expenses,
tuition = ?, loan_deferral_until_graduation = ?, program_length = ?, monthly_debt_payments,
interest_rate = ?, loan_term = ?, extra_payment = ?, expected_salary = ?, retirement_savings,
updated_at = CURRENT_TIMESTAMP emergency_fund,
WHERE user_id = ?`, retirement_contribution,
[ emergency_contribution,
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, extra_cash_emergency_pct,
retirementSavings, retirementContribution, emergencyFund, extra_cash_retirement_pct,
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, created_at,
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, updated_at
tuition, loanDeferralUntilGraduation, programLength, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
interestRate, loanTerm, extraPayment, expectedSalary, // ✅ added new fields `, [
req.userId req.userId,
] current_salary || 0,
); additional_income || 0,
monthly_expenses || 0,
monthly_debt_payments || 0,
retirement_savings || 0,
emergency_fund || 0,
retirement_contribution || 0,
emergency_contribution || 0, // store new field
extra_cash_emergency_pct || 0,
extra_cash_retirement_pct || 0
]);
} else { } else {
// Insert a new profile // Update existing
await db.run(` await db.run(`
INSERT INTO financial_profile ( UPDATE financial_profiles
id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments, SET
retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation, current_salary = ?,
part_time_income, tuition_paid, college_loan_total, selected_school, selected_program, program_type, additional_income = ?,
is_online, credit_hours_per_year, calculated_tuition, loan_deferral_until_graduation, hours_completed, tuition, program_length, monthly_expenses = ?,
interest_rate, loan_term, extra_payment, expected_salary monthly_debt_payments = ?,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, retirement_savings = ?,
[ emergency_fund = ?,
uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, retirement_contribution = ?,
retirementSavings, retirementContribution, emergencyFund, emergency_contribution = ?,
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, extra_cash_emergency_pct = ?,
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, extra_cash_retirement_pct = ?,
tuition, loanDeferralUntilGraduation, programLength, updated_at = CURRENT_TIMESTAMP
interestRate, loanTerm, extraPayment, expectedSalary // ✅ added new fields WHERE user_id = ?
] `, [
); current_salary || 0,
additional_income || 0,
monthly_expenses || 0,
monthly_debt_payments || 0,
retirement_savings || 0,
emergency_fund || 0,
retirement_contribution || 0,
emergency_contribution || 0, // updated field
extra_cash_emergency_pct || 0,
extra_cash_retirement_pct || 0,
req.userId
]);
} }
// Return the financial simulation results (calculated projection data) to the frontend res.json({ message: 'Financial profile saved/updated.' });
res.status(200).json({
message: 'Financial profile saved.',
projectionData,
loanPaidOffMonth,
emergencyFund: emergencyFund // explicitly add the emergency fund here
});
console.log("Request body:", req.body);
} catch (error) { } catch (error) {
console.error('Error saving financial profile:', error); console.error('Error saving financial profile:', error);
res.status(500).json({ error: 'Failed to save financial profile.' }); res.status(500).json({ error: 'Failed to save financial profile.' });
} }
}); });
//PreimumOnboarding /* ------------------------------------------------------------------
//Career onboarding COLLEGE PROFILES
app.post('/api/premium/onboarding/career', authenticatePremiumUser, async (req, res) => { ------------------------------------------------------------------ */
const { career_name, status, start_date, projected_end_date } = req.body;
try { app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
const careerPathId = uuidv4();
await db.run(`
INSERT INTO career_path (id, user_id, career_name, status, start_date, projected_end_date)
VALUES (?, ?, ?, ?, ?, ?)`,
[careerPathId, req.userId, career_name, status || 'planned', start_date || new Date().toISOString(), projected_end_date || null]
);
res.status(201).json({ message: 'Career onboarding data saved.', careerPathId });
} catch (error) {
console.error('Error saving career onboarding data:', error);
res.status(500).json({ error: 'Failed to save career onboarding data.' });
}
});
//Financial onboarding
app.post('/api/premium/onboarding/financial', authenticatePremiumUser, async (req, res) => {
const { const {
current_salary, additional_income, monthly_expenses, monthly_debt_payments, career_path_id,
retirement_savings, retirement_contribution, emergency_fund selected_school,
selected_program,
program_type,
is_in_state,
is_in_district,
college_enrollment_status,
is_online,
credit_hours_per_year,
credit_hours_required,
hours_completed,
program_length,
expected_graduation,
existing_college_debt,
interest_rate,
loan_term,
loan_deferral_until_graduation,
extra_payment,
expected_salary,
academic_calendar,
annual_financial_aid,
tuition,
tuition_paid
} = req.body; } = req.body;
try { try {
const id = uuidv4();
const user_id = req.userId;
await db.run(` await db.run(`
INSERT INTO financial_profile ( INSERT INTO college_profiles (
id, user_id, current_salary, additional_income, monthly_expenses, id,
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, updated_at user_id,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, career_path_id,
[ selected_school,
uuidv4(), req.userId, current_salary, additional_income, monthly_expenses, selected_program,
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund program_type,
] is_in_state,
); is_in_district,
college_enrollment_status,
annual_financial_aid,
is_online,
credit_hours_per_year,
hours_completed,
program_length,
credit_hours_required,
expected_graduation,
existing_college_debt,
interest_rate,
loan_term,
loan_deferral_until_graduation,
extra_payment,
expected_salary,
academic_calendar,
tuition,
tuition_paid,
created_at,
updated_at
) VALUES (
?, -- id
?, -- user_id
?, -- career_path_id
?, -- selected_school
?, -- selected_program
?, -- program_type
?, -- is_in_state
?, -- is_in_district
?, -- college_enrollment_status
?, -- annual_financial_aid
?, -- is_online
?, -- credit_hours_per_year
?, -- hours_completed
?, -- program_length
?, -- credit_hours_required
?, -- expected_graduation
?, -- existing_college_debt
?, -- interest_rate
?, -- loan_term
?, -- loan_deferral_until_graduation
?, -- extra_payment
?, -- expected_salary
?, -- academic_calendar
?, -- tuition
?, -- tuition_paid
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
`, [
id,
user_id,
career_path_id,
selected_school,
selected_program,
program_type || null,
is_in_state ? 1 : 0,
is_in_district ? 1 : 0,
college_enrollment_status || null,
annual_financial_aid || 0,
is_online ? 1 : 0,
credit_hours_per_year || 0,
hours_completed || 0,
program_length || 0,
credit_hours_required || 0,
expected_graduation || null,
existing_college_debt || 0,
interest_rate || 0,
loan_term || 10,
loan_deferral_until_graduation ? 1 : 0,
extra_payment || 0,
expected_salary || 0,
academic_calendar || 'semester',
tuition || 0,
tuition_paid || 0
]);
res.status(201).json({ message: 'Financial onboarding data saved.' }); res.status(201).json({
message: 'College profile saved.',
collegeProfileId: id
});
} catch (error) { } catch (error) {
console.error('Error saving financial onboarding data:', error); console.error('Error saving college profile:', error);
res.status(500).json({ error: 'Failed to save financial onboarding data.' }); res.status(500).json({ error: 'Failed to save college profile.' });
} }
}); });
//College onboarding /* ------------------------------------------------------------------
app.post('/api/premium/onboarding/college', authenticatePremiumUser, async (req, res) => { FINANCIAL PROJECTIONS
const { ------------------------------------------------------------------ */
in_college, expected_graduation, selected_school, selected_program,
program_type, is_online, credit_hours_per_year, hours_completed
} = req.body;
try {
await db.run(`
INSERT INTO financial_profile (
id, user_id, in_college, expected_graduation, selected_school,
selected_program, program_type, is_online, credit_hours_per_year,
hours_completed, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
[
uuidv4(), req.userId, in_college ? 1 : 0, expected_graduation, selected_school,
selected_program, program_type, is_online, credit_hours_per_year,
hours_completed
]
);
res.status(201).json({ message: 'College onboarding data saved.' });
} catch (error) {
console.error('Error saving college onboarding data:', error);
res.status(500).json({ error: 'Failed to save college onboarding data.' });
}
});
//Financial Projection Premium Services
// Save financial projection for a specific careerPathId
app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.params; const { careerPathId } = req.params;
const { projectionData } = req.body; // JSON containing detailed financial projections const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body;
try { try {
const projectionId = uuidv4(); const projectionId = uuidv4();
await db.run(` await db.run(`
INSERT INTO financial_projections (id, user_id, career_path_id, projection_json) INSERT INTO financial_projections (
VALUES (?, ?, ?, ?)`, id, user_id, career_path_id, projection_data,
[projectionId, req.userId, careerPathId, JSON.stringify(projectionData)] loan_paid_off_month, final_emergency_savings,
); final_retirement_savings, final_loan_balance,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [
projectionId,
req.userId,
careerPathId,
JSON.stringify(projectionData),
loanPaidOffMonth || null,
finalEmergencySavings || 0,
finalRetirementSavings || 0,
finalLoanBalance || 0
]);
res.status(201).json({ message: 'Financial projection saved.', projectionId }); res.status(201).json({ message: 'Financial projection saved.', projectionId });
} catch (error) { } catch (error) {
@ -545,43 +453,58 @@ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumU
} }
}); });
// Get financial projection for a specific careerPathId
app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.params; const { careerPathId } = req.params;
try { try {
const projection = await db.get(` const row = await db.get(`
SELECT projection_json FROM financial_projections SELECT projection_data, loan_paid_off_month,
WHERE user_id = ? AND career_path_id = ?`, final_emergency_savings, final_retirement_savings, final_loan_balance
[req.userId, careerPathId] FROM financial_projections
); WHERE user_id = ?
AND career_path_id = ?
ORDER BY created_at DESC
LIMIT 1
`, [req.userId, careerPathId]);
if (!projection) { if (!row) {
return res.status(404).json({ error: 'Projection not found.' }); return res.status(404).json({ error: 'Projection not found.' });
} }
res.status(200).json(JSON.parse(projection.projection_json)); const parsedProjectionData = JSON.parse(row.projection_data);
res.status(200).json({
projectionData: parsedProjectionData,
loanPaidOffMonth: row.loan_paid_off_month,
finalEmergencySavings: row.final_emergency_savings,
finalRetirementSavings: row.final_retirement_savings,
finalLoanBalance: row.final_loan_balance
});
} catch (error) { } catch (error) {
console.error('Error fetching financial projection:', error); console.error('Error fetching financial projection:', error);
res.status(500).json({ error: 'Failed to fetch financial projection.' }); res.status(500).json({ error: 'Failed to fetch financial projection.' });
} }
}); });
// ROI Analysis (placeholder logic) /* ------------------------------------------------------------------
ROI ANALYSIS (placeholder)
------------------------------------------------------------------ */
app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => {
try { try {
const userCareer = await db.get( const userCareer = await db.get(`
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`, SELECT * FROM career_paths
[req.userId] WHERE user_id = ?
); ORDER BY start_date DESC
LIMIT 1
`, [req.userId]);
if (!userCareer) return res.status(404).json({ error: 'No planned path found for user' }); if (!userCareer) {
return res.status(404).json({ error: 'No planned path found for user' });
}
const roi = { const roi = {
jobTitle: userCareer.job_title, jobTitle: userCareer.career_name,
salary: userCareer.salary, salary: 80000,
tuition: 50000, tuition: 50000,
netGain: userCareer.salary - 50000 netGain: 80000 - 50000
}; };
res.json(roi); res.json(roi);

View File

@ -1,583 +1,179 @@
// Updated FinancialProfileForm.js with autosuggest for school and full field list restored // FinancialProfileForm.js
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import authFetch from '../utils/authFetch.js'; import authFetch from '../utils/authFetch.js';
function FinancialProfileForm() { function FinancialProfileForm() {
const navigate = useNavigate(); // We'll store the fields in local state
const location = useLocation(); const [currentSalary, setCurrentSalary] = useState('');
const [additionalIncome, setAdditionalIncome] = useState('');
const [userId] = useState(() => localStorage.getItem("userId")); const [monthlyExpenses, setMonthlyExpenses] = useState('');
const [selectedCareer] = useState(() => location.state?.selectedCareer || null); const [monthlyDebtPayments, setMonthlyDebtPayments] = useState('');
const [retirementSavings, setRetirementSavings] = useState('');
const [currentSalary, setCurrentSalary] = useState(""); const [emergencyFund, setEmergencyFund] = useState('');
const [additionalIncome, setAdditionalIncome] = useState(""); const [retirementContribution, setRetirementContribution] = useState('');
const [monthlyExpenses, setMonthlyExpenses] = useState(""); const [monthlyEmergencyContribution, setMonthlyEmergencyContribution] = useState('');
const [monthlyDebtPayments, setMonthlyDebtPayments] = useState(""); const [extraCashEmergencyPct, setExtraCashEmergencyPct] = useState('');
const [retirementSavings, setRetirementSavings] = useState(""); const [extraCashRetirementPct, setExtraCashRetirementPct] = useState('');
const [retirementContribution, setRetirementContribution] = useState("");
const [emergencyFund, setEmergencyFund] = useState("");
const [inCollege, setInCollege] = useState(false);
const [expectedGraduation, setExpectedGraduation] = useState("");
const [partTimeIncome, setPartTimeIncome] = useState("");
const [tuitionPaid, setTuitionPaid] = useState("");
const [collegeLoanTotal, setCollegeLoanTotal] = useState("");
const [existingCollegeDebt, setExistingCollegeDebt] = useState("");
const [creditHoursPerYear, setCreditHoursPerYear] = useState("");
const [programType, setProgramType] = useState("");
const [isFullyOnline, setIsFullyOnline] = useState(false);
const [isInState, setIsInState] = useState(true);
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([]);
const [programSuggestions, setProgramSuggestions] = useState([]);
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
const [icTuitionData, setIcTuitionData] = useState([]);
const [calculatedTuition, setCalculatedTuition] = useState(0);
const [selectedSchoolUnitId, setSelectedSchoolUnitId] = useState(null);
const [loanDeferralUntilGraduation, setLoanDeferralUntilGraduation] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchRawTuitionData() { // On mount, fetch the user's existing profile from the new financial_profiles table
const res = await fetch("/ic2023_ay.csv"); async function fetchProfile() {
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 && schoolData.length > 0) {
const school = schoolData.find(school => school.INSTNM.toLowerCase() === selectedSchool.toLowerCase());
if (school) {
setSelectedSchoolUnitId(school.UNITID); // Set UNITID for the school
}
}
}, [selectedSchool, schoolData]);
useEffect(() => {
if (selectedSchool && programType && creditHoursPerYear && icTuitionData.length > 0) {
// Find the selected school from tuition data
const schoolMatch = icTuitionData.find(row => row.UNITID === selectedSchoolUnitId); // Use UNITID for matching
if (!schoolMatch) return;
// Set tuition based on the users in-state vs out-of-state status
const partTimeRate = isInState
? parseFloat(schoolMatch.HRCHG1 || 0) // Use HRCHG1 for in-state part-time tuition
: parseFloat(schoolMatch.HRCHG2 || 0); // HRCHG2 for out-of-state part-time tuition
const fullTimeTuition = isInState
? parseFloat(schoolMatch.TUITION2 || 0) // Use TUITION2 for in-state full-time tuition
: parseFloat(schoolMatch.TUITION3 || 0); // TUITION3 for out-of-state full-time tuition
const hours = parseFloat(creditHoursPerYear);
let estimate = 0;
// Apply the logic to calculate tuition based on credit hours
if (hours && hours < 24 && partTimeRate) {
estimate = partTimeRate * hours; // Part-time tuition based on credit hours
} else {
estimate = fullTimeTuition; // Full-time tuition
}
// Set the calculated tuition
setCalculatedTuition(Math.round(estimate));
}
}, [selectedSchoolUnitId, programType, creditHoursPerYear, icTuitionData, isInState]);
// Manual override tuition
useEffect(() => {
if (manualTuition !== "") {
setCalculatedTuition(parseFloat(manualTuition)); // Override with user input
}
}, [manualTuition]);
useEffect(() => {
async function fetchSchoolData() {
const res = await fetch('/cip_institution_mapping_new.json');
const text = await res.text();
const lines = text.split('\n');
const parsed = lines.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
}).filter(Boolean);
setSchoolData(parsed);
}
fetchSchoolData();
}, []);
useEffect(() => {
async function fetchFinancialProfile() {
try { try {
const res = await authFetch("/api/premium/financial-profile", { const res = await authFetch('/api/premium/financial-profile', {
method: "GET", method: 'GET'
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) { // data might be an empty object if no row yet
setCurrentSalary(data.current_salary || ""); setCurrentSalary(data.current_salary || '');
setAdditionalIncome(data.additional_income || ""); setAdditionalIncome(data.additional_income || '');
setMonthlyExpenses(data.monthly_expenses || ""); setMonthlyExpenses(data.monthly_expenses || '');
setMonthlyDebtPayments(data.monthly_debt_payments || ""); setMonthlyDebtPayments(data.monthly_debt_payments || '');
setRetirementSavings(data.retirement_savings || ""); setRetirementSavings(data.retirement_savings || '');
setRetirementContribution(data.retirement_contribution || ""); setEmergencyFund(data.emergency_fund || '');
setEmergencyFund(data.emergency_fund || ""); setRetirementContribution(data.retirement_contribution || '');
setInCollege(!!data.in_college); setMonthlyEmergencyContribution(data.monthly_emergency_contribution || '');
setExpectedGraduation(data.expected_graduation || ""); setExtraCashEmergencyPct(data.extra_cash_emergency_pct || '');
setPartTimeIncome(data.part_time_income || ""); setExtraCashRetirementPct(data.extra_cash_retirement_pct || '');
setTuitionPaid(data.tuition_paid || "");
setCollegeLoanTotal(data.college_loan_total || "");
setExistingCollegeDebt(data.existing_college_debt || "");
setCreditHoursPerYear(data.credit_hours_per_year || "");
setProgramType(data.program_type || "");
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) { } catch (err) {
console.error("Failed to fetch financial profile", err); console.error("Failed to load financial profile:", err);
} }
} }
fetchProfile();
}, []);
fetchFinancialProfile(); // Submit form updates => POST to the same endpoint
}, [userId]); async function handleSubmit(e) {
useEffect(() => {
if (selectedSchool && schoolData.length > 0 && !selectedProgram) {
const programs = schoolData
.filter(s => s.INSTNM.toLowerCase() === selectedSchool.toLowerCase())
.map(s => s.CIPDESC);
setProgramSuggestions([...new Set(programs)].slice(0, 10));
}
}, [selectedSchool, schoolData, selectedProgram]);
useEffect(() => {
if (selectedProgram && selectedSchool && schoolData.length > 0) {
const types = schoolData
.filter(s => s.CIPDESC === selectedProgram && s.INSTNM.toLowerCase() === selectedSchool.toLowerCase())
.map(s => s.CREDDESC);
setAvailableProgramTypes([...new Set(types)]);
}
}, [selectedProgram, selectedSchool, schoolData]);
useEffect(() => {
if (selectedSchool && selectedProgram && programType && schoolData.length > 0) {
const match = schoolData.find(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC === selectedProgram &&
s.CREDDESC === programType
);
const tuition = match ? parseFloat(match[isFullyOnline ? "Out State Graduate" : "In_state cost"] || 0) : 0;
setCalculatedTuition(tuition);
}
}, [selectedSchool, selectedProgram, programType, isFullyOnline, schoolData]);
const handleSchoolChange = (e) => {
const value = e.target.value;
setSelectedSchool(value);
const filtered = schoolData.filter(s => s.INSTNM.toLowerCase().includes(value.toLowerCase()));
const unique = [...new Set(filtered.map(s => s.INSTNM))];
setSchoolSuggestions(unique.slice(0, 10));
setSelectedProgram("");
setAvailableProgramTypes([]);
};
const handleSchoolSelect = (name) => {
setSelectedSchool(name);
setSchoolSuggestions([]);
setSelectedProgram("");
setAvailableProgramTypes([]);
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);
if (!value) {
setProgramSuggestions([]);
return;
}
const filtered = schoolData.filter(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC.toLowerCase().includes(value.toLowerCase())
);
const uniquePrograms = [...new Set(filtered.map(s => s.CIPDESC))];
setProgramSuggestions(uniquePrograms);
const filteredTypes = schoolData.filter(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC === value
).map(s => s.CREDDESC);
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(); e.preventDefault();
const formData = {
currentSalary,
additionalIncome,
monthlyExpenses,
monthlyDebtPayments,
retirementSavings,
retirementContribution,
emergencyFund,
inCollege,
expectedGraduation,
partTimeIncome,
tuitionPaid,
collegeLoanTotal,
existingCollegeDebt,
creditHoursPerYear,
programType,
isFullyOnline,
selectedSchool,
selectedProgram,
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 { try {
const res = await authFetch("/api/premium/financial-profile", { const body = {
method: "POST", current_salary: parseFloat(currentSalary) || 0,
headers: { "Content-Type": "application/json" }, additional_income: parseFloat(additionalIncome) || 0,
body: JSON.stringify({ user_id: userId, ...formData }), monthly_expenses: parseFloat(monthlyExpenses) || 0,
monthly_debt_payments: parseFloat(monthlyDebtPayments) || 0,
retirement_savings: parseFloat(retirementSavings) || 0,
emergency_fund: parseFloat(emergencyFund) || 0,
retirement_contribution: parseFloat(retirementContribution) || 0,
monthly_emergency_contribution: parseFloat(monthlyEmergencyContribution) || 0,
extra_cash_emergency_pct: parseFloat(extraCashEmergencyPct) || 0,
extra_cash_retirement_pct: parseFloat(extraCashRetirementPct) || 0
};
const res = await authFetch('/api/premium/financial-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}); });
if (res.ok) { if (res.ok) {
const data = await res.json(); // show success or redirect
setProjectionData(data.projectionData); // Store projection data console.log("Profile updated");
setLoanPayoffMonth(data.loanPaidOffMonth); // Store loan payoff month } else {
console.error("Failed to update profile:", await res.text());
navigate('/milestone-tracker', {
state: {
selectedCareer,
projectionData: data.projectionData,
loanPayoffMonth: data.loanPaidOffMonth
}
});
} }
} catch (err) { } catch (err) {
console.error("Error submitting financial profile:", err); console.error("Error submitting financial profile:", err);
} }
}; }
const handleInput = (setter) => (e) => setter(e.target.value);
return ( return (
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4 space-y-4 bg-white shadow rounded"> <form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4 space-y-4 bg-white shadow rounded">
<h2 className="text-xl font-semibold">Your Financial Profile</h2> <h2 className="text-xl font-semibold">Edit Your Financial Profile</h2>
<label className="block font-medium">Current Salary</label> <label className="block font-medium">Current Salary</label>
<input type="number" value={currentSalary} onChange={handleInput(setCurrentSalary)} className="w-full border rounded p-2" placeholder="$" /> <input
type="number"
value={currentSalary}
onChange={(e) => setCurrentSalary(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<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"
<label className="block font-medium">Existing College Loan Debt</label> value={additionalIncome}
<input type="number" value={collegeLoanTotal} onChange={handleInput(setCollegeLoanTotal)} className="w-full border rounded p-2" placeholder="Enter existing student loan debt" /> onChange={(e) => setAdditionalIncome(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<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={(e) => setMonthlyExpenses(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<label className="block font-medium">Monthly Debt Payments</label> <label className="block font-medium">Monthly Debt Payments</label>
<input type="number" value={monthlyDebtPayments} onChange={handleInput(setMonthlyDebtPayments)} className="w-full border rounded p-2" placeholder="$" /> <input
type="number"
value={monthlyDebtPayments}
onChange={(e) => setMonthlyDebtPayments(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<label className="block font-medium">Retirement Savings</label> <label className="block font-medium">Retirement Savings</label>
<input type="number" value={retirementSavings} onChange={handleInput(setRetirementSavings)} className="w-full border rounded p-2" placeholder="$" /> <input
type="number"
value={retirementSavings}
onChange={(e) => setRetirementSavings(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<label className="block font-medium">Emergency Fund</label>
<input
type="number"
value={emergencyFund}
onChange={(e) => setEmergencyFund(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<label className="block font-medium">Monthly Retirement Contribution</label> <label className="block font-medium">Monthly Retirement Contribution</label>
<input type="number" value={retirementContribution} onChange={handleInput(setRetirementContribution)} className="w-full border rounded p-2" placeholder="$" /> <input
type="number"
value={retirementContribution}
onChange={(e) => setRetirementContribution(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<label className="block font-medium">Emergency Fund Balance</label> <label className="block font-medium">Monthly Emergency Contribution</label>
<input type="number" value={emergencyFund} onChange={handleInput(setEmergencyFund)} className="w-full border rounded p-2" placeholder="$" /> <input
type="number"
value={monthlyEmergencyContribution}
onChange={(e) => setMonthlyEmergencyContribution(e.target.value)}
className="w-full border rounded p-2"
placeholder="$"
/>
<div className="flex items-center space-x-2"> <label className="block font-medium">Extra Cash to Emergency (%)</label>
<input id="inCollege" type="checkbox" checked={inCollege} onChange={(e) => setInCollege(e.target.checked)} /> <input
<label htmlFor="inCollege" className="font-medium">Are you currently in college?</label> type="number"
</div> value={extraCashEmergencyPct}
onChange={(e) => setExtraCashEmergencyPct(e.target.value)}
className="w-full border rounded p-2"
placeholder="e.g. 30"
/>
{inCollege && ( <label className="block font-medium">Extra Cash to Retirement (%)</label>
<> <input
{/* Selected School input with suggestions */} type="number"
<label className="block font-medium">Selected School</label> value={extraCashRetirementPct}
<input onChange={(e) => setExtraCashRetirementPct(e.target.value)}
type="text" className="w-full border rounded p-2"
value={selectedSchool} placeholder="e.g. 70"
onChange={handleSchoolChange} />
className="w-full border rounded p-2"
placeholder="Search for a School"
/>
{schoolSuggestions.length > 0 && (
<ul className="border rounded bg-white max-h-40 overflow-y-auto shadow-md">
{schoolSuggestions.map((suggestion, idx) => (
<li
key={idx}
onClick={() => handleSchoolSelect(suggestion)}
className="p-2 hover:bg-blue-100 cursor-pointer"
>
{suggestion}
</li>
))}
</ul>
)}
{/* Program input with suggestions */} <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
<label className="block font-medium">Program</label> Save and Continue
<input </button>
type="text"
value={selectedProgram}
onChange={handleProgramChange}
className="w-full border rounded p-2"
placeholder="Search for a Program"
/>
{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={() => handleProgramSelect(suggestion)}
className="p-2 hover:bg-blue-100 cursor-pointer"
>
{suggestion}
</li>
))}
</ul>
)}
{/* Program Type input */}
<label className="block font-medium">Program Type</label>
<select
value={programType}
onChange={handleProgramTypeSelect}
className="w-full border rounded p-2"
>
<option value="">Select Program Type</option>
{availableProgramTypes.map((type, idx) => (
<option key={idx} value={type}>
{type}
</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)} />
<label htmlFor="isInState" className="font-medium">In-State Tuition</label>
</div>
<div className="flex items-center space-x-2">
<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"
value={creditHoursPerYear}
onChange={(e) => setCreditHoursPerYear(e.target.value)}
className="w-full border rounded p-2"
placeholder="e.g. 30"
/>
<label className="block font-medium">Calculated Yearly Tuition (override if needed)</label>
<input
type="number"
value={manualTuition || calculatedTuition}
onChange={handleTuitionInput}
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="$"
/>
</>
<div className="pt-4">
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Save and Continue
</button>
</div>
</form> </form>
); );
} }

View File

@ -41,27 +41,18 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
useEffect(() => { useEffect(() => {
const fetchCareerPaths = async () => { const fetchCareerPaths = async () => {
const res = await authFetch(`${apiURL}/premium/planned-path/all`); const res = await authFetch(`${apiURL}/premium/career-profile/all`);
if (!res) return; if (!res) return;
const data = await res.json(); const data = await res.json();
const { careerPaths } = data;
// Flatten nested array setExistingCareerPaths(careerPaths);
const flatPaths = data.careerPath.flat();
// Handle duplicates
const uniquePaths = Array.from(
new Set(flatPaths.map(cp => cp.career_name))
).map(name => flatPaths.find(cp => cp.career_name === name));
setExistingCareerPaths(uniquePaths);
const fromPopout = location.state?.selectedCareer; const fromPopout = location.state?.selectedCareer;
if (fromPopout) { if (fromPopout) {
setSelectedCareer(fromPopout); setSelectedCareer(fromPopout);
setCareerPathId(fromPopout.career_path_id); setCareerPathId(fromPopout.career_path_id);
} else if (!selectedCareer) { } else if (!selectedCareer) {
const latest = await authFetch(`${apiURL}/premium/planned-path/latest`); const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
if (latest) { if (latest) {
const latestData = await latest.json(); const latestData = await latest.json();
if (latestData?.id) { if (latestData?.id) {
@ -152,12 +143,16 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const handleConfirmCareerSelection = async () => { const handleConfirmCareerSelection = async () => {
const newId = uuidv4(); const newId = uuidv4();
const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] }; const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] };
const res = await authFetch(`${apiURL}/premium/planned-path`, { method: 'POST', body: JSON.stringify(body) }); const res = await authFetch(`${apiURL}/premium/career-profile`, { method: 'POST', body: JSON.stringify(body) });
if (!res || !res.ok) return; if (!res || !res.ok) return;
setSelectedCareer({ career_name: pendingCareerForModal }); const result = await res.json();
setCareerPathId(newId); setCareerPathId(result.career_path_id);
setPendingCareerForModal(null); setSelectedCareer({
}; career_name: pendingCareerForModal,
id: result.career_path_id
});
setPendingCareerForModal(null);
};
return ( return (
<div className="milestone-tracker"> <div className="milestone-tracker">

View File

@ -1,34 +1,175 @@
// CareerOnboarding.js // CareerOnboarding.js (inline implementation of career search)
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Input } from '../ui/input.js'; // Ensure path matches your structure
import authFetch from '../../utils/authFetch.js';
const CareerOnboarding = ({ nextStep, prevStep }) => { const apiURL = process.env.REACT_APP_API_URL;
const [careerData, setCareerData] = useState({
currentJob: '', const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
industry: '', const [userId, setUserId] = useState(null);
employmentStatus: '', const [currentlyWorking, setCurrentlyWorking] = useState('');
careerGoal: '', const [selectedCareer, setSelectedCareer] = useState('');
}); const [careerPathId, setCareerPathId] = useState(null);
const [collegeEnrollmentStatus, setCollegeEnrollmentStatus] = useState('');
const [careers, setCareers] = useState([]);
const [searchInput, setSearchInput] = useState('');
useEffect(() => {
const storedUserId = localStorage.getItem('userId');
if (storedUserId) {
setUserId(storedUserId);
} else {
console.error('User ID not found in localStorage');
}
}, []);
// Fetch careers exactly once on mount
useEffect(() => {
const fetchCareerTitles = async () => {
try {
const response = await fetch('/career_clusters.json');
const data = await response.json();
const careerTitlesSet = new Set();
const clusters = Object.keys(data);
for (let i = 0; i < clusters.length; i++) {
const cluster = clusters[i];
const subdivisions = Object.keys(data[cluster]);
for (let j = 0; j < subdivisions.length; j++) {
const subdivision = subdivisions[j];
const careersArray = data[cluster][subdivision];
for (let k = 0; k < careersArray.length; k++) {
const careerObj = careersArray[k];
if (careerObj.title) {
careerTitlesSet.add(careerObj.title);
}
}
}
}
setCareers([...careerTitlesSet]);
} catch (error) {
console.error("Error fetching or processing career_clusters.json:", error);
}
};
fetchCareerTitles();
}, []);
// Update career selection automatically whenever the searchInput matches a valid career explicitly
useEffect(() => {
if (careers.includes(searchInput)) {
setSelectedCareer(searchInput);
setData(prev => ({ ...prev, career_name: searchInput }));
}
}, [searchInput, careers, setData]);
const handleChange = (e) => { const handleChange = (e) => {
setCareerData({ ...careerData, [e.target.name]: e.target.value }); setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
}; };
const handleCareerInputChange = (e) => {
const inputValue = e.target.value;
setSearchInput(inputValue);
// only set explicitly when an exact match occurs
if (careers.includes(inputValue)) {
setSelectedCareer(inputValue);
setData(prev => ({ ...prev, career_name: inputValue }));
}
};
const handleSubmit = () => {
if (!selectedCareer || !currentlyWorking || !collegeEnrollmentStatus) {
alert("Please complete all required fields before continuing.");
return;
}
setData(prevData => ({
...prevData,
career_name: selectedCareer,
college_enrollment_status: collegeEnrollmentStatus,
currently_working: currentlyWorking,
status: prevData.status || 'planned',
start_date: prevData.start_date || new Date().toISOString(),
projected_end_date: prevData.projected_end_date || null,
user_id: userId
}));
nextStep();
};
return ( return (
<div> <div>
<h2>Career Details</h2> <h2>Career Details</h2>
<input name="currentJob" placeholder="Current Job Title" onChange={handleChange} />
<input name="industry" placeholder="Industry" onChange={handleChange} /> <label className="block font-medium">
<select name="employmentStatus" onChange={handleChange}> Are you currently working or earning any income (even part-time)?
<option value="">Employment Status</option> </label>
<option value="FT">Full-Time</option> <select
<option value="PT">Part-Time</option> value={currentlyWorking}
<option value="Unemployed">Unemployed</option> onChange={(e) => setCurrentlyWorking(e.target.value)}
<option value="Student">Student</option> className="w-full border rounded p-2"
>
<option value="">Select one</option>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
<div>
<h3>Search for Career</h3>
<input
value={searchInput}
onChange={handleCareerInputChange}
placeholder="Start typing a career..."
list="career-titles"
/>
<datalist id="career-titles">
{careers.map((career, index) => (
<option key={index} value={career} />
))}
</datalist>
</div>
{selectedCareer && <p>Selected Career: <strong>{selectedCareer}</strong></p>}
<label>Status:</label>
<select name="status" onChange={handleChange} value={data.status || ''}>
<option value="">Select Status</option>
<option value="Planned">Planned</option>
<option value="Current">Current</option>
<option value="Exploring">Exploring</option>
</select>
<label>Career Start Date:</label>
<input name="start_date" type="date" onChange={handleChange} value={data.start_date || ''} />
<label>Projected End Date (optional):</label>
<input name="projected_end_date" type="date" onChange={handleChange} value={data.projected_end_date || ''} />
<label className="block font-medium">
Are you currently enrolled in college or planning to enroll?
</label>
<select
value={collegeEnrollmentStatus}
onChange={(e) => setCollegeEnrollmentStatus(e.target.value)}
className="w-full border rounded p-2"
>
<option value="">Select one</option>
<option value="not_enrolled">Not Enrolled / Not Planning</option>
<option value="currently_enrolled">Currently Enrolled</option>
<option value="prospective_student">Planning to Enroll (Prospective Student)</option>
</select> </select>
<input name="careerGoal" placeholder="Career Goal (optional)" onChange={handleChange} />
<button onClick={prevStep}>Back</button> <button onClick={prevStep}>Back</button>
<button onClick={nextStep}>Next: Financial </button> <button onClick={handleSubmit}>Financial </button>
</div> </div>
); );
}; };

View File

@ -1,35 +1,558 @@
// CollegeOnboarding.js import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
const CollegeOnboarding = ({ nextStep, prevStep }) => { function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
const [collegeData, setCollegeData] = useState({ // CIP / iPEDS local states (purely for CIP data and suggestions)
studentStatus: '', const [schoolData, setSchoolData] = useState([]);
school: '', const [icTuitionData, setIcTuitionData] = useState([]);
program: '', const [schoolSuggestions, setSchoolSuggestions] = useState([]);
creditHoursPerYear: '', const [programSuggestions, setProgramSuggestions] = useState([]);
}); const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
const handleChange = (e) => { // ---- DESCTRUCTURE PARENT DATA FOR ALL FIELDS EXCEPT TUITION/PROGRAM_LENGTH ----
setCollegeData({ ...collegeData, [e.target.name]: e.target.value }); // We'll store user "typed" values for tuition/program_length in local states,
// but everything else comes directly from `data`.
const {
college_enrollment_status = '',
selected_school = '',
selected_program = '',
program_type = '',
academic_calendar = 'semester',
annual_financial_aid = '',
is_online = false,
existing_college_debt = '',
expected_graduation = '',
interest_rate = 5.5,
loan_term = 10,
extra_payment = '',
expected_salary = '',
is_in_state = true,
is_in_district = false,
loan_deferral_until_graduation = false,
credit_hours_per_year = '',
hours_completed = '',
credit_hours_required = '',
tuition_paid = '',
// We do NOT consume data.tuition or data.program_length directly here
// because we store them in local states (manualTuition, manualProgramLength).
} = data;
// ---- 1. LOCAL STATES for auto/manual logic on TWO fields ----
// manualTuition: user typed override
// autoTuition: iPEDS calculation
const [manualTuition, setManualTuition] = useState(''); // '' means no manual override
const [autoTuition, setAutoTuition] = useState(0);
// same approach for program_length
const [manualProgramLength, setManualProgramLength] = useState('');
const [autoProgramLength, setAutoProgramLength] = useState('0.00');
// ------------------------------------------
// Universal handleChange for all parent fields
// ------------------------------------------
const handleParentFieldChange = (e) => {
const { name, value, type, checked } = e.target;
let val = value;
if (type === 'checkbox') {
val = checked;
}
// parse numeric fields that are NOT tuition or program_length
if (['interest_rate','loan_term','extra_payment','expected_salary'].includes(name)) {
val = parseFloat(val) || 0;
} else if (
['annual_financial_aid','existing_college_debt','credit_hours_per_year',
'hours_completed','credit_hours_required','tuition_paid']
.includes(name)
) {
val = val === '' ? '' : parseFloat(val);
}
setData(prev => ({ ...prev, [name]: val }));
}; };
// ------------------------------------------
// handleManualTuition, handleManualProgramLength
// for local fields
// ------------------------------------------
const handleManualTuitionChange = (e) => {
// user typed something => override
setManualTuition(e.target.value); // store as string for partial typing
};
const handleManualProgramLengthChange = (e) => {
setManualProgramLength(e.target.value);
};
// ------------------------------------------
// CIP Data fetch once
// ------------------------------------------
useEffect(() => {
async function fetchCipData() {
try {
const res = await fetch('/cip_institution_mapping_new.json');
const text = await res.text();
const lines = text.split('\n');
const parsed = lines.map(line => {
try { return JSON.parse(line); } catch { return null; }
}).filter(Boolean);
setSchoolData(parsed);
} catch (err) {
console.error("Failed to load CIP data:", err);
}
}
fetchCipData();
}, []);
// ------------------------------------------
// iPEDS Data fetch once
// ------------------------------------------
useEffect(() => {
async function fetchIpedsData() {
try {
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 dataRows = rows.slice(1).map(row =>
Object.fromEntries(row.map((val, idx) => [headers[idx], val]))
);
setIcTuitionData(dataRows);
} catch (err) {
console.error("Failed to load iPEDS data:", err);
}
}
fetchIpedsData();
}, []);
// ------------------------------------------
// handleSchoolChange, handleProgramChange, etc. => update parent fields
// ------------------------------------------
const handleSchoolChange = (e) => {
const value = e.target.value;
setData(prev => ({
...prev,
selected_school: value,
selected_program: '',
program_type: '',
credit_hours_required: '',
}));
// CIP suggestions
const filtered = schoolData.filter(s =>
s.INSTNM.toLowerCase().includes(value.toLowerCase())
);
const uniqueSchools = [...new Set(filtered.map(s => s.INSTNM))];
setSchoolSuggestions(uniqueSchools.slice(0, 10));
setProgramSuggestions([]);
setAvailableProgramTypes([]);
};
const handleSchoolSelect = (schoolName) => {
setData(prev => ({
...prev,
selected_school: schoolName,
selected_program: '',
program_type: '',
credit_hours_required: '',
}));
setSchoolSuggestions([]);
setProgramSuggestions([]);
setAvailableProgramTypes([]);
};
const handleProgramChange = (e) => {
const value = e.target.value;
setData(prev => ({ ...prev, selected_program: value }));
if (!value) {
setProgramSuggestions([]);
return;
}
const filtered = schoolData.filter(
s => s.INSTNM.toLowerCase() === selected_school.toLowerCase() &&
s.CIPDESC.toLowerCase().includes(value.toLowerCase())
);
const uniquePrograms = [...new Set(filtered.map(s => s.CIPDESC))];
setProgramSuggestions(uniquePrograms.slice(0, 10));
};
const handleProgramSelect = (prog) => {
setData(prev => ({ ...prev, selected_program: prog }));
setProgramSuggestions([]);
};
const handleProgramTypeSelect = (e) => {
const val = e.target.value;
setData(prev => ({
...prev,
program_type: val,
credit_hours_required: '',
}));
setManualProgramLength(''); // reset manual override
setAutoProgramLength('0.00');
};
// Once we have school+program, load possible program types
useEffect(() => {
if (!selected_program || !selected_school || !schoolData.length) return;
const possibleTypes = schoolData
.filter(
s => s.INSTNM.toLowerCase() === selected_school.toLowerCase() &&
s.CIPDESC === selected_program
)
.map(s => s.CREDDESC);
setAvailableProgramTypes([...new Set(possibleTypes)]);
}, [selected_program, selected_school, schoolData]);
// ------------------------------------------
// Auto-calc Tuition => store in local autoTuition
// ------------------------------------------
useEffect(() => {
// do we have enough to calc?
if (!icTuitionData.length) return;
if (!selected_school || !program_type || !credit_hours_per_year) return;
// find row
const found = schoolData.find(s => s.INSTNM.toLowerCase() === selected_school.toLowerCase());
if (!found) return;
const unitId = found.UNITID;
if (!unitId) return;
const match = icTuitionData.find(row => row.UNITID === unitId);
if (!match) return;
// grad or undergrad
const isGradOrProf = [
"Master's Degree",
"Doctoral Degree",
"Graduate/Professional Certificate",
"First Professional Degree"
].includes(program_type);
let partTimeRate = 0;
let fullTimeTuition = 0;
if (isGradOrProf) {
if (is_in_district) {
partTimeRate = parseFloat(match.HRCHG5 || 0);
fullTimeTuition = parseFloat(match.TUITION5 || 0);
} else if (is_in_state) {
partTimeRate = parseFloat(match.HRCHG6 || 0);
fullTimeTuition = parseFloat(match.TUITION6 || 0);
} else {
partTimeRate = parseFloat(match.HRCHG7 || 0);
fullTimeTuition = parseFloat(match.TUITION7 || 0);
}
} else {
// undergrad
if (is_in_district) {
partTimeRate = parseFloat(match.HRCHG1 || 0);
fullTimeTuition = parseFloat(match.TUITION1 || 0);
} else if (is_in_state) {
partTimeRate = parseFloat(match.HRCHG2 || 0);
fullTimeTuition = parseFloat(match.TUITION2 || 0);
} else {
partTimeRate = parseFloat(match.HRCHG3 || 0);
fullTimeTuition = parseFloat(match.TUITION3 || 0);
}
}
const chpy = parseFloat(credit_hours_per_year) || 0;
let estimate = 0;
// threshold
if (chpy < 24 && partTimeRate) {
estimate = partTimeRate * chpy;
} else {
estimate = fullTimeTuition;
}
setAutoTuition(Math.round(estimate));
// We do NOT auto-update parent's data. We'll do that in handleSubmit or if you prefer, you can store it in parent's data anyway.
}, [
icTuitionData, selected_school, program_type,
credit_hours_per_year, is_in_state, is_in_district, schoolData
]);
// ------------------------------------------
// Auto-calc Program Length => store in local autoProgramLength
// ------------------------------------------
useEffect(() => {
if (!program_type) return;
if (!hours_completed || !credit_hours_per_year) return;
let required = 0;
switch (program_type) {
case "Associate's Degree": required = 60; break;
case "Bachelor's Degree": required = 120; break;
case "Master's Degree": required = 60; break;
case "Doctoral Degree": required = 120; break;
case "First Professional Degree": required = 180; break;
case "Graduate/Professional Certificate":
required = parseInt(credit_hours_required, 10) || 0; break;
default:
required = parseInt(credit_hours_required, 10) || 0; break;
}
const remain = required - (parseInt(hours_completed, 10) || 0);
const yrs = remain / (parseFloat(credit_hours_per_year) || 1);
const calcLength = yrs.toFixed(2);
setAutoProgramLength(calcLength);
}, [program_type, hours_completed, credit_hours_per_year, credit_hours_required]);
// ------------------------------------------
// handleSubmit => merges final chosen values
// ------------------------------------------
const handleSubmit = () => {
// If user typed a manual value, we use that. If they left it blank,
// we use the autoTuition.
const chosenTuition = (manualTuition.trim() === '' ? autoTuition : parseFloat(manualTuition));
// Same for program length
const chosenProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
// Write them into parent's data
setData(prev => ({
...prev,
tuition: chosenTuition,
program_length: chosenProgramLength
}));
nextStep();
};
// The displayed tuition => (manualTuition !== '' ? manualTuition : autoTuition)
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
// The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength)
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
return ( return (
<div> <div>
<h2>College Plans (optional)</h2> <h2>College Details</h2>
<select name="studentStatus" onChange={handleChange}>
<option value="">Student Status</option>
<option value="Enrolled">Currently Enrolled</option>
<option value="Prospective">Prospective Student</option>
<option value="None">Not Applicable</option>
</select>
<input name="school" placeholder="School Name" onChange={handleChange} />
<input name="program" placeholder="Program Name" onChange={handleChange} />
<input name="creditHoursPerYear" placeholder="Credit Hours per Year" type="number" onChange={handleChange} />
<button onClick={prevStep}> Previous: Financial</button> {(college_enrollment_status === 'currently_enrolled' ||
<button onClick={nextStep}>Finish Onboarding</button> college_enrollment_status === 'prospective_student') ? (
<>
<label>School Name*</label>
<input
name="selected_school"
value={selected_school}
onChange={handleSchoolChange}
list="school-suggestions"
placeholder="Enter school name..."
/>
<datalist id="school-suggestions">
{schoolSuggestions.map((sch, idx) => (
<option
key={idx}
value={sch}
onClick={() => handleSchoolSelect(sch)}
/>
))}
</datalist>
<label>Program Name*</label>
<input
name="selected_program"
value={selected_program}
onChange={handleProgramChange}
list="program-suggestions"
placeholder="Enter program name..."
/>
<datalist id="program-suggestions">
{programSuggestions.map((prog, idx) => (
<option
key={idx}
value={prog}
onClick={() => handleProgramSelect(prog)}
/>
))}
</datalist>
<label>Program Type*</label>
<select
name="program_type"
value={program_type}
onChange={handleProgramTypeSelect}
>
<option value="">Select Program Type</option>
{availableProgramTypes.map((t, i) => (
<option key={i} value={t}>{t}</option>
))}
</select>
{(program_type === 'Graduate/Professional Certificate' ||
program_type === 'First Professional Degree' ||
program_type === 'Doctoral Degree') && (
<>
<label>Credit Hours Required</label>
<input
type="number"
name="credit_hours_required"
value={credit_hours_required}
onChange={handleParentFieldChange}
placeholder="e.g. 30"
/>
</>
)}
<label>Credit Hours Per Year</label>
<input
type="number"
name="credit_hours_per_year"
value={credit_hours_per_year}
onChange={handleParentFieldChange}
placeholder="e.g. 24"
/>
<label>Yearly Tuition</label>
{/* If user typed a custom value => manualTuition, else autoTuition */}
<input
type="number"
value={displayedTuition}
onChange={handleManualTuitionChange}
placeholder="Leave blank to use auto, or type an override"
/>
<label>Annual Financial Aid</label>
<input
type="number"
name="annual_financial_aid"
value={annual_financial_aid}
onChange={handleParentFieldChange}
placeholder="e.g. 2000"
/>
<label>Existing College Loan Debt</label>
<input
type="number"
name="existing_college_debt"
value={existing_college_debt}
onChange={handleParentFieldChange}
placeholder="e.g. 2000"
/>
{college_enrollment_status === 'currently_enrolled' && (
<>
<label>Tuition Paid</label>
<input
type="number"
name="tuition_paid"
value={tuition_paid}
onChange={handleParentFieldChange}
placeholder="Already paid"
/>
<label>Hours Completed</label>
<input
type="number"
name="hours_completed"
value={hours_completed}
onChange={handleParentFieldChange}
placeholder="Credit hours done"
/>
<label>Program Length</label>
{/* If user typed a custom => manualProgramLength, else autoProgramLength */}
<input
type="number"
value={displayedProgramLength}
onChange={handleManualProgramLengthChange}
placeholder="Leave blank to use auto, or type an override"
/>
</>
)}
<label>Expected Graduation</label>
<input
type="date"
name="expected_graduation"
value={expected_graduation}
onChange={handleParentFieldChange}
/>
<label>Loan Interest Rate (%)</label>
<input
type="number"
name="interest_rate"
value={interest_rate}
onChange={handleParentFieldChange}
placeholder="e.g. 5.5"
/>
<label>Loan Term (years)</label>
<input
type="number"
name="loan_term"
value={loan_term}
onChange={handleParentFieldChange}
placeholder="e.g. 10"
/>
<label>Extra Monthly Payment</label>
<input
type="number"
name="extra_payment"
value={extra_payment}
onChange={handleParentFieldChange}
placeholder="Optional"
/>
<label>Expected Salary After Graduation</label>
<input
type="number"
name="expected_salary"
value={expected_salary}
onChange={handleParentFieldChange}
placeholder="e.g. 65000"
/>
<label>
<input
type="checkbox"
name="is_in_district"
checked={is_in_district}
onChange={handleParentFieldChange}
/>
In District?
</label>
<label>
<input
type="checkbox"
name="is_in_state"
checked={is_in_state}
onChange={handleParentFieldChange}
/>
In State Tuition?
</label>
<label>
<input
type="checkbox"
name="is_online"
checked={is_online}
onChange={handleParentFieldChange}
/>
Program is Fully Online
</label>
<label>
<input
type="checkbox"
name="loan_deferral_until_graduation"
checked={loan_deferral_until_graduation}
onChange={handleParentFieldChange}
/>
Defer Loan Payments until Graduation?
</label>
</>
) : (
<p>Not currently enrolled or prospective student. Skipping college onboarding.</p>
)}
<button onClick={prevStep} style={{ marginRight: '1rem' }}> Previous</button>
<button onClick={handleSubmit}>Finish Onboarding</button>
</div> </div>
); );
}; }
export default CollegeOnboarding; export default CollegeOnboarding;

View File

@ -1,25 +1,138 @@
// FinancialOnboarding.js import React from 'react';
import React, { useState } from 'react';
const FinancialOnboarding = ({ nextStep, prevStep }) => { const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
const [financialData, setFinancialData] = useState({ const {
salary: '', currently_working = '',
expenses: '', current_salary = 0,
savings: '', additional_income = 0,
debts: '', monthly_expenses = 0,
}); monthly_debt_payments = 0,
retirement_savings = 0,
retirement_contribution = 0,
emergency_fund = 0,
emergency_contribution = 0,
extra_cash_emergency_pct = "",
extra_cash_retirement_pct = ""
} = data;
const handleChange = (e) => { const handleChange = (e) => {
setFinancialData({ ...financialData, [e.target.name]: e.target.value }); const { name, value } = e.target;
let val = parseFloat(value) || 0;
if (name === 'extra_cash_emergency_pct') {
val = Math.min(Math.max(val, 0), 100);
setData(prevData => ({
...prevData,
extra_cash_emergency_pct: val,
extra_cash_retirement_pct: 100 - val
}));
} else if (name === 'extra_cash_retirement_pct') {
val = Math.min(Math.max(val, 0), 100);
setData(prevData => ({
...prevData,
extra_cash_retirement_pct: val,
extra_cash_emergency_pct: 100 - val
}));
} else {
setData(prevData => ({
...prevData,
[name]: val
}));
}
}; };
return ( return (
<div> <div>
<h2>Financial Details</h2> <h2>Financial Details</h2>
<input name="salary" placeholder="Annual Salary" type="number" onChange={handleChange} />
<input name="expenses" placeholder="Monthly Expenses" type="number" onChange={handleChange} /> {currently_working === 'yes' && (
<input name="savings" placeholder="Total Savings" type="number" onChange={handleChange} /> <>
<input name="debts" placeholder="Outstanding Debts" type="number" onChange={handleChange} /> <input
name="current_salary"
type="number"
placeholder="Current Annual Salary"
value={current_salary || ''} // controlled
onChange={handleChange}
/>
<input
name="additional_income"
type="number"
placeholder="Additional Annual Income (Investments, annuitites, additional jobs, etc. - optional)"
value={additional_income || ''}
onChange={handleChange}
/>
</>
)}
<input
name="monthly_expenses"
type="number"
placeholder="Monthly Expenses"
value={monthly_expenses || ''}
onChange={handleChange}
/>
<input
name="monthly_debt_payments"
type="number"
placeholder="Monthly Debt Payments (optional)"
value={monthly_debt_payments || ''}
onChange={handleChange}
/>
<input
name="retirement_savings"
type="number"
placeholder="Retirement Savings"
value={retirement_savings || ''}
onChange={handleChange}
/>
<input
name="retirement_contribution"
type="number"
placeholder="Monthly Retirement Contribution"
value={retirement_contribution || ''}
onChange={handleChange}
/>
<input
name="emergency_fund"
type="number"
placeholder="Emergency Fund Savings"
value={emergency_fund || ''}
onChange={handleChange}
/>
<input
name="emergency_contribution"
type="number"
placeholder="Monthly Emergency Fund Contribution (optional)"
value={emergency_contribution || ''}
onChange={handleChange}
/>
<h3>Extra Monthly Cash Allocation</h3>
<p>If you have extra money left each month after expenses, how would you like to allocate it? (Must add to 100%)</p>
<label>Extra Monthly Cash to Emergency Fund (%)</label>
<input
name="extra_cash_emergency_pct"
type="number"
placeholder="% to Emergency Savings (e.g., 30)"
value={extra_cash_emergency_pct}
onChange={handleChange}
/>
<label>Extra Monthly Cash to Retirement Fund (%)</label>
<input
name="extra_cash_retirement_pct"
type="number"
placeholder="% to Retirement Savings (e.g., 70)"
value={extra_cash_retirement_pct}
onChange={handleChange}
/>
<button onClick={prevStep}> Previous: Career</button> <button onClick={prevStep}> Previous: Career</button>
<button onClick={nextStep}>Next: College </button> <button onClick={nextStep}>Next: College </button>

View File

@ -1,28 +1,91 @@
// OnboardingContainer.js // OnboardingContainer.js
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import PremiumWelcome from './PremiumWelcome.js'; import PremiumWelcome from './PremiumWelcome.js';
import CareerOnboarding from './CareerOnboarding.js'; import CareerOnboarding from './CareerOnboarding.js';
import FinancialOnboarding from './FinancialOnboarding.js'; import FinancialOnboarding from './FinancialOnboarding.js';
import CollegeOnboarding from './CollegeOnboarding.js'; import CollegeOnboarding from './CollegeOnboarding.js';
import authFetch from '../../utils/authFetch.js';
import { useNavigate } from 'react-router-dom';
const OnboardingContainer = () => { const OnboardingContainer = () => {
console.log('OnboardingContainer MOUNT');
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [careerData, setCareerData] = useState({});
const [financialData, setFinancialData] = useState({});
const [collegeData, setCollegeData] = useState({});
const navigate = useNavigate();
const nextStep = () => setStep(step + 1); const nextStep = () => setStep(step + 1);
const prevStep = () => setStep(step - 1); const prevStep = () => setStep(step - 1);
const submitData = async () => {
await authFetch('/api/premium/career-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(careerData),
});
await authFetch('/api/premium/financial-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(financialData),
});
await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(collegeData),
});
navigate('/milestone-tracker');
};
console.log('collegeData to submit:', collegeData);
useEffect(() => {
return () => console.log('OnboardingContainer UNMOUNT');
}, []);
// Merge the parent's collegeData with the override from careerData
const mergedCollegeData = {
...collegeData,
// If careerData has a truthy enrollment_status, override
college_enrollment_status:
careerData.college_enrollment_status ?? collegeData.college_enrollment_status
};
const onboardingSteps = [ const onboardingSteps = [
<PremiumWelcome nextStep={nextStep} />, <PremiumWelcome nextStep={nextStep} />,
<CareerOnboarding nextStep={nextStep} prevStep={prevStep} />,
<FinancialOnboarding nextStep={nextStep} prevStep={prevStep} />, <CareerOnboarding
<CollegeOnboarding nextStep={nextStep} prevStep={prevStep} />, nextStep={nextStep}
data={careerData}
setData={setCareerData}
/>,
<FinancialOnboarding
nextStep={nextStep}
prevStep={prevStep}
data={{
...financialData,
currently_working: careerData.currently_working,
}}
setData={setFinancialData}
/>,
<CollegeOnboarding
nextStep={submitData}
prevStep={prevStep}
// Pass the merged data so that college_enrollment_status is never lost
data={mergedCollegeData}
setData={setCollegeData}
/>
]; ];
return ( return <div>{onboardingSteps[step]}</div>;
<div className="onboarding-container">
{onboardingSteps[step]}
</div>
);
}; };
export default OnboardingContainer; export default OnboardingContainer;

View File

@ -1,137 +1,318 @@
import moment from 'moment'; import moment from 'moment';
// Function to simulate monthly financial projection // Example fields in userProfile that matter here:
// src/utils/FinancialProjectionService.js // - academicCalendar: 'semester' | 'quarter' | 'trimester' | 'monthly'
// - annualFinancialAid: amount of scholarships/grants per year
// - inCollege, loanDeferralUntilGraduation, graduationDate, etc.
//
// Additional logic now for lumps instead of monthly tuition payments.
export function simulateFinancialProjection(userProfile) { export function simulateFinancialProjection(userProfile) {
const { const {
currentSalary, // Income & expenses
monthlyExpenses, currentSalary = 0,
monthlyDebtPayments, monthlyExpenses = 0,
studentLoanAmount, monthlyDebtPayments = 0,
interestRate, // ✅ Corrected partTimeIncome = 0,
loanTerm, // ✅ Corrected extraPayment = 0,
extraPayment,
expectedSalary,
emergencySavings,
retirementSavings,
monthlyRetirementContribution,
monthlyEmergencyContribution,
gradDate,
fullTimeCollegeStudent: inCollege,
partTimeIncome,
startDate,
programType,
isFullyOnline,
creditHoursPerYear,
calculatedTuition,
hoursCompleted,
loanDeferralUntilGraduation,
programLength
} = userProfile;
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); // Loan info
studentLoanAmount = 0,
interestRate = 5, // %
loanTerm = 10, // years
loanDeferralUntilGraduation = false,
let totalEmergencySavings = emergencySavings; // College & tuition
let totalRetirementSavings = retirementSavings; inCollege = false,
let loanBalance = studentLoanAmount; programType,
let projectionData = []; hoursCompleted = 0,
creditHoursPerYear = 30,
calculatedTuition = 10000, // e.g. annual tuition
gradDate, // known graduation date, or null
startDate, // when sim starts
academicCalendar = 'monthly', // new
annualFinancialAid = 0,
const graduationDate = gradDate ? new Date(gradDate) : null; // Salary after graduation
let milestoneIndex = 0; expectedSalary = 0,
let loanPaidOffMonth = null;
// Dynamic credit hours based on the program type // Savings
let requiredCreditHours; emergencySavings = 0,
switch (programType) { retirementSavings = 0,
case "Associate Degree":
requiredCreditHours = 60; // Monthly contributions
break; monthlyRetirementContribution = 0,
case "Bachelor's Degree": monthlyEmergencyContribution = 0,
requiredCreditHours = 120;
break; // Surplus allocation
case "Master's Degree": surplusEmergencyAllocation = 50,
requiredCreditHours = 30; surplusRetirementAllocation = 50,
break;
case "Doctoral Degree": // Potential override
requiredCreditHours = 60; programLength
break; } = userProfile;
default:
requiredCreditHours = 120; // 1. Calculate standard monthly loan payment
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
// 2. Determine how many credit hours remain
let requiredCreditHours = 120;
switch (programType) {
case "Associate's 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 = Math.max(0, requiredCreditHours - hoursCompleted);
const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear);
const finalProgramLength = programLength || dynamicProgramLength;
// 3. Net annual tuition after financial aid
const netAnnualTuition = Math.max(0, calculatedTuition - annualFinancialAid);
const totalTuitionCost = netAnnualTuition * finalProgramLength;
// 4. Setup lumps per year based on academicCalendar
let lumpsPerYear = 12; // monthly fallback
let lumpsSchedule = []; // which months from start of academic year
// We'll store an array of month offsets in a single year (0-based)
// for semester, quarter, trimester
switch (academicCalendar) {
case 'semester':
lumpsPerYear = 2;
lumpsSchedule = [0, 6]; // months 0 & 6 from start of each academic year
break;
case 'quarter':
lumpsPerYear = 4;
lumpsSchedule = [0, 3, 6, 9];
break;
case 'trimester':
lumpsPerYear = 3;
lumpsSchedule = [0, 4, 8];
break;
case 'monthly':
default:
lumpsPerYear = 12;
lumpsSchedule = [...Array(12).keys()]; // 0..11
break;
}
// Each academic year is 12 months, for finalProgramLength years => totalAcademicMonths
const totalAcademicMonths = finalProgramLength * 12;
// Each lump sum = totalTuitionCost / (lumpsPerYear * finalProgramLength)
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
// 5. We'll loop for up to 20 years
const maxMonths = 240;
let date = startDate ? new Date(startDate) : new Date();
let loanBalance = studentLoanAmount;
let loanPaidOffMonth = null;
let currentEmergencySavings = emergencySavings;
let currentRetirementSavings = retirementSavings;
let projectionData = [];
// Convert gradDate to actual if present
const graduationDate = gradDate ? new Date(gradDate) : null;
for (let month = 0; month < maxMonths; month++) {
date.setMonth(date.getMonth() + 1);
// If loan is fully paid, record if not done already
if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
} }
const remainingCreditHours = requiredCreditHours - hoursCompleted; // Are we still in college? We either trust gradDate or approximate finalProgramLength
const programDuration = Math.ceil(remainingCreditHours / creditHoursPerYear); let stillInCollege = false;
const tuitionCost = calculatedTuition; if (inCollege) {
const totalTuitionCost = tuitionCost * programDuration; if (graduationDate) {
stillInCollege = date < graduationDate;
} else {
// approximate by how many months since start
const simStart = startDate ? new Date(startDate) : new Date();
const elapsedMonths =
(date.getFullYear() - simStart.getFullYear()) * 12 +
(date.getMonth() - simStart.getMonth());
stillInCollege = (elapsedMonths < totalAcademicMonths);
}
}
const date = new Date(startDate); // 6. If we pay lumps: check if this is a "lump" month within the user's academic year
for (let month = 0; month < 240; month++) { // We'll find how many academic years have passed since they started
date.setMonth(date.getMonth() + 1); let tuitionCostThisMonth = 0;
if (stillInCollege && lumpsPerYear > 0) {
const simStart = startDate ? new Date(startDate) : new Date();
const elapsedMonths =
(date.getFullYear() - simStart.getFullYear()) * 12 +
(date.getMonth() - simStart.getMonth());
if (loanBalance <= 0 && !loanPaidOffMonth) { // Which academic year index are we in?
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; const academicYearIndex = Math.floor(elapsedMonths / 12);
} // Within that year, which month are we in? (0..11)
const monthInYear = elapsedMonths % 12;
let tuitionCostThisMonth = 0; // If we find monthInYear in lumpsSchedule, then lumps are due
if (inCollege && !loanDeferralUntilGraduation) { if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
tuitionCostThisMonth = totalTuitionCost / programDuration / 12; tuitionCostThisMonth = lumpAmount;
} }
}
let thisMonthLoanPayment = 0; // 7. Decide if user defers or pays out of pocket
// If deferring, add lumps to loan
if (stillInCollege && loanDeferralUntilGraduation) {
// Instead of user paying out of pocket, add to loan
if (tuitionCostThisMonth > 0) {
loanBalance += tuitionCostThisMonth;
tuitionCostThisMonth = 0; // paid by the loan
}
}
if (loanDeferralUntilGraduation && graduationDate && date < graduationDate) { // 8. monthly income
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here let monthlyIncome = 0;
loanBalance += interestForMonth; if (!inCollege || !stillInCollege) {
} else if (loanBalance > 0) { // user has graduated or never in college
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here monthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
const principalForMonth = Math.min(loanBalance, monthlyLoanPayment + extraPayment - interestForMonth); } else {
loanBalance -= principalForMonth; // in college => currentSalary + partTimeIncome
loanBalance = Math.max(loanBalance, 0); monthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
thisMonthLoanPayment = monthlyLoanPayment + extraPayment; }
}
const salaryNow = graduationDate && date >= graduationDate ? expectedSalary : currentSalary; // 9. mandatory expenses (excluding student loan if deferring)
let thisMonthLoanPayment = 0;
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth;
const totalMonthlyExpenses = monthlyExpenses if (stillInCollege && loanDeferralUntilGraduation) {
+ tuitionCostThisMonth // Accrue interest only
+ monthlyDebtPayments const interestForMonth = loanBalance * (interestRate / 100 / 12);
+ thisMonthLoanPayment; loanBalance += interestForMonth;
} else {
// Normal loan repayment if loan > 0
if (loanBalance > 0) {
const interestForMonth = loanBalance * (interestRate / 100 / 12);
const principalForMonth = Math.min(
loanBalance,
(monthlyLoanPayment + extraPayment) - interestForMonth
);
loanBalance -= principalForMonth;
loanBalance = Math.max(loanBalance, 0);
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
totalMonthlyExpenses += thisMonthLoanPayment;
}
}
const monthlyIncome = salaryNow / 12; // 10. leftover after mandatory expenses
let leftover = monthlyIncome - totalMonthlyExpenses;
if (leftover < 0) {
leftover = 0; // can't do partial negative leftover; they simply can't afford it
}
let extraCash = monthlyIncome - totalMonthlyExpenses - monthlyRetirementContribution - monthlyEmergencyContribution; // Baseline monthly contributions
extraCash = Math.max(extraCash, 0); const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
let effectiveRetirementContribution = 0;
let effectiveEmergencyContribution = 0;
// update savings explicitly with contributions first if (leftover >= baselineContributions) {
totalEmergencySavings += monthlyEmergencyContribution + (extraCash * 0.3); effectiveRetirementContribution = monthlyRetirementContribution;
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7); effectiveEmergencyContribution = monthlyEmergencyContribution;
totalRetirementSavings *= (1 + 0.07 / 12); leftover -= baselineContributions;
} else {
// not enough leftover
// for real life, we typically set them to 0 if we can't afford them
// or reduce proportionally. We'll do the simpler approach: set them to 0
// as requested.
effectiveRetirementContribution = 0;
effectiveEmergencyContribution = 0;
}
// netSavings calculation fixed // 11. Now see if leftover is negative => shortfall from mandatory expenses
projectionData.push({ // Actually we zeroed leftover if it was negative. So let's check if the user
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`, // truly can't afford mandatoryExpenses
salary: salaryNow, const totalMandatoryPlusContrib = monthlyIncome - leftover;
monthlyIncome: monthlyIncome, const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
expenses: totalMonthlyExpenses, const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
loanPayment: thisMonthLoanPayment, let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
retirementContribution: monthlyRetirementContribution, if (shortfall > 0) {
emergencyContribution: monthlyEmergencyContribution, // We can reduce from emergency savings
netSavings: monthlyIncome - totalMonthlyExpenses, // Exclude contributions here explicitly! const canCover = Math.min(shortfall, currentEmergencySavings);
totalEmergencySavings, currentEmergencySavings -= canCover;
totalRetirementSavings, shortfall -= canCover;
loanBalance if (shortfall > 0) {
}); // user is effectively bankrupt
// we can break out or keep going to show negative net worth
// For demonstration, let's break
break;
}
}
} // 12. If leftover > 0 after baseline contributions, allocate surplus
// (we do it after we've handled shortfall)
const newLeftover = leftover; // leftover not used for baseline
let surplusUsed = 0;
if (newLeftover > 0) {
// Allocate by percent
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
const emergencyPortion = newLeftover * (surplusEmergencyAllocation / totalPct);
const retirementPortion = newLeftover * (surplusRetirementAllocation / totalPct);
currentEmergencySavings += emergencyPortion;
currentRetirementSavings += retirementPortion;
surplusUsed = newLeftover;
}
return { projectionData, loanPaidOffMonth, emergencySavings }; // 13. netSavings is monthlyIncome - actual expenses - all contributions
} // But we must recalc actual final expenses paid
const finalExpensesPaid = totalMonthlyExpenses + (effectiveRetirementContribution + effectiveEmergencyContribution);
const netSavings = monthlyIncome - finalExpensesPaid;
function calculateLoanPayment(principal, annualRate, years) { projectionData.push({
const monthlyRate = annualRate / 100 / 12; month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
const numPayments = years * 12; monthlyIncome,
return (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -numPayments)); totalExpenses: finalExpensesPaid,
effectiveRetirementContribution,
effectiveEmergencyContribution,
netSavings,
emergencySavings: currentEmergencySavings,
retirementSavings: currentRetirementSavings,
loanBalance: Math.round(loanBalance * 100) / 100,
loanPaymentThisMonth: thisMonthLoanPayment
});
}
// Return final
return {
projectionData,
loanPaidOffMonth,
finalEmergencySavings: currentEmergencySavings,
finalRetirementSavings: currentRetirementSavings,
finalLoanBalance: Math.round(loanBalance * 100) / 100
};
} }
/**
* Calculate the standard monthly loan payment for principal, annualRate (%) and term (years)
*/
function calculateLoanPayment(principal, annualRate, years) {
if (principal <= 0) return 0;
const monthlyRate = annualRate / 100 / 12;
const numPayments = years * 12;
if (monthlyRate === 0) {
// no interest
return principal / numPayments;
}
return (
(principal * monthlyRate) /
(1 - Math.pow(1 + monthlyRate, -numPayments))
);
}

Binary file not shown.