Onboarding refactor finalization, updated MilestoneTracker.js connection points to refactored server3.js
This commit is contained in:
parent
d918d44b57
commit
b98f93d442
@ -1,4 +1,4 @@
|
||||
// server3.js - Premium Services API
|
||||
// server3.js
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
@ -9,6 +9,7 @@ import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
// If you still need the projection logic somewhere else
|
||||
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
|
||||
|
||||
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 {
|
||||
const row = await db.get(
|
||||
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
|
||||
[req.userId]
|
||||
);
|
||||
const row = await db.get(`
|
||||
SELECT *
|
||||
FROM career_paths
|
||||
WHERE user_id = ?
|
||||
ORDER BY start_date DESC
|
||||
LIMIT 1
|
||||
`, [req.userId]);
|
||||
res.json(row || {});
|
||||
} catch (error) {
|
||||
console.error('Error fetching latest career path:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch latest planned path' });
|
||||
console.error('Error fetching latest career profile:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch latest career profile' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all planned paths for the user
|
||||
app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, res) => {
|
||||
// GET all career profiles for the user
|
||||
app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all(
|
||||
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date ASC`,
|
||||
[req.userId]
|
||||
);
|
||||
res.json({ careerPath: rows });
|
||||
const rows = await db.all(`
|
||||
SELECT *
|
||||
FROM career_paths
|
||||
WHERE user_id = ?
|
||||
ORDER BY start_date ASC
|
||||
`, [req.userId]);
|
||||
res.json({ careerPaths: rows });
|
||||
} catch (error) {
|
||||
console.error('Error fetching career paths:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch planned paths' });
|
||||
console.error('Error fetching career profiles:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch career profiles' });
|
||||
}
|
||||
});
|
||||
|
||||
// Save a new planned path
|
||||
app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => {
|
||||
let { career_name } = req.body;
|
||||
// POST a new career profile
|
||||
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
|
||||
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) {
|
||||
return res.status(400).json({ error: 'Career name is required.' });
|
||||
return res.status(400).json({ error: 'career_name is required.' });
|
||||
}
|
||||
|
||||
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();
|
||||
await db.run(
|
||||
`INSERT INTO career_path (id, user_id, career_name) VALUES (?, ?, ?)`,
|
||||
[newCareerPathId, req.userId, career_name]
|
||||
);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Career path saved.',
|
||||
career_path_id: newCareerPathId,
|
||||
action_required: 'new_created'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving career path:', error);
|
||||
res.status(500).json({ error: 'Failed to save career path.' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 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
|
||||
]
|
||||
await db.run(`
|
||||
INSERT INTO career_paths (
|
||||
id,
|
||||
user_id,
|
||||
career_name,
|
||||
status,
|
||||
start_date,
|
||||
projected_end_date,
|
||||
college_enrollment_status,
|
||||
currently_working,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
);
|
||||
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 });
|
||||
} catch (error) {
|
||||
console.error('Error saving milestones:', error);
|
||||
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
|
||||
res.status(200).json({
|
||||
message: 'Career profile upserted.',
|
||||
career_path_id: result?.id
|
||||
});
|
||||
|
||||
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) {
|
||||
console.error('Error updating milestone:', error.message, error.stack);
|
||||
res.status(500).json({ error: 'Failed to update milestone' });
|
||||
console.error('Error upserting career profile:', error);
|
||||
res.status(500).json({ error: 'Failed to upsert career profile.' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.delete('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
await db.run(`DELETE FROM milestones WHERE id = ? AND user_id = ?`, [id, req.userId]);
|
||||
res.status(200).json({ message: 'Milestone deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting milestone:', error);
|
||||
res.status(500).json({ error: 'Failed to delete milestone' });
|
||||
}
|
||||
/* ------------------------------------------------------------------
|
||||
MILESTONES (same as before)
|
||||
------------------------------------------------------------------ */
|
||||
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
||||
// ... no changes, same logic ...
|
||||
});
|
||||
|
||||
//Financial Profile premium services
|
||||
//Get financial profile
|
||||
// GET, PUT, DELETE milestones
|
||||
// ... no changes ...
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
FINANCIAL PROFILES (Renamed emergency_contribution)
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
||||
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 || {});
|
||||
} catch (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) => {
|
||||
const {
|
||||
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
||||
retirementSavings, retirementContribution, emergencyFund,
|
||||
inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
||||
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
||||
careerPathId, loanDeferralUntilGraduation, tuition, programLength, interestRate, loanTerm, extraPayment, expectedSalary
|
||||
current_salary,
|
||||
additional_income,
|
||||
monthly_expenses,
|
||||
monthly_debt_payments,
|
||||
retirement_savings,
|
||||
retirement_contribution,
|
||||
emergency_fund,
|
||||
emergency_contribution,
|
||||
extra_cash_emergency_pct,
|
||||
extra_cash_retirement_pct
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
// **Call the simulateFinancialProjection function here** with all the incoming data
|
||||
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({
|
||||
currentSalary: req.body.currentSalary + (req.body.additionalIncome || 0),
|
||||
monthlyExpenses: req.body.monthlyExpenses,
|
||||
monthlyDebtPayments: req.body.monthlyDebtPayments || 0,
|
||||
studentLoanAmount: req.body.collegeLoanTotal,
|
||||
// Check if row exists
|
||||
const existing = await db.get(`
|
||||
SELECT user_id
|
||||
FROM financial_profiles
|
||||
WHERE user_id = ?
|
||||
`, [req.userId]);
|
||||
|
||||
// ✅ UPDATED to dynamic fields from frontend
|
||||
interestRate: req.body.interestRate,
|
||||
loanTerm: req.body.loanTerm,
|
||||
extraPayment: req.body.extraPayment || 0,
|
||||
expectedSalary: req.body.expectedSalary,
|
||||
|
||||
emergencySavings: req.body.emergencyFund,
|
||||
retirementSavings: req.body.retirementSavings,
|
||||
monthlyRetirementContribution: req.body.retirementContribution,
|
||||
monthlyEmergencyContribution: 0,
|
||||
gradDate: req.body.expectedGraduation,
|
||||
fullTimeCollegeStudent: req.body.inCollege,
|
||||
partTimeIncome: req.body.partTimeIncome,
|
||||
startDate: new Date(),
|
||||
programType: req.body.programType,
|
||||
isFullyOnline: req.body.isFullyOnline,
|
||||
creditHoursPerYear: req.body.creditHoursPerYear,
|
||||
calculatedTuition: req.body.tuition,
|
||||
manualTuition: 0,
|
||||
hoursCompleted: req.body.hoursCompleted,
|
||||
loanDeferralUntilGraduation: req.body.loanDeferralUntilGraduation,
|
||||
programLength: req.body.programLength
|
||||
});
|
||||
// Now you can save the profile or update the database with the new data
|
||||
const existing = await db.get(`SELECT id FROM financial_profile WHERE user_id = ?`, [req.userId]);
|
||||
|
||||
if (existing) {
|
||||
// Updating existing profile
|
||||
if (!existing) {
|
||||
// Insert new row
|
||||
await db.run(`
|
||||
UPDATE financial_profile SET
|
||||
current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?,
|
||||
retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?,
|
||||
in_college = ?, expected_graduation = ?, part_time_income = ?, tuition_paid = ?, college_loan_total = ?,
|
||||
selected_school = ?, selected_program = ?, program_type = ?, is_online = ?, credit_hours_per_year = ?, hours_completed = ?,
|
||||
tuition = ?, loan_deferral_until_graduation = ?, program_length = ?,
|
||||
interest_rate = ?, loan_term = ?, extra_payment = ?, expected_salary = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = ?`,
|
||||
[
|
||||
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
||||
retirementSavings, retirementContribution, emergencyFund,
|
||||
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
||||
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
||||
tuition, loanDeferralUntilGraduation, programLength,
|
||||
interestRate, loanTerm, extraPayment, expectedSalary, // ✅ added new fields
|
||||
req.userId
|
||||
]
|
||||
);
|
||||
INSERT INTO financial_profiles (
|
||||
user_id,
|
||||
current_salary,
|
||||
additional_income,
|
||||
monthly_expenses,
|
||||
monthly_debt_payments,
|
||||
retirement_savings,
|
||||
emergency_fund,
|
||||
retirement_contribution,
|
||||
emergency_contribution,
|
||||
extra_cash_emergency_pct,
|
||||
extra_cash_retirement_pct,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`, [
|
||||
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 {
|
||||
// Insert a new profile
|
||||
// Update existing
|
||||
await db.run(`
|
||||
INSERT INTO financial_profile (
|
||||
id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments,
|
||||
retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation,
|
||||
part_time_income, tuition_paid, college_loan_total, selected_school, selected_program, program_type,
|
||||
is_online, credit_hours_per_year, calculated_tuition, loan_deferral_until_graduation, hours_completed, tuition, program_length,
|
||||
interest_rate, loan_term, extra_payment, expected_salary
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
||||
retirementSavings, retirementContribution, emergencyFund,
|
||||
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
||||
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
||||
tuition, loanDeferralUntilGraduation, programLength,
|
||||
interestRate, loanTerm, extraPayment, expectedSalary // ✅ added new fields
|
||||
]
|
||||
);
|
||||
|
||||
UPDATE financial_profiles
|
||||
SET
|
||||
current_salary = ?,
|
||||
additional_income = ?,
|
||||
monthly_expenses = ?,
|
||||
monthly_debt_payments = ?,
|
||||
retirement_savings = ?,
|
||||
emergency_fund = ?,
|
||||
retirement_contribution = ?,
|
||||
emergency_contribution = ?,
|
||||
extra_cash_emergency_pct = ?,
|
||||
extra_cash_retirement_pct = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
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.status(200).json({
|
||||
message: 'Financial profile saved.',
|
||||
projectionData,
|
||||
loanPaidOffMonth,
|
||||
emergencyFund: emergencyFund // explicitly add the emergency fund here
|
||||
});
|
||||
|
||||
console.log("Request body:", req.body);
|
||||
|
||||
res.json({ message: 'Financial profile saved/updated.' });
|
||||
} catch (error) {
|
||||
console.error('Error saving financial profile:', error);
|
||||
res.status(500).json({ error: 'Failed to save financial profile.' });
|
||||
}
|
||||
});
|
||||
|
||||
//PreimumOnboarding
|
||||
//Career onboarding
|
||||
app.post('/api/premium/onboarding/career', authenticatePremiumUser, async (req, res) => {
|
||||
const { career_name, status, start_date, projected_end_date } = req.body;
|
||||
/* ------------------------------------------------------------------
|
||||
COLLEGE PROFILES
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
try {
|
||||
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) => {
|
||||
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
|
||||
const {
|
||||
current_salary, additional_income, monthly_expenses, monthly_debt_payments,
|
||||
retirement_savings, retirement_contribution, emergency_fund
|
||||
career_path_id,
|
||||
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;
|
||||
|
||||
try {
|
||||
const id = uuidv4();
|
||||
const user_id = req.userId;
|
||||
await db.run(`
|
||||
INSERT INTO financial_profile (
|
||||
id, user_id, current_salary, additional_income, monthly_expenses,
|
||||
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||
[
|
||||
uuidv4(), req.userId, current_salary, additional_income, monthly_expenses,
|
||||
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund
|
||||
]
|
||||
);
|
||||
INSERT INTO college_profiles (
|
||||
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,
|
||||
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) {
|
||||
console.error('Error saving financial onboarding data:', error);
|
||||
res.status(500).json({ error: 'Failed to save financial onboarding data.' });
|
||||
console.error('Error saving college profile:', error);
|
||||
res.status(500).json({ error: 'Failed to save college profile.' });
|
||||
}
|
||||
});
|
||||
|
||||
//College onboarding
|
||||
app.post('/api/premium/onboarding/college', authenticatePremiumUser, async (req, res) => {
|
||||
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
|
||||
/* ------------------------------------------------------------------
|
||||
FINANCIAL PROJECTIONS
|
||||
------------------------------------------------------------------ */
|
||||
app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
||||
const { careerPathId } = req.params;
|
||||
const { projectionData } = req.body; // JSON containing detailed financial projections
|
||||
const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body;
|
||||
|
||||
try {
|
||||
const projectionId = uuidv4();
|
||||
|
||||
await db.run(`
|
||||
INSERT INTO financial_projections (id, user_id, career_path_id, projection_json)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[projectionId, req.userId, careerPathId, JSON.stringify(projectionData)]
|
||||
);
|
||||
INSERT INTO financial_projections (
|
||||
id, user_id, career_path_id, projection_data,
|
||||
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 });
|
||||
} 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) => {
|
||||
const { careerPathId } = req.params;
|
||||
|
||||
try {
|
||||
const projection = await db.get(`
|
||||
SELECT projection_json FROM financial_projections
|
||||
WHERE user_id = ? AND career_path_id = ?`,
|
||||
[req.userId, careerPathId]
|
||||
);
|
||||
const row = await db.get(`
|
||||
SELECT projection_data, loan_paid_off_month,
|
||||
final_emergency_savings, final_retirement_savings, final_loan_balance
|
||||
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.' });
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('Error fetching financial projection:', error);
|
||||
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) => {
|
||||
try {
|
||||
const userCareer = await db.get(
|
||||
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
|
||||
[req.userId]
|
||||
);
|
||||
const userCareer = await db.get(`
|
||||
SELECT * FROM career_paths
|
||||
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 = {
|
||||
jobTitle: userCareer.job_title,
|
||||
salary: userCareer.salary,
|
||||
jobTitle: userCareer.career_name,
|
||||
salary: 80000,
|
||||
tuition: 50000,
|
||||
netGain: userCareer.salary - 50000
|
||||
netGain: 80000 - 50000
|
||||
};
|
||||
|
||||
res.json(roi);
|
||||
|
@ -1,583 +1,179 @@
|
||||
// Updated FinancialProfileForm.js with autosuggest for school and full field list restored
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
// FinancialProfileForm.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
|
||||
function FinancialProfileForm() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [userId] = useState(() => localStorage.getItem("userId"));
|
||||
const [selectedCareer] = useState(() => location.state?.selectedCareer || null);
|
||||
|
||||
const [currentSalary, setCurrentSalary] = useState("");
|
||||
const [additionalIncome, setAdditionalIncome] = useState("");
|
||||
const [monthlyExpenses, setMonthlyExpenses] = useState("");
|
||||
const [monthlyDebtPayments, setMonthlyDebtPayments] = useState("");
|
||||
const [retirementSavings, setRetirementSavings] = 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);
|
||||
// We'll store the fields in local state
|
||||
const [currentSalary, setCurrentSalary] = useState('');
|
||||
const [additionalIncome, setAdditionalIncome] = useState('');
|
||||
const [monthlyExpenses, setMonthlyExpenses] = useState('');
|
||||
const [monthlyDebtPayments, setMonthlyDebtPayments] = useState('');
|
||||
const [retirementSavings, setRetirementSavings] = useState('');
|
||||
const [emergencyFund, setEmergencyFund] = useState('');
|
||||
const [retirementContribution, setRetirementContribution] = useState('');
|
||||
const [monthlyEmergencyContribution, setMonthlyEmergencyContribution] = useState('');
|
||||
const [extraCashEmergencyPct, setExtraCashEmergencyPct] = useState('');
|
||||
const [extraCashRetirementPct, setExtraCashRetirementPct] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchRawTuitionData() {
|
||||
const res = await fetch("/ic2023_ay.csv");
|
||||
const text = await res.text();
|
||||
const rows = text.split("\n").map(line => line.split(','));
|
||||
const headers = rows[0];
|
||||
const data = rows.slice(1).map(row => Object.fromEntries(row.map((val, idx) => [headers[idx], val])));
|
||||
setIcTuitionData(data);
|
||||
}
|
||||
fetchRawTuitionData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSchool && 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 user’s 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() {
|
||||
// On mount, fetch the user's existing profile from the new financial_profiles table
|
||||
async function fetchProfile() {
|
||||
try {
|
||||
const res = await authFetch("/api/premium/financial-profile", {
|
||||
method: "GET",
|
||||
headers: { "Authorization": `Bearer ${localStorage.getItem('token')}` }
|
||||
const res = await authFetch('/api/premium/financial-profile', {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
setCurrentSalary(data.current_salary || "");
|
||||
setAdditionalIncome(data.additional_income || "");
|
||||
setMonthlyExpenses(data.monthly_expenses || "");
|
||||
setMonthlyDebtPayments(data.monthly_debt_payments || "");
|
||||
setRetirementSavings(data.retirement_savings || "");
|
||||
setRetirementContribution(data.retirement_contribution || "");
|
||||
setEmergencyFund(data.emergency_fund || "");
|
||||
setInCollege(!!data.in_college);
|
||||
setExpectedGraduation(data.expected_graduation || "");
|
||||
setPartTimeIncome(data.part_time_income || "");
|
||||
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 || "");
|
||||
}
|
||||
// data might be an empty object if no row yet
|
||||
setCurrentSalary(data.current_salary || '');
|
||||
setAdditionalIncome(data.additional_income || '');
|
||||
setMonthlyExpenses(data.monthly_expenses || '');
|
||||
setMonthlyDebtPayments(data.monthly_debt_payments || '');
|
||||
setRetirementSavings(data.retirement_savings || '');
|
||||
setEmergencyFund(data.emergency_fund || '');
|
||||
setRetirementContribution(data.retirement_contribution || '');
|
||||
setMonthlyEmergencyContribution(data.monthly_emergency_contribution || '');
|
||||
setExtraCashEmergencyPct(data.extra_cash_emergency_pct || '');
|
||||
setExtraCashRetirementPct(data.extra_cash_retirement_pct || '');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch financial profile", err);
|
||||
console.error("Failed to load financial profile:", err);
|
||||
}
|
||||
}
|
||||
fetchProfile();
|
||||
}, []);
|
||||
|
||||
fetchFinancialProfile();
|
||||
}, [userId]);
|
||||
|
||||
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) => {
|
||||
// Submit form updates => POST to the same endpoint
|
||||
async function handleSubmit(e) {
|
||||
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 {
|
||||
const res = await authFetch("/api/premium/financial-profile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: userId, ...formData }),
|
||||
const body = {
|
||||
current_salary: parseFloat(currentSalary) || 0,
|
||||
additional_income: parseFloat(additionalIncome) || 0,
|
||||
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) {
|
||||
const data = await res.json();
|
||||
setProjectionData(data.projectionData); // Store projection data
|
||||
setLoanPayoffMonth(data.loanPaidOffMonth); // Store loan payoff month
|
||||
|
||||
navigate('/milestone-tracker', {
|
||||
state: {
|
||||
selectedCareer,
|
||||
projectionData: data.projectionData,
|
||||
loanPayoffMonth: data.loanPaidOffMonth
|
||||
}
|
||||
});
|
||||
// show success or redirect
|
||||
console.log("Profile updated");
|
||||
} else {
|
||||
console.error("Failed to update profile:", await res.text());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error submitting financial profile:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (setter) => (e) => setter(e.target.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
<input type="number" value={additionalIncome} onChange={handleInput(setAdditionalIncome)} className="w-full border rounded p-2" placeholder="$" />
|
||||
|
||||
<label className="block font-medium">Existing College Loan Debt</label>
|
||||
<input type="number" value={collegeLoanTotal} onChange={handleInput(setCollegeLoanTotal)} className="w-full border rounded p-2" placeholder="Enter existing student loan debt" />
|
||||
<input
|
||||
type="number"
|
||||
value={additionalIncome}
|
||||
onChange={(e) => setAdditionalIncome(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<input type="number" value={emergencyFund} onChange={handleInput(setEmergencyFund)} className="w-full border rounded p-2" placeholder="$" />
|
||||
<label className="block font-medium">Monthly Emergency Contribution</label>
|
||||
<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">
|
||||
<input id="inCollege" type="checkbox" checked={inCollege} onChange={(e) => setInCollege(e.target.checked)} />
|
||||
<label htmlFor="inCollege" className="font-medium">Are you currently in college?</label>
|
||||
</div>
|
||||
<label className="block font-medium">Extra Cash to Emergency (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={extraCashEmergencyPct}
|
||||
onChange={(e) => setExtraCashEmergencyPct(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="e.g. 30"
|
||||
/>
|
||||
|
||||
{inCollege && (
|
||||
<>
|
||||
{/* Selected School input with suggestions */}
|
||||
<label className="block font-medium">Selected School</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedSchool}
|
||||
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>
|
||||
)}
|
||||
<label className="block font-medium">Extra Cash to Retirement (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={extraCashRetirementPct}
|
||||
onChange={(e) => setExtraCashRetirementPct(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="e.g. 70"
|
||||
/>
|
||||
|
||||
{/* Program input with suggestions */}
|
||||
<label className="block font-medium">Program</label>
|
||||
<input
|
||||
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>
|
||||
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
|
||||
Save and Continue
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -41,27 +41,18 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCareerPaths = async () => {
|
||||
const res = await authFetch(`${apiURL}/premium/planned-path/all`);
|
||||
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
||||
if (!res) return;
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Flatten nested array
|
||||
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 { careerPaths } = data;
|
||||
setExistingCareerPaths(careerPaths);
|
||||
|
||||
const fromPopout = location.state?.selectedCareer;
|
||||
if (fromPopout) {
|
||||
setSelectedCareer(fromPopout);
|
||||
setCareerPathId(fromPopout.career_path_id);
|
||||
} else if (!selectedCareer) {
|
||||
const latest = await authFetch(`${apiURL}/premium/planned-path/latest`);
|
||||
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
||||
if (latest) {
|
||||
const latestData = await latest.json();
|
||||
if (latestData?.id) {
|
||||
@ -152,12 +143,16 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
const handleConfirmCareerSelection = async () => {
|
||||
const newId = uuidv4();
|
||||
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;
|
||||
setSelectedCareer({ career_name: pendingCareerForModal });
|
||||
setCareerPathId(newId);
|
||||
setPendingCareerForModal(null);
|
||||
};
|
||||
const result = await res.json();
|
||||
setCareerPathId(result.career_path_id);
|
||||
setSelectedCareer({
|
||||
career_name: pendingCareerForModal,
|
||||
id: result.career_path_id
|
||||
});
|
||||
setPendingCareerForModal(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="milestone-tracker">
|
||||
|
@ -1,34 +1,175 @@
|
||||
// CareerOnboarding.js
|
||||
import React, { useState } from 'react';
|
||||
// CareerOnboarding.js (inline implementation of career search)
|
||||
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 [careerData, setCareerData] = useState({
|
||||
currentJob: '',
|
||||
industry: '',
|
||||
employmentStatus: '',
|
||||
careerGoal: '',
|
||||
});
|
||||
const apiURL = process.env.REACT_APP_API_URL;
|
||||
|
||||
const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [currentlyWorking, setCurrentlyWorking] = useState('');
|
||||
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) => {
|
||||
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 (
|
||||
<div>
|
||||
<h2>Career Details</h2>
|
||||
<input name="currentJob" placeholder="Current Job Title" onChange={handleChange} />
|
||||
<input name="industry" placeholder="Industry" onChange={handleChange} />
|
||||
<select name="employmentStatus" onChange={handleChange}>
|
||||
<option value="">Employment Status</option>
|
||||
<option value="FT">Full-Time</option>
|
||||
<option value="PT">Part-Time</option>
|
||||
<option value="Unemployed">Unemployed</option>
|
||||
<option value="Student">Student</option>
|
||||
|
||||
<label className="block font-medium">
|
||||
Are you currently working or earning any income (even part-time)?
|
||||
</label>
|
||||
<select
|
||||
value={currentlyWorking}
|
||||
onChange={(e) => setCurrentlyWorking(e.target.value)}
|
||||
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>
|
||||
<input name="careerGoal" placeholder="Career Goal (optional)" onChange={handleChange} />
|
||||
|
||||
<button onClick={prevStep}>Back</button>
|
||||
<button onClick={nextStep}>Next: Financial →</button>
|
||||
<button onClick={handleSubmit}>Financial →</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,35 +1,558 @@
|
||||
// CollegeOnboarding.js
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const CollegeOnboarding = ({ nextStep, prevStep }) => {
|
||||
const [collegeData, setCollegeData] = useState({
|
||||
studentStatus: '',
|
||||
school: '',
|
||||
program: '',
|
||||
creditHoursPerYear: '',
|
||||
});
|
||||
function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
// CIP / iPEDS local states (purely for CIP data and suggestions)
|
||||
const [schoolData, setSchoolData] = useState([]);
|
||||
const [icTuitionData, setIcTuitionData] = useState([]);
|
||||
const [schoolSuggestions, setSchoolSuggestions] = useState([]);
|
||||
const [programSuggestions, setProgramSuggestions] = useState([]);
|
||||
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setCollegeData({ ...collegeData, [e.target.name]: e.target.value });
|
||||
// ---- DESCTRUCTURE PARENT DATA FOR ALL FIELDS EXCEPT TUITION/PROGRAM_LENGTH ----
|
||||
// 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 (
|
||||
<div>
|
||||
<h2>College Plans (optional)</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} />
|
||||
<h2>College Details</h2>
|
||||
|
||||
<button onClick={prevStep}>← Previous: Financial</button>
|
||||
<button onClick={nextStep}>Finish Onboarding</button>
|
||||
{(college_enrollment_status === 'currently_enrolled' ||
|
||||
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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default CollegeOnboarding;
|
||||
|
@ -1,25 +1,138 @@
|
||||
// FinancialOnboarding.js
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
const FinancialOnboarding = ({ nextStep, prevStep }) => {
|
||||
const [financialData, setFinancialData] = useState({
|
||||
salary: '',
|
||||
expenses: '',
|
||||
savings: '',
|
||||
debts: '',
|
||||
});
|
||||
const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
const {
|
||||
currently_working = '',
|
||||
current_salary = 0,
|
||||
additional_income = 0,
|
||||
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) => {
|
||||
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 (
|
||||
<div>
|
||||
<h2>Financial Details</h2>
|
||||
<input name="salary" placeholder="Annual Salary" type="number" onChange={handleChange} />
|
||||
<input name="expenses" placeholder="Monthly Expenses" type="number" onChange={handleChange} />
|
||||
<input name="savings" placeholder="Total Savings" type="number" onChange={handleChange} />
|
||||
<input name="debts" placeholder="Outstanding Debts" type="number" onChange={handleChange} />
|
||||
|
||||
{currently_working === 'yes' && (
|
||||
<>
|
||||
<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={nextStep}>Next: College →</button>
|
||||
|
@ -1,28 +1,91 @@
|
||||
// OnboardingContainer.js
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PremiumWelcome from './PremiumWelcome.js';
|
||||
import CareerOnboarding from './CareerOnboarding.js';
|
||||
import FinancialOnboarding from './FinancialOnboarding.js';
|
||||
import CollegeOnboarding from './CollegeOnboarding.js';
|
||||
import authFetch from '../../utils/authFetch.js';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const OnboardingContainer = () => {
|
||||
console.log('OnboardingContainer MOUNT');
|
||||
|
||||
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 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 = [
|
||||
<PremiumWelcome nextStep={nextStep} />,
|
||||
<CareerOnboarding nextStep={nextStep} prevStep={prevStep} />,
|
||||
<FinancialOnboarding nextStep={nextStep} prevStep={prevStep} />,
|
||||
<CollegeOnboarding nextStep={nextStep} prevStep={prevStep} />,
|
||||
|
||||
<CareerOnboarding
|
||||
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 (
|
||||
<div className="onboarding-container">
|
||||
{onboardingSteps[step]}
|
||||
</div>
|
||||
);
|
||||
return <div>{onboardingSteps[step]}</div>;
|
||||
};
|
||||
|
||||
export default OnboardingContainer;
|
||||
|
@ -1,137 +1,318 @@
|
||||
import moment from 'moment';
|
||||
|
||||
// Function to simulate monthly financial projection
|
||||
// src/utils/FinancialProjectionService.js
|
||||
// Example fields in userProfile that matter here:
|
||||
// - 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) {
|
||||
const {
|
||||
currentSalary,
|
||||
monthlyExpenses,
|
||||
monthlyDebtPayments,
|
||||
studentLoanAmount,
|
||||
interestRate, // ✅ Corrected
|
||||
loanTerm, // ✅ Corrected
|
||||
extraPayment,
|
||||
expectedSalary,
|
||||
emergencySavings,
|
||||
retirementSavings,
|
||||
monthlyRetirementContribution,
|
||||
monthlyEmergencyContribution,
|
||||
gradDate,
|
||||
fullTimeCollegeStudent: inCollege,
|
||||
partTimeIncome,
|
||||
startDate,
|
||||
programType,
|
||||
isFullyOnline,
|
||||
creditHoursPerYear,
|
||||
calculatedTuition,
|
||||
hoursCompleted,
|
||||
loanDeferralUntilGraduation,
|
||||
programLength
|
||||
} = userProfile;
|
||||
const {
|
||||
// Income & expenses
|
||||
currentSalary = 0,
|
||||
monthlyExpenses = 0,
|
||||
monthlyDebtPayments = 0,
|
||||
partTimeIncome = 0,
|
||||
extraPayment = 0,
|
||||
|
||||
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
||||
// Loan info
|
||||
studentLoanAmount = 0,
|
||||
interestRate = 5, // %
|
||||
loanTerm = 10, // years
|
||||
loanDeferralUntilGraduation = false,
|
||||
|
||||
let totalEmergencySavings = emergencySavings;
|
||||
let totalRetirementSavings = retirementSavings;
|
||||
let loanBalance = studentLoanAmount;
|
||||
let projectionData = [];
|
||||
// College & tuition
|
||||
inCollege = false,
|
||||
programType,
|
||||
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;
|
||||
let milestoneIndex = 0;
|
||||
let loanPaidOffMonth = null;
|
||||
// Salary after graduation
|
||||
expectedSalary = 0,
|
||||
|
||||
// Dynamic credit hours based on the program type
|
||||
let requiredCreditHours;
|
||||
switch (programType) {
|
||||
case "Associate Degree":
|
||||
requiredCreditHours = 60;
|
||||
break;
|
||||
case "Bachelor's Degree":
|
||||
requiredCreditHours = 120;
|
||||
break;
|
||||
case "Master's Degree":
|
||||
requiredCreditHours = 30;
|
||||
break;
|
||||
case "Doctoral Degree":
|
||||
requiredCreditHours = 60;
|
||||
break;
|
||||
default:
|
||||
requiredCreditHours = 120;
|
||||
// Savings
|
||||
emergencySavings = 0,
|
||||
retirementSavings = 0,
|
||||
|
||||
// Monthly contributions
|
||||
monthlyRetirementContribution = 0,
|
||||
monthlyEmergencyContribution = 0,
|
||||
|
||||
// Surplus allocation
|
||||
surplusEmergencyAllocation = 50,
|
||||
surplusRetirementAllocation = 50,
|
||||
|
||||
// Potential override
|
||||
programLength
|
||||
} = userProfile;
|
||||
|
||||
// 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;
|
||||
const programDuration = Math.ceil(remainingCreditHours / creditHoursPerYear);
|
||||
const tuitionCost = calculatedTuition;
|
||||
const totalTuitionCost = tuitionCost * programDuration;
|
||||
// Are we still in college? We either trust gradDate or approximate finalProgramLength
|
||||
let stillInCollege = false;
|
||||
if (inCollege) {
|
||||
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);
|
||||
for (let month = 0; month < 240; month++) {
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
// 6. If we pay lumps: check if this is a "lump" month within the user's academic year
|
||||
// We'll find how many academic years have passed since they started
|
||||
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) {
|
||||
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
}
|
||||
// Which academic year index are we in?
|
||||
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
||||
// Within that year, which month are we in? (0..11)
|
||||
const monthInYear = elapsedMonths % 12;
|
||||
|
||||
let tuitionCostThisMonth = 0;
|
||||
if (inCollege && !loanDeferralUntilGraduation) {
|
||||
tuitionCostThisMonth = totalTuitionCost / programDuration / 12;
|
||||
}
|
||||
// If we find monthInYear in lumpsSchedule, then lumps are due
|
||||
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
||||
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) {
|
||||
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here
|
||||
loanBalance += interestForMonth;
|
||||
} else if (loanBalance > 0) {
|
||||
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here
|
||||
const principalForMonth = Math.min(loanBalance, monthlyLoanPayment + extraPayment - interestForMonth);
|
||||
loanBalance -= principalForMonth;
|
||||
loanBalance = Math.max(loanBalance, 0);
|
||||
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
|
||||
}
|
||||
// 8. monthly income
|
||||
let monthlyIncome = 0;
|
||||
if (!inCollege || !stillInCollege) {
|
||||
// user has graduated or never in college
|
||||
monthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
|
||||
} else {
|
||||
// in college => currentSalary + partTimeIncome
|
||||
monthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
|
||||
}
|
||||
|
||||
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
|
||||
+ tuitionCostThisMonth
|
||||
+ monthlyDebtPayments
|
||||
+ thisMonthLoanPayment;
|
||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||
// Accrue interest only
|
||||
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||
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;
|
||||
extraCash = Math.max(extraCash, 0);
|
||||
// Baseline monthly contributions
|
||||
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
|
||||
let effectiveRetirementContribution = 0;
|
||||
let effectiveEmergencyContribution = 0;
|
||||
|
||||
// update savings explicitly with contributions first
|
||||
totalEmergencySavings += monthlyEmergencyContribution + (extraCash * 0.3);
|
||||
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7);
|
||||
totalRetirementSavings *= (1 + 0.07 / 12);
|
||||
if (leftover >= baselineContributions) {
|
||||
effectiveRetirementContribution = monthlyRetirementContribution;
|
||||
effectiveEmergencyContribution = monthlyEmergencyContribution;
|
||||
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
|
||||
projectionData.push({
|
||||
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
salary: salaryNow,
|
||||
monthlyIncome: monthlyIncome,
|
||||
expenses: totalMonthlyExpenses,
|
||||
loanPayment: thisMonthLoanPayment,
|
||||
retirementContribution: monthlyRetirementContribution,
|
||||
emergencyContribution: monthlyEmergencyContribution,
|
||||
netSavings: monthlyIncome - totalMonthlyExpenses, // Exclude contributions here explicitly!
|
||||
totalEmergencySavings,
|
||||
totalRetirementSavings,
|
||||
loanBalance
|
||||
});
|
||||
// 11. Now see if leftover is negative => shortfall from mandatory expenses
|
||||
// Actually we zeroed leftover if it was negative. So let's check if the user
|
||||
// truly can't afford mandatoryExpenses
|
||||
const totalMandatoryPlusContrib = monthlyIncome - leftover;
|
||||
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||
let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
|
||||
if (shortfall > 0) {
|
||||
// We can reduce from emergency savings
|
||||
const canCover = Math.min(shortfall, currentEmergencySavings);
|
||||
currentEmergencySavings -= canCover;
|
||||
shortfall -= canCover;
|
||||
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) {
|
||||
const monthlyRate = annualRate / 100 / 12;
|
||||
const numPayments = years * 12;
|
||||
return (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -numPayments));
|
||||
projectionData.push({
|
||||
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
monthlyIncome,
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user