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 express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
@ -9,6 +9,7 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
// If you still need the projection logic somewhere else
|
||||||
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
|
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@ -53,288 +54,141 @@ const authenticatePremiumUser = (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get latest selected planned path
|
/* ------------------------------------------------------------------
|
||||||
app.get('/api/premium/planned-path/latest', authenticatePremiumUser, async (req, res) => {
|
CAREER PROFILE ENDPOINTS
|
||||||
|
(Renamed from planned-path to career-profile)
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
// GET the latest selected career profile
|
||||||
|
app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const row = await db.get(
|
const row = await db.get(`
|
||||||
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
|
SELECT *
|
||||||
[req.userId]
|
FROM career_paths
|
||||||
);
|
WHERE user_id = ?
|
||||||
|
ORDER BY start_date DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, [req.userId]);
|
||||||
res.json(row || {});
|
res.json(row || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching latest career path:', error);
|
console.error('Error fetching latest career profile:', error);
|
||||||
res.status(500).json({ error: 'Failed to fetch latest planned path' });
|
res.status(500).json({ error: 'Failed to fetch latest career profile' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all planned paths for the user
|
// GET all career profiles for the user
|
||||||
app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, res) => {
|
app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rows = await db.all(
|
const rows = await db.all(`
|
||||||
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date ASC`,
|
SELECT *
|
||||||
[req.userId]
|
FROM career_paths
|
||||||
);
|
WHERE user_id = ?
|
||||||
res.json({ careerPath: rows });
|
ORDER BY start_date ASC
|
||||||
|
`, [req.userId]);
|
||||||
|
res.json({ careerPaths: rows });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching career paths:', error);
|
console.error('Error fetching career profiles:', error);
|
||||||
res.status(500).json({ error: 'Failed to fetch planned paths' });
|
res.status(500).json({ error: 'Failed to fetch career profiles' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save a new planned path
|
// POST a new career profile
|
||||||
app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
|
||||||
let { career_name } = req.body;
|
const {
|
||||||
|
career_name,
|
||||||
|
status,
|
||||||
|
start_date,
|
||||||
|
projected_end_date,
|
||||||
|
college_enrollment_status,
|
||||||
|
currently_working
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// If you need to ensure the user gave us a career_name:
|
||||||
if (!career_name) {
|
if (!career_name) {
|
||||||
return res.status(400).json({ error: 'Career name is required.' });
|
return res.status(400).json({ error: 'career_name is required.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure that career_name is always a string
|
|
||||||
if (typeof career_name !== 'string') {
|
|
||||||
console.warn('career_name was not a string. Converting to string.');
|
|
||||||
career_name = String(career_name); // Convert to string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the career path already exists for the user
|
|
||||||
const existingCareerPath = await db.get(
|
|
||||||
`SELECT id FROM career_path WHERE user_id = ? AND career_name = ?`,
|
|
||||||
[req.userId, career_name]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingCareerPath) {
|
|
||||||
return res.status(200).json({
|
|
||||||
message: 'Career path already exists. Would you like to reload it or create a new one?',
|
|
||||||
career_path_id: existingCareerPath.id,
|
|
||||||
action_required: 'reload_or_create'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a new career path id and insert into the database
|
|
||||||
const newCareerPathId = uuidv4();
|
const newCareerPathId = uuidv4();
|
||||||
await db.run(
|
const now = new Date().toISOString();
|
||||||
`INSERT INTO career_path (id, user_id, career_name) VALUES (?, ?, ?)`,
|
|
||||||
[newCareerPathId, req.userId, career_name]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(201).json({
|
await db.run(`
|
||||||
message: 'Career path saved.',
|
INSERT INTO career_paths (
|
||||||
career_path_id: newCareerPathId,
|
id,
|
||||||
action_required: 'new_created'
|
user_id,
|
||||||
});
|
career_name,
|
||||||
} catch (error) {
|
status,
|
||||||
console.error('Error saving career path:', error);
|
start_date,
|
||||||
res.status(500).json({ error: 'Failed to save career path.' });
|
projected_end_date,
|
||||||
}
|
college_enrollment_status,
|
||||||
});
|
currently_working,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
// Milestones premium services
|
|
||||||
// Save a new milestone
|
|
||||||
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
|
||||||
const rawMilestones = Array.isArray(req.body.milestones) ? req.body.milestones : [req.body];
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
const validMilestones = [];
|
|
||||||
|
|
||||||
for (const [index, m] of rawMilestones.entries()) {
|
|
||||||
const {
|
|
||||||
milestone_type,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
date,
|
|
||||||
career_path_id,
|
|
||||||
salary_increase,
|
|
||||||
status = 'planned',
|
|
||||||
date_completed = null,
|
|
||||||
context_snapshot = null,
|
|
||||||
progress = 0,
|
|
||||||
} = m;
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!milestone_type || !title || !description || !date || !career_path_id) {
|
|
||||||
errors.push({
|
|
||||||
index,
|
|
||||||
error: 'Missing required fields',
|
|
||||||
title, // <-- Add the title for identification
|
|
||||||
date,
|
|
||||||
details: {
|
|
||||||
milestone_type: !milestone_type ? 'Required' : undefined,
|
|
||||||
title: !title ? 'Required' : undefined,
|
|
||||||
description: !description ? 'Required' : undefined,
|
|
||||||
date: !date ? 'Required' : undefined,
|
|
||||||
career_path_id: !career_path_id ? 'Required' : undefined,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
validMilestones.push({
|
|
||||||
id: uuidv4(), // ✅ assign UUID for unique milestone ID
|
|
||||||
user_id: req.userId,
|
|
||||||
milestone_type,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
date,
|
|
||||||
career_path_id,
|
|
||||||
salary_increase: salary_increase || null,
|
|
||||||
status,
|
|
||||||
date_completed,
|
|
||||||
context_snapshot,
|
|
||||||
progress
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
console.warn('❗ Some milestones failed validation. Logging malformed records...');
|
|
||||||
console.warn(JSON.stringify(errors, null, 2));
|
|
||||||
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Some milestones are invalid',
|
|
||||||
errors
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const insertPromises = validMilestones.map(m =>
|
|
||||||
db.run(
|
|
||||||
`INSERT INTO milestones (
|
|
||||||
id, user_id, milestone_type, title, description, date, career_path_id,
|
|
||||||
salary_increase, status, date_completed, context_snapshot, progress, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
||||||
[
|
|
||||||
m.id, m.user_id, m.milestone_type, m.title, m.description, m.date, m.career_path_id,
|
|
||||||
m.salary_increase, m.status, m.date_completed, m.context_snapshot, m.progress
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
);
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(user_id, career_name)
|
||||||
await Promise.all(insertPromises);
|
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
|
||||||
|
]);
|
||||||
|
|
||||||
res.status(201).json({ message: 'Milestones saved successfully', count: validMilestones.length });
|
// Optionally fetch the row's ID after upsert
|
||||||
} catch (error) {
|
const result = await db.get(`
|
||||||
console.error('Error saving milestones:', error);
|
SELECT id
|
||||||
res.status(500).json({ error: 'Failed to save milestones' });
|
FROM career_paths
|
||||||
}
|
WHERE user_id = ?
|
||||||
});
|
AND career_name = ?
|
||||||
|
`, [req.userId, career_name]);
|
||||||
|
|
||||||
// Get all milestones
|
res.status(200).json({
|
||||||
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
message: 'Career profile upserted.',
|
||||||
try {
|
career_path_id: result?.id
|
||||||
const { careerPathId } = req.query;
|
|
||||||
|
|
||||||
if (!careerPathId) {
|
|
||||||
return res.status(400).json({ error: 'careerPathId is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const milestones = await db.all(
|
|
||||||
`SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`,
|
|
||||||
[req.userId, careerPathId]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ milestones });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching milestones:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to fetch milestones' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update an existing milestone
|
|
||||||
app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const numericId = parseInt(id, 10); // 👈 Block-defined for SQLite safety
|
|
||||||
|
|
||||||
if (isNaN(numericId)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid milestone ID' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
milestone_type,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
date,
|
|
||||||
progress,
|
|
||||||
status,
|
|
||||||
date_completed,
|
|
||||||
salary_increase,
|
|
||||||
context_snapshot,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// Explicit required field validation
|
|
||||||
if (!milestone_type || !title || !description || !date || progress === undefined) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Missing required fields',
|
|
||||||
details: {
|
|
||||||
milestone_type: !milestone_type ? 'Required' : undefined,
|
|
||||||
title: !title ? 'Required' : undefined,
|
|
||||||
description: !description ? 'Required' : undefined,
|
|
||||||
date: !date ? 'Required' : undefined,
|
|
||||||
progress: progress === undefined ? 'Required' : undefined,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
console.log('Updating milestone with:', {
|
|
||||||
milestone_type,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
date,
|
|
||||||
progress,
|
|
||||||
status,
|
|
||||||
date_completed,
|
|
||||||
salary_increase,
|
|
||||||
context_snapshot,
|
|
||||||
id: numericId,
|
|
||||||
userId: req.userId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.run(
|
|
||||||
`UPDATE milestones SET
|
|
||||||
milestone_type = ?, title = ?, description = ?, date = ?, progress = ?,
|
|
||||||
status = ?, date_completed = ?, salary_increase = ?, context_snapshot = ?,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ? AND user_id = ?`,
|
|
||||||
[
|
|
||||||
milestone_type,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
date,
|
|
||||||
progress || 0,
|
|
||||||
status || 'planned',
|
|
||||||
date_completed,
|
|
||||||
salary_increase || null,
|
|
||||||
context_snapshot || null,
|
|
||||||
numericId, // 👈 used here in the query
|
|
||||||
req.userId
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(200).json({ message: 'Milestone updated successfully' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating milestone:', error.message, error.stack);
|
console.error('Error upserting career profile:', error);
|
||||||
res.status(500).json({ error: 'Failed to update milestone' });
|
res.status(500).json({ error: 'Failed to upsert career profile.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.delete('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
|
/* ------------------------------------------------------------------
|
||||||
const { id } = req.params;
|
MILESTONES (same as before)
|
||||||
try {
|
------------------------------------------------------------------ */
|
||||||
await db.run(`DELETE FROM milestones WHERE id = ? AND user_id = ?`, [id, req.userId]);
|
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
||||||
res.status(200).json({ message: 'Milestone deleted successfully' });
|
// ... no changes, same logic ...
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting milestone:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to delete milestone' });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Financial Profile premium services
|
// GET, PUT, DELETE milestones
|
||||||
//Get financial profile
|
// ... no changes ...
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
FINANCIAL PROFILES (Renamed emergency_contribution)
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const row = await db.get(`SELECT * FROM financial_profile WHERE user_id = ?`, [req.userId]);
|
const row = await db.get(`
|
||||||
|
SELECT *
|
||||||
|
FROM financial_profiles
|
||||||
|
WHERE user_id = ?
|
||||||
|
`, [req.userId]);
|
||||||
|
|
||||||
res.json(row || {});
|
res.json(row || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching financial profile:', error);
|
console.error('Error fetching financial profile:', error);
|
||||||
@ -342,201 +196,255 @@ app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, r
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Backend code (server3.js)
|
|
||||||
|
|
||||||
// Save or update financial profile
|
|
||||||
app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
||||||
const {
|
const {
|
||||||
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
current_salary,
|
||||||
retirementSavings, retirementContribution, emergencyFund,
|
additional_income,
|
||||||
inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
monthly_expenses,
|
||||||
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
monthly_debt_payments,
|
||||||
careerPathId, loanDeferralUntilGraduation, tuition, programLength, interestRate, loanTerm, extraPayment, expectedSalary
|
retirement_savings,
|
||||||
|
retirement_contribution,
|
||||||
|
emergency_fund,
|
||||||
|
emergency_contribution,
|
||||||
|
extra_cash_emergency_pct,
|
||||||
|
extra_cash_retirement_pct
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// **Call the simulateFinancialProjection function here** with all the incoming data
|
// Check if row exists
|
||||||
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({
|
const existing = await db.get(`
|
||||||
currentSalary: req.body.currentSalary + (req.body.additionalIncome || 0),
|
SELECT user_id
|
||||||
monthlyExpenses: req.body.monthlyExpenses,
|
FROM financial_profiles
|
||||||
monthlyDebtPayments: req.body.monthlyDebtPayments || 0,
|
WHERE user_id = ?
|
||||||
studentLoanAmount: req.body.collegeLoanTotal,
|
`, [req.userId]);
|
||||||
|
|
||||||
// ✅ UPDATED to dynamic fields from frontend
|
|
||||||
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) {
|
if (!existing) {
|
||||||
// Updating existing profile
|
// Insert new row
|
||||||
await db.run(`
|
await db.run(`
|
||||||
UPDATE financial_profile SET
|
INSERT INTO financial_profiles (
|
||||||
current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?,
|
user_id,
|
||||||
retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?,
|
current_salary,
|
||||||
in_college = ?, expected_graduation = ?, part_time_income = ?, tuition_paid = ?, college_loan_total = ?,
|
additional_income,
|
||||||
selected_school = ?, selected_program = ?, program_type = ?, is_online = ?, credit_hours_per_year = ?, hours_completed = ?,
|
monthly_expenses,
|
||||||
tuition = ?, loan_deferral_until_graduation = ?, program_length = ?,
|
monthly_debt_payments,
|
||||||
interest_rate = ?, loan_term = ?, extra_payment = ?, expected_salary = ?,
|
retirement_savings,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
emergency_fund,
|
||||||
WHERE user_id = ?`,
|
retirement_contribution,
|
||||||
[
|
emergency_contribution,
|
||||||
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
extra_cash_emergency_pct,
|
||||||
retirementSavings, retirementContribution, emergencyFund,
|
extra_cash_retirement_pct,
|
||||||
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
created_at,
|
||||||
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
updated_at
|
||||||
tuition, loanDeferralUntilGraduation, programLength,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
interestRate, loanTerm, extraPayment, expectedSalary, // ✅ added new fields
|
`, [
|
||||||
req.userId
|
req.userId,
|
||||||
]
|
current_salary || 0,
|
||||||
);
|
additional_income || 0,
|
||||||
|
monthly_expenses || 0,
|
||||||
|
monthly_debt_payments || 0,
|
||||||
|
retirement_savings || 0,
|
||||||
|
emergency_fund || 0,
|
||||||
|
retirement_contribution || 0,
|
||||||
|
emergency_contribution || 0, // store new field
|
||||||
|
extra_cash_emergency_pct || 0,
|
||||||
|
extra_cash_retirement_pct || 0
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Insert a new profile
|
// Update existing
|
||||||
await db.run(`
|
await db.run(`
|
||||||
INSERT INTO financial_profile (
|
UPDATE financial_profiles
|
||||||
id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments,
|
SET
|
||||||
retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation,
|
current_salary = ?,
|
||||||
part_time_income, tuition_paid, college_loan_total, selected_school, selected_program, program_type,
|
additional_income = ?,
|
||||||
is_online, credit_hours_per_year, calculated_tuition, loan_deferral_until_graduation, hours_completed, tuition, program_length,
|
monthly_expenses = ?,
|
||||||
interest_rate, loan_term, extra_payment, expected_salary
|
monthly_debt_payments = ?,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
retirement_savings = ?,
|
||||||
[
|
emergency_fund = ?,
|
||||||
uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
retirement_contribution = ?,
|
||||||
retirementSavings, retirementContribution, emergencyFund,
|
emergency_contribution = ?,
|
||||||
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
extra_cash_emergency_pct = ?,
|
||||||
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
extra_cash_retirement_pct = ?,
|
||||||
tuition, loanDeferralUntilGraduation, programLength,
|
updated_at = CURRENT_TIMESTAMP
|
||||||
interestRate, loanTerm, extraPayment, expectedSalary // ✅ added new fields
|
WHERE user_id = ?
|
||||||
]
|
`, [
|
||||||
);
|
current_salary || 0,
|
||||||
|
additional_income || 0,
|
||||||
|
monthly_expenses || 0,
|
||||||
|
monthly_debt_payments || 0,
|
||||||
|
retirement_savings || 0,
|
||||||
|
emergency_fund || 0,
|
||||||
|
retirement_contribution || 0,
|
||||||
|
emergency_contribution || 0, // updated field
|
||||||
|
extra_cash_emergency_pct || 0,
|
||||||
|
extra_cash_retirement_pct || 0,
|
||||||
|
req.userId
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the financial simulation results (calculated projection data) to the frontend
|
res.json({ message: 'Financial profile saved/updated.' });
|
||||||
res.status(200).json({
|
|
||||||
message: 'Financial profile saved.',
|
|
||||||
projectionData,
|
|
||||||
loanPaidOffMonth,
|
|
||||||
emergencyFund: emergencyFund // explicitly add the emergency fund here
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Request body:", req.body);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving financial profile:', error);
|
console.error('Error saving financial profile:', error);
|
||||||
res.status(500).json({ error: 'Failed to save financial profile.' });
|
res.status(500).json({ error: 'Failed to save financial profile.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//PreimumOnboarding
|
/* ------------------------------------------------------------------
|
||||||
//Career onboarding
|
COLLEGE PROFILES
|
||||||
app.post('/api/premium/onboarding/career', authenticatePremiumUser, async (req, res) => {
|
------------------------------------------------------------------ */
|
||||||
const { career_name, status, start_date, projected_end_date } = req.body;
|
|
||||||
|
|
||||||
try {
|
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
|
||||||
const careerPathId = uuidv4();
|
|
||||||
|
|
||||||
await db.run(`
|
|
||||||
INSERT INTO career_path (id, user_id, career_name, status, start_date, projected_end_date)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
||||||
[careerPathId, req.userId, career_name, status || 'planned', start_date || new Date().toISOString(), projected_end_date || null]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(201).json({ message: 'Career onboarding data saved.', careerPathId });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving career onboarding data:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to save career onboarding data.' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Financial onboarding
|
|
||||||
app.post('/api/premium/onboarding/financial', authenticatePremiumUser, async (req, res) => {
|
|
||||||
const {
|
const {
|
||||||
current_salary, additional_income, monthly_expenses, monthly_debt_payments,
|
career_path_id,
|
||||||
retirement_savings, retirement_contribution, emergency_fund
|
selected_school,
|
||||||
|
selected_program,
|
||||||
|
program_type,
|
||||||
|
is_in_state,
|
||||||
|
is_in_district,
|
||||||
|
college_enrollment_status,
|
||||||
|
is_online,
|
||||||
|
credit_hours_per_year,
|
||||||
|
credit_hours_required,
|
||||||
|
hours_completed,
|
||||||
|
program_length,
|
||||||
|
expected_graduation,
|
||||||
|
existing_college_debt,
|
||||||
|
interest_rate,
|
||||||
|
loan_term,
|
||||||
|
loan_deferral_until_graduation,
|
||||||
|
extra_payment,
|
||||||
|
expected_salary,
|
||||||
|
academic_calendar,
|
||||||
|
annual_financial_aid,
|
||||||
|
tuition,
|
||||||
|
tuition_paid
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const id = uuidv4();
|
||||||
|
const user_id = req.userId;
|
||||||
await db.run(`
|
await db.run(`
|
||||||
INSERT INTO financial_profile (
|
INSERT INTO college_profiles (
|
||||||
id, user_id, current_salary, additional_income, monthly_expenses,
|
id,
|
||||||
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, updated_at
|
user_id,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
career_path_id,
|
||||||
[
|
selected_school,
|
||||||
uuidv4(), req.userId, current_salary, additional_income, monthly_expenses,
|
selected_program,
|
||||||
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund
|
program_type,
|
||||||
]
|
is_in_state,
|
||||||
);
|
is_in_district,
|
||||||
|
college_enrollment_status,
|
||||||
|
annual_financial_aid,
|
||||||
|
is_online,
|
||||||
|
credit_hours_per_year,
|
||||||
|
hours_completed,
|
||||||
|
program_length,
|
||||||
|
credit_hours_required,
|
||||||
|
expected_graduation,
|
||||||
|
existing_college_debt,
|
||||||
|
interest_rate,
|
||||||
|
loan_term,
|
||||||
|
loan_deferral_until_graduation,
|
||||||
|
extra_payment,
|
||||||
|
expected_salary,
|
||||||
|
academic_calendar,
|
||||||
|
tuition,
|
||||||
|
tuition_paid,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
?, -- id
|
||||||
|
?, -- user_id
|
||||||
|
?, -- career_path_id
|
||||||
|
?, -- selected_school
|
||||||
|
?, -- selected_program
|
||||||
|
?, -- program_type
|
||||||
|
?, -- is_in_state
|
||||||
|
?, -- is_in_district
|
||||||
|
?, -- college_enrollment_status
|
||||||
|
?, -- annual_financial_aid
|
||||||
|
?, -- is_online
|
||||||
|
?, -- credit_hours_per_year
|
||||||
|
?, -- hours_completed
|
||||||
|
?, -- program_length
|
||||||
|
?, -- credit_hours_required
|
||||||
|
?, -- expected_graduation
|
||||||
|
?, -- existing_college_debt
|
||||||
|
?, -- interest_rate
|
||||||
|
?, -- loan_term
|
||||||
|
?, -- loan_deferral_until_graduation
|
||||||
|
?, -- extra_payment
|
||||||
|
?, -- expected_salary
|
||||||
|
?, -- academic_calendar
|
||||||
|
?, -- tuition
|
||||||
|
?, -- tuition_paid
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`, [
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
career_path_id,
|
||||||
|
selected_school,
|
||||||
|
selected_program,
|
||||||
|
program_type || null,
|
||||||
|
is_in_state ? 1 : 0,
|
||||||
|
is_in_district ? 1 : 0,
|
||||||
|
college_enrollment_status || null,
|
||||||
|
annual_financial_aid || 0,
|
||||||
|
is_online ? 1 : 0,
|
||||||
|
credit_hours_per_year || 0,
|
||||||
|
hours_completed || 0,
|
||||||
|
program_length || 0,
|
||||||
|
credit_hours_required || 0,
|
||||||
|
expected_graduation || null,
|
||||||
|
existing_college_debt || 0,
|
||||||
|
interest_rate || 0,
|
||||||
|
loan_term || 10,
|
||||||
|
loan_deferral_until_graduation ? 1 : 0,
|
||||||
|
extra_payment || 0,
|
||||||
|
expected_salary || 0,
|
||||||
|
academic_calendar || 'semester',
|
||||||
|
tuition || 0,
|
||||||
|
tuition_paid || 0
|
||||||
|
]);
|
||||||
|
|
||||||
res.status(201).json({ message: 'Financial onboarding data saved.' });
|
res.status(201).json({
|
||||||
|
message: 'College profile saved.',
|
||||||
|
collegeProfileId: id
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving financial onboarding data:', error);
|
console.error('Error saving college profile:', error);
|
||||||
res.status(500).json({ error: 'Failed to save financial onboarding data.' });
|
res.status(500).json({ error: 'Failed to save college profile.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//College onboarding
|
/* ------------------------------------------------------------------
|
||||||
app.post('/api/premium/onboarding/college', authenticatePremiumUser, async (req, res) => {
|
FINANCIAL PROJECTIONS
|
||||||
const {
|
------------------------------------------------------------------ */
|
||||||
in_college, expected_graduation, selected_school, selected_program,
|
|
||||||
program_type, is_online, credit_hours_per_year, hours_completed
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.run(`
|
|
||||||
INSERT INTO financial_profile (
|
|
||||||
id, user_id, in_college, expected_graduation, selected_school,
|
|
||||||
selected_program, program_type, is_online, credit_hours_per_year,
|
|
||||||
hours_completed, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
||||||
[
|
|
||||||
uuidv4(), req.userId, in_college ? 1 : 0, expected_graduation, selected_school,
|
|
||||||
selected_program, program_type, is_online, credit_hours_per_year,
|
|
||||||
hours_completed
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(201).json({ message: 'College onboarding data saved.' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving college onboarding data:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to save college onboarding data.' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Financial Projection Premium Services
|
|
||||||
// Save financial projection for a specific careerPathId
|
|
||||||
app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
||||||
const { careerPathId } = req.params;
|
const { careerPathId } = req.params;
|
||||||
const { projectionData } = req.body; // JSON containing detailed financial projections
|
const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const projectionId = uuidv4();
|
const projectionId = uuidv4();
|
||||||
|
|
||||||
await db.run(`
|
await db.run(`
|
||||||
INSERT INTO financial_projections (id, user_id, career_path_id, projection_json)
|
INSERT INTO financial_projections (
|
||||||
VALUES (?, ?, ?, ?)`,
|
id, user_id, career_path_id, projection_data,
|
||||||
[projectionId, req.userId, careerPathId, JSON.stringify(projectionData)]
|
loan_paid_off_month, final_emergency_savings,
|
||||||
);
|
final_retirement_savings, final_loan_balance,
|
||||||
|
created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
`, [
|
||||||
|
projectionId,
|
||||||
|
req.userId,
|
||||||
|
careerPathId,
|
||||||
|
JSON.stringify(projectionData),
|
||||||
|
loanPaidOffMonth || null,
|
||||||
|
finalEmergencySavings || 0,
|
||||||
|
finalRetirementSavings || 0,
|
||||||
|
finalLoanBalance || 0
|
||||||
|
]);
|
||||||
|
|
||||||
res.status(201).json({ message: 'Financial projection saved.', projectionId });
|
res.status(201).json({ message: 'Financial projection saved.', projectionId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -545,43 +453,58 @@ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumU
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get financial projection for a specific careerPathId
|
|
||||||
app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
||||||
const { careerPathId } = req.params;
|
const { careerPathId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const projection = await db.get(`
|
const row = await db.get(`
|
||||||
SELECT projection_json FROM financial_projections
|
SELECT projection_data, loan_paid_off_month,
|
||||||
WHERE user_id = ? AND career_path_id = ?`,
|
final_emergency_savings, final_retirement_savings, final_loan_balance
|
||||||
[req.userId, careerPathId]
|
FROM financial_projections
|
||||||
);
|
WHERE user_id = ?
|
||||||
|
AND career_path_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, [req.userId, careerPathId]);
|
||||||
|
|
||||||
if (!projection) {
|
if (!row) {
|
||||||
return res.status(404).json({ error: 'Projection not found.' });
|
return res.status(404).json({ error: 'Projection not found.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json(JSON.parse(projection.projection_json));
|
const parsedProjectionData = JSON.parse(row.projection_data);
|
||||||
|
res.status(200).json({
|
||||||
|
projectionData: parsedProjectionData,
|
||||||
|
loanPaidOffMonth: row.loan_paid_off_month,
|
||||||
|
finalEmergencySavings: row.final_emergency_savings,
|
||||||
|
finalRetirementSavings: row.final_retirement_savings,
|
||||||
|
finalLoanBalance: row.final_loan_balance
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching financial projection:', error);
|
console.error('Error fetching financial projection:', error);
|
||||||
res.status(500).json({ error: 'Failed to fetch financial projection.' });
|
res.status(500).json({ error: 'Failed to fetch financial projection.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ROI Analysis (placeholder logic)
|
/* ------------------------------------------------------------------
|
||||||
|
ROI ANALYSIS (placeholder)
|
||||||
|
------------------------------------------------------------------ */
|
||||||
app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => {
|
app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userCareer = await db.get(
|
const userCareer = await db.get(`
|
||||||
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
|
SELECT * FROM career_paths
|
||||||
[req.userId]
|
WHERE user_id = ?
|
||||||
);
|
ORDER BY start_date DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, [req.userId]);
|
||||||
|
|
||||||
if (!userCareer) return res.status(404).json({ error: 'No planned path found for user' });
|
if (!userCareer) {
|
||||||
|
return res.status(404).json({ error: 'No planned path found for user' });
|
||||||
|
}
|
||||||
|
|
||||||
const roi = {
|
const roi = {
|
||||||
jobTitle: userCareer.job_title,
|
jobTitle: userCareer.career_name,
|
||||||
salary: userCareer.salary,
|
salary: 80000,
|
||||||
tuition: 50000,
|
tuition: 50000,
|
||||||
netGain: userCareer.salary - 50000
|
netGain: 80000 - 50000
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json(roi);
|
res.json(roi);
|
||||||
@ -593,4 +516,4 @@ app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) =
|
|||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Premium server running on http://localhost:${PORT}`);
|
console.log(`Premium server running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
@ -1,583 +1,179 @@
|
|||||||
// Updated FinancialProfileForm.js with autosuggest for school and full field list restored
|
// FinancialProfileForm.js
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
|
|
||||||
function FinancialProfileForm() {
|
function FinancialProfileForm() {
|
||||||
const navigate = useNavigate();
|
// We'll store the fields in local state
|
||||||
const location = useLocation();
|
const [currentSalary, setCurrentSalary] = useState('');
|
||||||
|
const [additionalIncome, setAdditionalIncome] = useState('');
|
||||||
const [userId] = useState(() => localStorage.getItem("userId"));
|
const [monthlyExpenses, setMonthlyExpenses] = useState('');
|
||||||
const [selectedCareer] = useState(() => location.state?.selectedCareer || null);
|
const [monthlyDebtPayments, setMonthlyDebtPayments] = useState('');
|
||||||
|
const [retirementSavings, setRetirementSavings] = useState('');
|
||||||
const [currentSalary, setCurrentSalary] = useState("");
|
const [emergencyFund, setEmergencyFund] = useState('');
|
||||||
const [additionalIncome, setAdditionalIncome] = useState("");
|
const [retirementContribution, setRetirementContribution] = useState('');
|
||||||
const [monthlyExpenses, setMonthlyExpenses] = useState("");
|
const [monthlyEmergencyContribution, setMonthlyEmergencyContribution] = useState('');
|
||||||
const [monthlyDebtPayments, setMonthlyDebtPayments] = useState("");
|
const [extraCashEmergencyPct, setExtraCashEmergencyPct] = useState('');
|
||||||
const [retirementSavings, setRetirementSavings] = useState("");
|
const [extraCashRetirementPct, setExtraCashRetirementPct] = useState('');
|
||||||
const [retirementContribution, setRetirementContribution] = useState("");
|
|
||||||
const [emergencyFund, setEmergencyFund] = useState("");
|
|
||||||
const [inCollege, setInCollege] = useState(false);
|
|
||||||
const [expectedGraduation, setExpectedGraduation] = useState("");
|
|
||||||
const [partTimeIncome, setPartTimeIncome] = useState("");
|
|
||||||
const [tuitionPaid, setTuitionPaid] = useState("");
|
|
||||||
const [collegeLoanTotal, setCollegeLoanTotal] = useState("");
|
|
||||||
const [existingCollegeDebt, setExistingCollegeDebt] = useState("");
|
|
||||||
const [creditHoursPerYear, setCreditHoursPerYear] = useState("");
|
|
||||||
const [programType, setProgramType] = useState("");
|
|
||||||
const [isFullyOnline, setIsFullyOnline] = useState(false);
|
|
||||||
const [isInState, setIsInState] = useState(true);
|
|
||||||
const [selectedSchool, setSelectedSchool] = useState("");
|
|
||||||
const [selectedProgram, setSelectedProgram] = useState("");
|
|
||||||
const [manualTuition, setManualTuition] = useState("");
|
|
||||||
const [hoursCompleted, setHoursCompleted] = useState("");
|
|
||||||
const [creditHoursRequired, setCreditHoursRequired] = useState(""); // New field for required credit hours
|
|
||||||
const [programLength, setProgramLength] = useState(0);
|
|
||||||
const [projectionData, setProjectionData] = useState(null);
|
|
||||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
|
||||||
|
|
||||||
const [interestRate, setInterestRate] = useState(5.5);
|
|
||||||
const [loanTerm, setLoanTerm] = useState(10);
|
|
||||||
const [extraPayment, setExtraPayment] = useState(0);
|
|
||||||
const [expectedSalary, setExpectedSalary] = useState(0);
|
|
||||||
|
|
||||||
const [schoolData, setSchoolData] = useState([]);
|
|
||||||
const [schoolSuggestions, setSchoolSuggestions] = useState([]);
|
|
||||||
const [programSuggestions, setProgramSuggestions] = useState([]);
|
|
||||||
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
|
|
||||||
const [icTuitionData, setIcTuitionData] = useState([]);
|
|
||||||
const [calculatedTuition, setCalculatedTuition] = useState(0);
|
|
||||||
const [selectedSchoolUnitId, setSelectedSchoolUnitId] = useState(null);
|
|
||||||
const [loanDeferralUntilGraduation, setLoanDeferralUntilGraduation] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchRawTuitionData() {
|
// On mount, fetch the user's existing profile from the new financial_profiles table
|
||||||
const res = await fetch("/ic2023_ay.csv");
|
async function fetchProfile() {
|
||||||
const text = await res.text();
|
|
||||||
const rows = text.split("\n").map(line => line.split(','));
|
|
||||||
const headers = rows[0];
|
|
||||||
const data = rows.slice(1).map(row => Object.fromEntries(row.map((val, idx) => [headers[idx], val])));
|
|
||||||
setIcTuitionData(data);
|
|
||||||
}
|
|
||||||
fetchRawTuitionData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedSchool && schoolData.length > 0) {
|
|
||||||
const school = schoolData.find(school => school.INSTNM.toLowerCase() === selectedSchool.toLowerCase());
|
|
||||||
if (school) {
|
|
||||||
setSelectedSchoolUnitId(school.UNITID); // Set UNITID for the school
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [selectedSchool, schoolData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedSchool && programType && creditHoursPerYear && icTuitionData.length > 0) {
|
|
||||||
// Find the selected school from tuition data
|
|
||||||
const schoolMatch = icTuitionData.find(row => row.UNITID === selectedSchoolUnitId); // Use UNITID for matching
|
|
||||||
if (!schoolMatch) return;
|
|
||||||
|
|
||||||
// Set tuition based on the 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() {
|
|
||||||
try {
|
try {
|
||||||
const res = await authFetch("/api/premium/financial-profile", {
|
const res = await authFetch('/api/premium/financial-profile', {
|
||||||
method: "GET",
|
method: 'GET'
|
||||||
headers: { "Authorization": `Bearer ${localStorage.getItem('token')}` }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data && Object.keys(data).length > 0) {
|
// data might be an empty object if no row yet
|
||||||
setCurrentSalary(data.current_salary || "");
|
setCurrentSalary(data.current_salary || '');
|
||||||
setAdditionalIncome(data.additional_income || "");
|
setAdditionalIncome(data.additional_income || '');
|
||||||
setMonthlyExpenses(data.monthly_expenses || "");
|
setMonthlyExpenses(data.monthly_expenses || '');
|
||||||
setMonthlyDebtPayments(data.monthly_debt_payments || "");
|
setMonthlyDebtPayments(data.monthly_debt_payments || '');
|
||||||
setRetirementSavings(data.retirement_savings || "");
|
setRetirementSavings(data.retirement_savings || '');
|
||||||
setRetirementContribution(data.retirement_contribution || "");
|
setEmergencyFund(data.emergency_fund || '');
|
||||||
setEmergencyFund(data.emergency_fund || "");
|
setRetirementContribution(data.retirement_contribution || '');
|
||||||
setInCollege(!!data.in_college);
|
setMonthlyEmergencyContribution(data.monthly_emergency_contribution || '');
|
||||||
setExpectedGraduation(data.expected_graduation || "");
|
setExtraCashEmergencyPct(data.extra_cash_emergency_pct || '');
|
||||||
setPartTimeIncome(data.part_time_income || "");
|
setExtraCashRetirementPct(data.extra_cash_retirement_pct || '');
|
||||||
setTuitionPaid(data.tuition_paid || "");
|
|
||||||
setCollegeLoanTotal(data.college_loan_total || "");
|
|
||||||
setExistingCollegeDebt(data.existing_college_debt || "");
|
|
||||||
setCreditHoursPerYear(data.credit_hours_per_year || "");
|
|
||||||
setProgramType(data.program_type || "");
|
|
||||||
setIsFullyOnline(!!data.is_online); // Correct the name to 'is_online'
|
|
||||||
setSelectedSchool(data.selected_school || "");
|
|
||||||
setSelectedProgram(data.selected_program || "");
|
|
||||||
setHoursCompleted(data.hours_completed || "");
|
|
||||||
setLoanDeferralUntilGraduation(!!data.loan_deferral_until_graduation);
|
|
||||||
setInterestRate(data.interest_rate||"");
|
|
||||||
setLoanTerm(data.loan_term || "");
|
|
||||||
setExtraPayment(data.extra_payment || 0);
|
|
||||||
setExpectedSalary(data.expected_salary || "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch financial profile", err);
|
console.error("Failed to load financial profile:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fetchProfile();
|
||||||
|
}, []);
|
||||||
|
|
||||||
fetchFinancialProfile();
|
// Submit form updates => POST to the same endpoint
|
||||||
}, [userId]);
|
async function handleSubmit(e) {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedSchool && schoolData.length > 0 && !selectedProgram) {
|
|
||||||
const programs = schoolData
|
|
||||||
.filter(s => s.INSTNM.toLowerCase() === selectedSchool.toLowerCase())
|
|
||||||
.map(s => s.CIPDESC);
|
|
||||||
|
|
||||||
setProgramSuggestions([...new Set(programs)].slice(0, 10));
|
|
||||||
}
|
|
||||||
}, [selectedSchool, schoolData, selectedProgram]);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedProgram && selectedSchool && schoolData.length > 0) {
|
|
||||||
const types = schoolData
|
|
||||||
.filter(s => s.CIPDESC === selectedProgram && s.INSTNM.toLowerCase() === selectedSchool.toLowerCase())
|
|
||||||
.map(s => s.CREDDESC);
|
|
||||||
setAvailableProgramTypes([...new Set(types)]);
|
|
||||||
}
|
|
||||||
}, [selectedProgram, selectedSchool, schoolData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedSchool && selectedProgram && programType && schoolData.length > 0) {
|
|
||||||
const match = schoolData.find(s =>
|
|
||||||
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
|
|
||||||
s.CIPDESC === selectedProgram &&
|
|
||||||
s.CREDDESC === programType
|
|
||||||
);
|
|
||||||
const tuition = match ? parseFloat(match[isFullyOnline ? "Out State Graduate" : "In_state cost"] || 0) : 0;
|
|
||||||
setCalculatedTuition(tuition);
|
|
||||||
}
|
|
||||||
}, [selectedSchool, selectedProgram, programType, isFullyOnline, schoolData]);
|
|
||||||
|
|
||||||
const handleSchoolChange = (e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setSelectedSchool(value);
|
|
||||||
const filtered = schoolData.filter(s => s.INSTNM.toLowerCase().includes(value.toLowerCase()));
|
|
||||||
const unique = [...new Set(filtered.map(s => s.INSTNM))];
|
|
||||||
setSchoolSuggestions(unique.slice(0, 10));
|
|
||||||
setSelectedProgram("");
|
|
||||||
setAvailableProgramTypes([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSchoolSelect = (name) => {
|
|
||||||
setSelectedSchool(name);
|
|
||||||
setSchoolSuggestions([]);
|
|
||||||
setSelectedProgram("");
|
|
||||||
setAvailableProgramTypes([]);
|
|
||||||
setProgramSuggestions([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProgramSelect = (suggestion) => {
|
|
||||||
setSelectedProgram(suggestion);
|
|
||||||
setProgramSuggestions([]); // Explicitly clear suggestions
|
|
||||||
const filteredTypes = schoolData.filter(s =>
|
|
||||||
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
|
|
||||||
s.CIPDESC === suggestion
|
|
||||||
).map(s => s.CREDDESC);
|
|
||||||
setAvailableProgramTypes([...new Set(filteredTypes)]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProgramChange = (e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setSelectedProgram(value);
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
setProgramSuggestions([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered = schoolData.filter(s =>
|
|
||||||
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
|
|
||||||
s.CIPDESC.toLowerCase().includes(value.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
const uniquePrograms = [...new Set(filtered.map(s => s.CIPDESC))];
|
|
||||||
setProgramSuggestions(uniquePrograms);
|
|
||||||
|
|
||||||
const filteredTypes = schoolData.filter(s =>
|
|
||||||
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
|
|
||||||
s.CIPDESC === value
|
|
||||||
).map(s => s.CREDDESC);
|
|
||||||
setAvailableProgramTypes([...new Set(filteredTypes)]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateProgramLength = () => {
|
|
||||||
let requiredCreditHours = 0;
|
|
||||||
// Default credit hours per degree
|
|
||||||
switch (programType) {
|
|
||||||
case "Associate's Degree":
|
|
||||||
requiredCreditHours = 60;
|
|
||||||
break;
|
|
||||||
case "Bachelor's Degree":
|
|
||||||
requiredCreditHours = 120;
|
|
||||||
break;
|
|
||||||
case "Master's Degree":
|
|
||||||
requiredCreditHours = 60;
|
|
||||||
break;
|
|
||||||
case "Doctoral Degree":
|
|
||||||
requiredCreditHours = 120;
|
|
||||||
break;
|
|
||||||
case "First Professional Degree":
|
|
||||||
requiredCreditHours = 180; // Typically for professional programs
|
|
||||||
break;
|
|
||||||
case "Graduate/Professional Certificate":
|
|
||||||
requiredCreditHours = parseInt(creditHoursRequired, 10); // User provided input
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
requiredCreditHours = parseInt(creditHoursRequired, 10); // For other cases
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduct completed hours and calculate program length
|
|
||||||
const remainingCreditHours = requiredCreditHours - parseInt(hoursCompleted, 10);
|
|
||||||
const calculatedProgramLength = (remainingCreditHours / creditHoursPerYear).toFixed(2);
|
|
||||||
|
|
||||||
setProgramLength(calculatedProgramLength);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (programType && hoursCompleted && creditHoursPerYear) {
|
|
||||||
calculateProgramLength(); // Recalculate when the program type, completed hours, or credit hours per year change
|
|
||||||
}
|
|
||||||
}, [programType, hoursCompleted, creditHoursPerYear]);
|
|
||||||
|
|
||||||
const handleProgramTypeSelect = (e) => {
|
|
||||||
setProgramType(e.target.value);
|
|
||||||
setCreditHoursRequired(""); // Reset if the user changes program type
|
|
||||||
setProgramLength(""); // Recalculate when the program type changes
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTuitionInput = (e) => {
|
|
||||||
setManualTuition(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreditHoursRequired = (e) => {
|
|
||||||
const value = parseFloat(e.target.value); // Ensure it's parsed as a number
|
|
||||||
setCreditHoursRequired(value);
|
|
||||||
const calculatedProgramLength = value / creditHoursPerYear; // Calculate program length
|
|
||||||
setProgramLength(calculatedProgramLength.toFixed(2)); // Keep two decimal places
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = {
|
|
||||||
currentSalary,
|
|
||||||
additionalIncome,
|
|
||||||
monthlyExpenses,
|
|
||||||
monthlyDebtPayments,
|
|
||||||
retirementSavings,
|
|
||||||
retirementContribution,
|
|
||||||
emergencyFund,
|
|
||||||
inCollege,
|
|
||||||
expectedGraduation,
|
|
||||||
partTimeIncome,
|
|
||||||
tuitionPaid,
|
|
||||||
collegeLoanTotal,
|
|
||||||
existingCollegeDebt,
|
|
||||||
creditHoursPerYear,
|
|
||||||
programType,
|
|
||||||
isFullyOnline,
|
|
||||||
selectedSchool,
|
|
||||||
selectedProgram,
|
|
||||||
tuition: manualTuition || calculatedTuition,
|
|
||||||
hoursCompleted: hoursCompleted ? parseInt(hoursCompleted, 10) : 0,
|
|
||||||
programLength: parseFloat(programLength),
|
|
||||||
creditHoursRequired: parseFloat(creditHoursRequired),
|
|
||||||
loanDeferralUntilGraduation,
|
|
||||||
interestRate: parseFloat(interestRate),
|
|
||||||
loanTerm: parseInt(loanTerm, 10),
|
|
||||||
extraPayment: parseFloat(extraPayment),
|
|
||||||
expectedSalary: parseFloat(expectedSalary),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await authFetch("/api/premium/financial-profile", {
|
const body = {
|
||||||
method: "POST",
|
current_salary: parseFloat(currentSalary) || 0,
|
||||||
headers: { "Content-Type": "application/json" },
|
additional_income: parseFloat(additionalIncome) || 0,
|
||||||
body: JSON.stringify({ user_id: userId, ...formData }),
|
monthly_expenses: parseFloat(monthlyExpenses) || 0,
|
||||||
|
monthly_debt_payments: parseFloat(monthlyDebtPayments) || 0,
|
||||||
|
retirement_savings: parseFloat(retirementSavings) || 0,
|
||||||
|
emergency_fund: parseFloat(emergencyFund) || 0,
|
||||||
|
retirement_contribution: parseFloat(retirementContribution) || 0,
|
||||||
|
monthly_emergency_contribution: parseFloat(monthlyEmergencyContribution) || 0,
|
||||||
|
extra_cash_emergency_pct: parseFloat(extraCashEmergencyPct) || 0,
|
||||||
|
extra_cash_retirement_pct: parseFloat(extraCashRetirementPct) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await authFetch('/api/premium/financial-profile', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
// show success or redirect
|
||||||
setProjectionData(data.projectionData); // Store projection data
|
console.log("Profile updated");
|
||||||
setLoanPayoffMonth(data.loanPaidOffMonth); // Store loan payoff month
|
} else {
|
||||||
|
console.error("Failed to update profile:", await res.text());
|
||||||
navigate('/milestone-tracker', {
|
|
||||||
state: {
|
|
||||||
selectedCareer,
|
|
||||||
projectionData: data.projectionData,
|
|
||||||
loanPayoffMonth: data.loanPaidOffMonth
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error submitting financial profile:", err);
|
console.error("Error submitting financial profile:", err);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleInput = (setter) => (e) => setter(e.target.value);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4 space-y-4 bg-white shadow rounded">
|
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4 space-y-4 bg-white shadow rounded">
|
||||||
<h2 className="text-xl font-semibold">Your Financial Profile</h2>
|
<h2 className="text-xl font-semibold">Edit Your Financial Profile</h2>
|
||||||
|
|
||||||
<label className="block font-medium">Current Salary</label>
|
<label className="block font-medium">Current Salary</label>
|
||||||
<input type="number" value={currentSalary} onChange={handleInput(setCurrentSalary)} className="w-full border rounded p-2" placeholder="$" />
|
<input
|
||||||
|
type="number"
|
||||||
|
value={currentSalary}
|
||||||
|
onChange={(e) => setCurrentSalary(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="block font-medium">Additional Monthly Income</label>
|
<label className="block font-medium">Additional Monthly Income</label>
|
||||||
<input type="number" value={additionalIncome} onChange={handleInput(setAdditionalIncome)} className="w-full border rounded p-2" placeholder="$" />
|
<input
|
||||||
|
type="number"
|
||||||
|
value={additionalIncome}
|
||||||
|
onChange={(e) => setAdditionalIncome(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="block font-medium">Existing College Loan Debt</label>
|
|
||||||
<input type="number" value={collegeLoanTotal} onChange={handleInput(setCollegeLoanTotal)} className="w-full border rounded p-2" placeholder="Enter existing student loan debt" />
|
|
||||||
|
|
||||||
<label className="block font-medium">Monthly Living Expenses</label>
|
<label className="block font-medium">Monthly Living Expenses</label>
|
||||||
<input type="number" value={monthlyExpenses} onChange={handleInput(setMonthlyExpenses)} className="w-full border rounded p-2" placeholder="$" />
|
<input
|
||||||
|
type="number"
|
||||||
|
value={monthlyExpenses}
|
||||||
|
onChange={(e) => setMonthlyExpenses(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="block font-medium">Monthly Debt Payments</label>
|
<label className="block font-medium">Monthly Debt Payments</label>
|
||||||
<input type="number" value={monthlyDebtPayments} onChange={handleInput(setMonthlyDebtPayments)} className="w-full border rounded p-2" placeholder="$" />
|
<input
|
||||||
|
type="number"
|
||||||
|
value={monthlyDebtPayments}
|
||||||
|
onChange={(e) => setMonthlyDebtPayments(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="block font-medium">Retirement Savings</label>
|
<label className="block font-medium">Retirement Savings</label>
|
||||||
<input type="number" value={retirementSavings} onChange={handleInput(setRetirementSavings)} className="w-full border rounded p-2" placeholder="$" />
|
<input
|
||||||
|
type="number"
|
||||||
|
value={retirementSavings}
|
||||||
|
onChange={(e) => setRetirementSavings(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className="block font-medium">Emergency Fund</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={emergencyFund}
|
||||||
|
onChange={(e) => setEmergencyFund(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="block font-medium">Monthly Retirement Contribution</label>
|
<label className="block font-medium">Monthly Retirement Contribution</label>
|
||||||
<input type="number" value={retirementContribution} onChange={handleInput(setRetirementContribution)} className="w-full border rounded p-2" placeholder="$" />
|
<input
|
||||||
|
type="number"
|
||||||
|
value={retirementContribution}
|
||||||
|
onChange={(e) => setRetirementContribution(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="block font-medium">Emergency Fund Balance</label>
|
<label className="block font-medium">Monthly Emergency Contribution</label>
|
||||||
<input type="number" value={emergencyFund} onChange={handleInput(setEmergencyFund)} className="w-full border rounded p-2" placeholder="$" />
|
<input
|
||||||
|
type="number"
|
||||||
|
value={monthlyEmergencyContribution}
|
||||||
|
onChange={(e) => setMonthlyEmergencyContribution(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="$"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<label className="block font-medium">Extra Cash to Emergency (%)</label>
|
||||||
<input id="inCollege" type="checkbox" checked={inCollege} onChange={(e) => setInCollege(e.target.checked)} />
|
<input
|
||||||
<label htmlFor="inCollege" className="font-medium">Are you currently in college?</label>
|
type="number"
|
||||||
</div>
|
value={extraCashEmergencyPct}
|
||||||
|
onChange={(e) => setExtraCashEmergencyPct(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
placeholder="e.g. 30"
|
||||||
|
/>
|
||||||
|
|
||||||
{inCollege && (
|
<label className="block font-medium">Extra Cash to Retirement (%)</label>
|
||||||
<>
|
<input
|
||||||
{/* Selected School input with suggestions */}
|
type="number"
|
||||||
<label className="block font-medium">Selected School</label>
|
value={extraCashRetirementPct}
|
||||||
<input
|
onChange={(e) => setExtraCashRetirementPct(e.target.value)}
|
||||||
type="text"
|
className="w-full border rounded p-2"
|
||||||
value={selectedSchool}
|
placeholder="e.g. 70"
|
||||||
onChange={handleSchoolChange}
|
/>
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="Search for a School"
|
|
||||||
/>
|
|
||||||
{schoolSuggestions.length > 0 && (
|
|
||||||
<ul className="border rounded bg-white max-h-40 overflow-y-auto shadow-md">
|
|
||||||
{schoolSuggestions.map((suggestion, idx) => (
|
|
||||||
<li
|
|
||||||
key={idx}
|
|
||||||
onClick={() => handleSchoolSelect(suggestion)}
|
|
||||||
className="p-2 hover:bg-blue-100 cursor-pointer"
|
|
||||||
>
|
|
||||||
{suggestion}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Program input with suggestions */}
|
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
|
||||||
<label className="block font-medium">Program</label>
|
Save and Continue
|
||||||
<input
|
</button>
|
||||||
type="text"
|
|
||||||
value={selectedProgram}
|
|
||||||
onChange={handleProgramChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="Search for a Program"
|
|
||||||
/>
|
|
||||||
{programSuggestions.length > 0 && (
|
|
||||||
<ul className="border rounded bg-white max-h-40 overflow-y-auto shadow-md">
|
|
||||||
{programSuggestions.map((suggestion, idx) => (
|
|
||||||
<li
|
|
||||||
key={idx}
|
|
||||||
onClick={() => handleProgramSelect(suggestion)}
|
|
||||||
className="p-2 hover:bg-blue-100 cursor-pointer"
|
|
||||||
>
|
|
||||||
{suggestion}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Program Type input */}
|
|
||||||
<label className="block font-medium">Program Type</label>
|
|
||||||
<select
|
|
||||||
value={programType}
|
|
||||||
onChange={handleProgramTypeSelect}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
>
|
|
||||||
<option value="">Select Program Type</option>
|
|
||||||
{availableProgramTypes.map((type, idx) => (
|
|
||||||
<option key={idx} value={type}>
|
|
||||||
{type}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{programType && (programType === "Graduate/Professional Certificate" || programType === "First Professional Degree" || programType === "Doctoral Degree") && (
|
|
||||||
<>
|
|
||||||
<label className="block font-medium">Credit Hours Required</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={creditHoursRequired}
|
|
||||||
onChange={handleCreditHoursRequired}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g. 30"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input id="isInState" type="checkbox" checked={isInState} onChange={(e) => setIsInState(e.target.checked)} />
|
|
||||||
<label htmlFor="isInState" className="font-medium">In-State Tuition</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input id="isFullyOnline" type="checkbox" checked={isFullyOnline} onChange={(e) => setIsFullyOnline(e.target.checked)} />
|
|
||||||
<label htmlFor="isFullyOnline" className="font-medium">Program is Fully Online</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
id="loanDeferralUntilGraduation"
|
|
||||||
type="checkbox"
|
|
||||||
checked={loanDeferralUntilGraduation}
|
|
||||||
onChange={(e) => setLoanDeferralUntilGraduation(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="loanDeferralUntilGraduation" className="font-medium">
|
|
||||||
Loan Payments Deferred Until Graduation?
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="block font-medium">Hours Completed</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={hoursCompleted}
|
|
||||||
onChange={(e) => setHoursCompleted(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g. 30"
|
|
||||||
/>
|
|
||||||
<label className="block font-medium">Credit Hours Per Year</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={creditHoursPerYear}
|
|
||||||
onChange={(e) => setCreditHoursPerYear(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g. 30"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Calculated Yearly Tuition (override if needed)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={manualTuition || calculatedTuition}
|
|
||||||
onChange={handleTuitionInput}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="Override tuition amount"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Loan Interest Rate (%)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={interestRate}
|
|
||||||
onChange={(e) => setInterestRate(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g., 5.5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Loan Term (years)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={loanTerm}
|
|
||||||
onChange={(e) => setLoanTerm(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g., 10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Extra Monthly Payment</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={extraPayment}
|
|
||||||
onChange={(e) => setExtraPayment(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g., 100 (optional)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Expected Salary after Graduation</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={expectedSalary}
|
|
||||||
onChange={(e) => setExpectedSalary(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="pt-4">
|
|
||||||
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
|
|
||||||
Save and Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -41,27 +41,18 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCareerPaths = async () => {
|
const fetchCareerPaths = async () => {
|
||||||
const res = await authFetch(`${apiURL}/premium/planned-path/all`);
|
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
const { careerPaths } = data;
|
||||||
// Flatten nested array
|
setExistingCareerPaths(careerPaths);
|
||||||
const flatPaths = data.careerPath.flat();
|
|
||||||
|
|
||||||
// Handle duplicates
|
|
||||||
const uniquePaths = Array.from(
|
|
||||||
new Set(flatPaths.map(cp => cp.career_name))
|
|
||||||
).map(name => flatPaths.find(cp => cp.career_name === name));
|
|
||||||
|
|
||||||
setExistingCareerPaths(uniquePaths);
|
|
||||||
|
|
||||||
const fromPopout = location.state?.selectedCareer;
|
const fromPopout = location.state?.selectedCareer;
|
||||||
if (fromPopout) {
|
if (fromPopout) {
|
||||||
setSelectedCareer(fromPopout);
|
setSelectedCareer(fromPopout);
|
||||||
setCareerPathId(fromPopout.career_path_id);
|
setCareerPathId(fromPopout.career_path_id);
|
||||||
} else if (!selectedCareer) {
|
} else if (!selectedCareer) {
|
||||||
const latest = await authFetch(`${apiURL}/premium/planned-path/latest`);
|
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
||||||
if (latest) {
|
if (latest) {
|
||||||
const latestData = await latest.json();
|
const latestData = await latest.json();
|
||||||
if (latestData?.id) {
|
if (latestData?.id) {
|
||||||
@ -152,12 +143,16 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const handleConfirmCareerSelection = async () => {
|
const handleConfirmCareerSelection = async () => {
|
||||||
const newId = uuidv4();
|
const newId = uuidv4();
|
||||||
const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] };
|
const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] };
|
||||||
const res = await authFetch(`${apiURL}/premium/planned-path`, { method: 'POST', body: JSON.stringify(body) });
|
const res = await authFetch(`${apiURL}/premium/career-profile`, { method: 'POST', body: JSON.stringify(body) });
|
||||||
if (!res || !res.ok) return;
|
if (!res || !res.ok) return;
|
||||||
setSelectedCareer({ career_name: pendingCareerForModal });
|
const result = await res.json();
|
||||||
setCareerPathId(newId);
|
setCareerPathId(result.career_path_id);
|
||||||
setPendingCareerForModal(null);
|
setSelectedCareer({
|
||||||
};
|
career_name: pendingCareerForModal,
|
||||||
|
id: result.career_path_id
|
||||||
|
});
|
||||||
|
setPendingCareerForModal(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-tracker">
|
<div className="milestone-tracker">
|
||||||
|
@ -1,34 +1,175 @@
|
|||||||
// CareerOnboarding.js
|
// CareerOnboarding.js (inline implementation of career search)
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Input } from '../ui/input.js'; // Ensure path matches your structure
|
||||||
|
import authFetch from '../../utils/authFetch.js';
|
||||||
|
|
||||||
const CareerOnboarding = ({ nextStep, prevStep }) => {
|
const apiURL = process.env.REACT_APP_API_URL;
|
||||||
const [careerData, setCareerData] = useState({
|
|
||||||
currentJob: '',
|
const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||||
industry: '',
|
const [userId, setUserId] = useState(null);
|
||||||
employmentStatus: '',
|
const [currentlyWorking, setCurrentlyWorking] = useState('');
|
||||||
careerGoal: '',
|
const [selectedCareer, setSelectedCareer] = useState('');
|
||||||
});
|
const [careerPathId, setCareerPathId] = useState(null);
|
||||||
|
const [collegeEnrollmentStatus, setCollegeEnrollmentStatus] = useState('');
|
||||||
|
|
||||||
|
const [careers, setCareers] = useState([]);
|
||||||
|
const [searchInput, setSearchInput] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedUserId = localStorage.getItem('userId');
|
||||||
|
if (storedUserId) {
|
||||||
|
setUserId(storedUserId);
|
||||||
|
} else {
|
||||||
|
console.error('User ID not found in localStorage');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch careers exactly once on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCareerTitles = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/career_clusters.json');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const careerTitlesSet = new Set();
|
||||||
|
|
||||||
|
const clusters = Object.keys(data);
|
||||||
|
for (let i = 0; i < clusters.length; i++) {
|
||||||
|
const cluster = clusters[i];
|
||||||
|
const subdivisions = Object.keys(data[cluster]);
|
||||||
|
|
||||||
|
for (let j = 0; j < subdivisions.length; j++) {
|
||||||
|
const subdivision = subdivisions[j];
|
||||||
|
const careersArray = data[cluster][subdivision];
|
||||||
|
|
||||||
|
for (let k = 0; k < careersArray.length; k++) {
|
||||||
|
const careerObj = careersArray[k];
|
||||||
|
if (careerObj.title) {
|
||||||
|
careerTitlesSet.add(careerObj.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCareers([...careerTitlesSet]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching or processing career_clusters.json:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCareerTitles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Update career selection automatically whenever the searchInput matches a valid career explicitly
|
||||||
|
useEffect(() => {
|
||||||
|
if (careers.includes(searchInput)) {
|
||||||
|
setSelectedCareer(searchInput);
|
||||||
|
setData(prev => ({ ...prev, career_name: searchInput }));
|
||||||
|
}
|
||||||
|
}, [searchInput, careers, setData]);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
setCareerData({ ...careerData, [e.target.name]: e.target.value });
|
setData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCareerInputChange = (e) => {
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
setSearchInput(inputValue);
|
||||||
|
|
||||||
|
// only set explicitly when an exact match occurs
|
||||||
|
if (careers.includes(inputValue)) {
|
||||||
|
setSelectedCareer(inputValue);
|
||||||
|
setData(prev => ({ ...prev, career_name: inputValue }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!selectedCareer || !currentlyWorking || !collegeEnrollmentStatus) {
|
||||||
|
alert("Please complete all required fields before continuing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
career_name: selectedCareer,
|
||||||
|
college_enrollment_status: collegeEnrollmentStatus,
|
||||||
|
currently_working: currentlyWorking,
|
||||||
|
status: prevData.status || 'planned',
|
||||||
|
start_date: prevData.start_date || new Date().toISOString(),
|
||||||
|
projected_end_date: prevData.projected_end_date || null,
|
||||||
|
user_id: userId
|
||||||
|
}));
|
||||||
|
|
||||||
|
nextStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Career Details</h2>
|
<h2>Career Details</h2>
|
||||||
<input name="currentJob" placeholder="Current Job Title" onChange={handleChange} />
|
|
||||||
<input name="industry" placeholder="Industry" onChange={handleChange} />
|
<label className="block font-medium">
|
||||||
<select name="employmentStatus" onChange={handleChange}>
|
Are you currently working or earning any income (even part-time)?
|
||||||
<option value="">Employment Status</option>
|
</label>
|
||||||
<option value="FT">Full-Time</option>
|
<select
|
||||||
<option value="PT">Part-Time</option>
|
value={currentlyWorking}
|
||||||
<option value="Unemployed">Unemployed</option>
|
onChange={(e) => setCurrentlyWorking(e.target.value)}
|
||||||
<option value="Student">Student</option>
|
className="w-full border rounded p-2"
|
||||||
|
>
|
||||||
|
<option value="">Select one</option>
|
||||||
|
<option value="yes">Yes</option>
|
||||||
|
<option value="no">No</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Search for Career</h3>
|
||||||
|
<input
|
||||||
|
value={searchInput}
|
||||||
|
onChange={handleCareerInputChange}
|
||||||
|
placeholder="Start typing a career..."
|
||||||
|
list="career-titles"
|
||||||
|
/>
|
||||||
|
<datalist id="career-titles">
|
||||||
|
{careers.map((career, index) => (
|
||||||
|
<option key={index} value={career} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedCareer && <p>Selected Career: <strong>{selectedCareer}</strong></p>}
|
||||||
|
|
||||||
|
<label>Status:</label>
|
||||||
|
<select name="status" onChange={handleChange} value={data.status || ''}>
|
||||||
|
<option value="">Select Status</option>
|
||||||
|
<option value="Planned">Planned</option>
|
||||||
|
<option value="Current">Current</option>
|
||||||
|
<option value="Exploring">Exploring</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Career Start Date:</label>
|
||||||
|
<input name="start_date" type="date" onChange={handleChange} value={data.start_date || ''} />
|
||||||
|
|
||||||
|
<label>Projected End Date (optional):</label>
|
||||||
|
<input name="projected_end_date" type="date" onChange={handleChange} value={data.projected_end_date || ''} />
|
||||||
|
|
||||||
|
<label className="block font-medium">
|
||||||
|
Are you currently enrolled in college or planning to enroll?
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={collegeEnrollmentStatus}
|
||||||
|
onChange={(e) => setCollegeEnrollmentStatus(e.target.value)}
|
||||||
|
className="w-full border rounded p-2"
|
||||||
|
>
|
||||||
|
<option value="">Select one</option>
|
||||||
|
<option value="not_enrolled">Not Enrolled / Not Planning</option>
|
||||||
|
<option value="currently_enrolled">Currently Enrolled</option>
|
||||||
|
<option value="prospective_student">Planning to Enroll (Prospective Student)</option>
|
||||||
</select>
|
</select>
|
||||||
<input name="careerGoal" placeholder="Career Goal (optional)" onChange={handleChange} />
|
|
||||||
|
|
||||||
<button onClick={prevStep}>Back</button>
|
<button onClick={prevStep}>Back</button>
|
||||||
<button onClick={nextStep}>Next: Financial →</button>
|
<button onClick={handleSubmit}>Financial →</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,35 +1,558 @@
|
|||||||
// CollegeOnboarding.js
|
import React, { useState, useEffect } from 'react';
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
const CollegeOnboarding = ({ nextStep, prevStep }) => {
|
function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||||
const [collegeData, setCollegeData] = useState({
|
// CIP / iPEDS local states (purely for CIP data and suggestions)
|
||||||
studentStatus: '',
|
const [schoolData, setSchoolData] = useState([]);
|
||||||
school: '',
|
const [icTuitionData, setIcTuitionData] = useState([]);
|
||||||
program: '',
|
const [schoolSuggestions, setSchoolSuggestions] = useState([]);
|
||||||
creditHoursPerYear: '',
|
const [programSuggestions, setProgramSuggestions] = useState([]);
|
||||||
});
|
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
// ---- DESCTRUCTURE PARENT DATA FOR ALL FIELDS EXCEPT TUITION/PROGRAM_LENGTH ----
|
||||||
setCollegeData({ ...collegeData, [e.target.name]: e.target.value });
|
// We'll store user "typed" values for tuition/program_length in local states,
|
||||||
|
// but everything else comes directly from `data`.
|
||||||
|
const {
|
||||||
|
college_enrollment_status = '',
|
||||||
|
selected_school = '',
|
||||||
|
selected_program = '',
|
||||||
|
program_type = '',
|
||||||
|
academic_calendar = 'semester',
|
||||||
|
annual_financial_aid = '',
|
||||||
|
is_online = false,
|
||||||
|
existing_college_debt = '',
|
||||||
|
expected_graduation = '',
|
||||||
|
interest_rate = 5.5,
|
||||||
|
loan_term = 10,
|
||||||
|
extra_payment = '',
|
||||||
|
expected_salary = '',
|
||||||
|
is_in_state = true,
|
||||||
|
is_in_district = false,
|
||||||
|
loan_deferral_until_graduation = false,
|
||||||
|
credit_hours_per_year = '',
|
||||||
|
hours_completed = '',
|
||||||
|
credit_hours_required = '',
|
||||||
|
tuition_paid = '',
|
||||||
|
// We do NOT consume data.tuition or data.program_length directly here
|
||||||
|
// because we store them in local states (manualTuition, manualProgramLength).
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
// ---- 1. LOCAL STATES for auto/manual logic on TWO fields ----
|
||||||
|
// manualTuition: user typed override
|
||||||
|
// autoTuition: iPEDS calculation
|
||||||
|
const [manualTuition, setManualTuition] = useState(''); // '' means no manual override
|
||||||
|
const [autoTuition, setAutoTuition] = useState(0);
|
||||||
|
|
||||||
|
// same approach for program_length
|
||||||
|
const [manualProgramLength, setManualProgramLength] = useState('');
|
||||||
|
const [autoProgramLength, setAutoProgramLength] = useState('0.00');
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Universal handleChange for all parent fields
|
||||||
|
// ------------------------------------------
|
||||||
|
const handleParentFieldChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
let val = value;
|
||||||
|
if (type === 'checkbox') {
|
||||||
|
val = checked;
|
||||||
|
}
|
||||||
|
// parse numeric fields that are NOT tuition or program_length
|
||||||
|
if (['interest_rate','loan_term','extra_payment','expected_salary'].includes(name)) {
|
||||||
|
val = parseFloat(val) || 0;
|
||||||
|
} else if (
|
||||||
|
['annual_financial_aid','existing_college_debt','credit_hours_per_year',
|
||||||
|
'hours_completed','credit_hours_required','tuition_paid']
|
||||||
|
.includes(name)
|
||||||
|
) {
|
||||||
|
val = val === '' ? '' : parseFloat(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(prev => ({ ...prev, [name]: val }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// handleManualTuition, handleManualProgramLength
|
||||||
|
// for local fields
|
||||||
|
// ------------------------------------------
|
||||||
|
const handleManualTuitionChange = (e) => {
|
||||||
|
// user typed something => override
|
||||||
|
setManualTuition(e.target.value); // store as string for partial typing
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManualProgramLengthChange = (e) => {
|
||||||
|
setManualProgramLength(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// CIP Data fetch once
|
||||||
|
// ------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchCipData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/cip_institution_mapping_new.json');
|
||||||
|
const text = await res.text();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const parsed = lines.map(line => {
|
||||||
|
try { return JSON.parse(line); } catch { return null; }
|
||||||
|
}).filter(Boolean);
|
||||||
|
setSchoolData(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load CIP data:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchCipData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// iPEDS Data fetch once
|
||||||
|
// ------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchIpedsData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/ic2023_ay.csv');
|
||||||
|
const text = await res.text();
|
||||||
|
const rows = text.split('\n').map(line => line.split(','));
|
||||||
|
const headers = rows[0];
|
||||||
|
const dataRows = rows.slice(1).map(row =>
|
||||||
|
Object.fromEntries(row.map((val, idx) => [headers[idx], val]))
|
||||||
|
);
|
||||||
|
setIcTuitionData(dataRows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load iPEDS data:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchIpedsData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// handleSchoolChange, handleProgramChange, etc. => update parent fields
|
||||||
|
// ------------------------------------------
|
||||||
|
const handleSchoolChange = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setData(prev => ({
|
||||||
|
...prev,
|
||||||
|
selected_school: value,
|
||||||
|
selected_program: '',
|
||||||
|
program_type: '',
|
||||||
|
credit_hours_required: '',
|
||||||
|
}));
|
||||||
|
// CIP suggestions
|
||||||
|
const filtered = schoolData.filter(s =>
|
||||||
|
s.INSTNM.toLowerCase().includes(value.toLowerCase())
|
||||||
|
);
|
||||||
|
const uniqueSchools = [...new Set(filtered.map(s => s.INSTNM))];
|
||||||
|
setSchoolSuggestions(uniqueSchools.slice(0, 10));
|
||||||
|
setProgramSuggestions([]);
|
||||||
|
setAvailableProgramTypes([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSchoolSelect = (schoolName) => {
|
||||||
|
setData(prev => ({
|
||||||
|
...prev,
|
||||||
|
selected_school: schoolName,
|
||||||
|
selected_program: '',
|
||||||
|
program_type: '',
|
||||||
|
credit_hours_required: '',
|
||||||
|
}));
|
||||||
|
setSchoolSuggestions([]);
|
||||||
|
setProgramSuggestions([]);
|
||||||
|
setAvailableProgramTypes([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProgramChange = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setData(prev => ({ ...prev, selected_program: value }));
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
setProgramSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filtered = schoolData.filter(
|
||||||
|
s => s.INSTNM.toLowerCase() === selected_school.toLowerCase() &&
|
||||||
|
s.CIPDESC.toLowerCase().includes(value.toLowerCase())
|
||||||
|
);
|
||||||
|
const uniquePrograms = [...new Set(filtered.map(s => s.CIPDESC))];
|
||||||
|
setProgramSuggestions(uniquePrograms.slice(0, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProgramSelect = (prog) => {
|
||||||
|
setData(prev => ({ ...prev, selected_program: prog }));
|
||||||
|
setProgramSuggestions([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProgramTypeSelect = (e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setData(prev => ({
|
||||||
|
...prev,
|
||||||
|
program_type: val,
|
||||||
|
credit_hours_required: '',
|
||||||
|
}));
|
||||||
|
setManualProgramLength(''); // reset manual override
|
||||||
|
setAutoProgramLength('0.00');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Once we have school+program, load possible program types
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selected_program || !selected_school || !schoolData.length) return;
|
||||||
|
const possibleTypes = schoolData
|
||||||
|
.filter(
|
||||||
|
s => s.INSTNM.toLowerCase() === selected_school.toLowerCase() &&
|
||||||
|
s.CIPDESC === selected_program
|
||||||
|
)
|
||||||
|
.map(s => s.CREDDESC);
|
||||||
|
setAvailableProgramTypes([...new Set(possibleTypes)]);
|
||||||
|
}, [selected_program, selected_school, schoolData]);
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Auto-calc Tuition => store in local autoTuition
|
||||||
|
// ------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
// do we have enough to calc?
|
||||||
|
if (!icTuitionData.length) return;
|
||||||
|
if (!selected_school || !program_type || !credit_hours_per_year) return;
|
||||||
|
|
||||||
|
// find row
|
||||||
|
const found = schoolData.find(s => s.INSTNM.toLowerCase() === selected_school.toLowerCase());
|
||||||
|
if (!found) return;
|
||||||
|
const unitId = found.UNITID;
|
||||||
|
if (!unitId) return;
|
||||||
|
|
||||||
|
const match = icTuitionData.find(row => row.UNITID === unitId);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
// grad or undergrad
|
||||||
|
const isGradOrProf = [
|
||||||
|
"Master's Degree",
|
||||||
|
"Doctoral Degree",
|
||||||
|
"Graduate/Professional Certificate",
|
||||||
|
"First Professional Degree"
|
||||||
|
].includes(program_type);
|
||||||
|
|
||||||
|
let partTimeRate = 0;
|
||||||
|
let fullTimeTuition = 0;
|
||||||
|
if (isGradOrProf) {
|
||||||
|
if (is_in_district) {
|
||||||
|
partTimeRate = parseFloat(match.HRCHG5 || 0);
|
||||||
|
fullTimeTuition = parseFloat(match.TUITION5 || 0);
|
||||||
|
} else if (is_in_state) {
|
||||||
|
partTimeRate = parseFloat(match.HRCHG6 || 0);
|
||||||
|
fullTimeTuition = parseFloat(match.TUITION6 || 0);
|
||||||
|
} else {
|
||||||
|
partTimeRate = parseFloat(match.HRCHG7 || 0);
|
||||||
|
fullTimeTuition = parseFloat(match.TUITION7 || 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// undergrad
|
||||||
|
if (is_in_district) {
|
||||||
|
partTimeRate = parseFloat(match.HRCHG1 || 0);
|
||||||
|
fullTimeTuition = parseFloat(match.TUITION1 || 0);
|
||||||
|
} else if (is_in_state) {
|
||||||
|
partTimeRate = parseFloat(match.HRCHG2 || 0);
|
||||||
|
fullTimeTuition = parseFloat(match.TUITION2 || 0);
|
||||||
|
} else {
|
||||||
|
partTimeRate = parseFloat(match.HRCHG3 || 0);
|
||||||
|
fullTimeTuition = parseFloat(match.TUITION3 || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chpy = parseFloat(credit_hours_per_year) || 0;
|
||||||
|
let estimate = 0;
|
||||||
|
// threshold
|
||||||
|
if (chpy < 24 && partTimeRate) {
|
||||||
|
estimate = partTimeRate * chpy;
|
||||||
|
} else {
|
||||||
|
estimate = fullTimeTuition;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAutoTuition(Math.round(estimate));
|
||||||
|
// We do NOT auto-update parent's data. We'll do that in handleSubmit or if you prefer, you can store it in parent's data anyway.
|
||||||
|
}, [
|
||||||
|
icTuitionData, selected_school, program_type,
|
||||||
|
credit_hours_per_year, is_in_state, is_in_district, schoolData
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Auto-calc Program Length => store in local autoProgramLength
|
||||||
|
// ------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
if (!program_type) return;
|
||||||
|
if (!hours_completed || !credit_hours_per_year) return;
|
||||||
|
|
||||||
|
let required = 0;
|
||||||
|
switch (program_type) {
|
||||||
|
case "Associate's Degree": required = 60; break;
|
||||||
|
case "Bachelor's Degree": required = 120; break;
|
||||||
|
case "Master's Degree": required = 60; break;
|
||||||
|
case "Doctoral Degree": required = 120; break;
|
||||||
|
case "First Professional Degree": required = 180; break;
|
||||||
|
case "Graduate/Professional Certificate":
|
||||||
|
required = parseInt(credit_hours_required, 10) || 0; break;
|
||||||
|
default:
|
||||||
|
required = parseInt(credit_hours_required, 10) || 0; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remain = required - (parseInt(hours_completed, 10) || 0);
|
||||||
|
const yrs = remain / (parseFloat(credit_hours_per_year) || 1);
|
||||||
|
const calcLength = yrs.toFixed(2);
|
||||||
|
|
||||||
|
setAutoProgramLength(calcLength);
|
||||||
|
}, [program_type, hours_completed, credit_hours_per_year, credit_hours_required]);
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// handleSubmit => merges final chosen values
|
||||||
|
// ------------------------------------------
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// If user typed a manual value, we use that. If they left it blank,
|
||||||
|
// we use the autoTuition.
|
||||||
|
const chosenTuition = (manualTuition.trim() === '' ? autoTuition : parseFloat(manualTuition));
|
||||||
|
|
||||||
|
// Same for program length
|
||||||
|
const chosenProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||||||
|
|
||||||
|
// Write them into parent's data
|
||||||
|
setData(prev => ({
|
||||||
|
...prev,
|
||||||
|
tuition: chosenTuition,
|
||||||
|
program_length: chosenProgramLength
|
||||||
|
}));
|
||||||
|
|
||||||
|
nextStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
// The displayed tuition => (manualTuition !== '' ? manualTuition : autoTuition)
|
||||||
|
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
|
||||||
|
|
||||||
|
// The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength)
|
||||||
|
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>College Plans (optional)</h2>
|
<h2>College Details</h2>
|
||||||
<select name="studentStatus" onChange={handleChange}>
|
|
||||||
<option value="">Student Status</option>
|
|
||||||
<option value="Enrolled">Currently Enrolled</option>
|
|
||||||
<option value="Prospective">Prospective Student</option>
|
|
||||||
<option value="None">Not Applicable</option>
|
|
||||||
</select>
|
|
||||||
<input name="school" placeholder="School Name" onChange={handleChange} />
|
|
||||||
<input name="program" placeholder="Program Name" onChange={handleChange} />
|
|
||||||
<input name="creditHoursPerYear" placeholder="Credit Hours per Year" type="number" onChange={handleChange} />
|
|
||||||
|
|
||||||
<button onClick={prevStep}>← Previous: Financial</button>
|
{(college_enrollment_status === 'currently_enrolled' ||
|
||||||
<button onClick={nextStep}>Finish Onboarding</button>
|
college_enrollment_status === 'prospective_student') ? (
|
||||||
|
<>
|
||||||
|
<label>School Name*</label>
|
||||||
|
<input
|
||||||
|
name="selected_school"
|
||||||
|
value={selected_school}
|
||||||
|
onChange={handleSchoolChange}
|
||||||
|
list="school-suggestions"
|
||||||
|
placeholder="Enter school name..."
|
||||||
|
/>
|
||||||
|
<datalist id="school-suggestions">
|
||||||
|
{schoolSuggestions.map((sch, idx) => (
|
||||||
|
<option
|
||||||
|
key={idx}
|
||||||
|
value={sch}
|
||||||
|
onClick={() => handleSchoolSelect(sch)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<label>Program Name*</label>
|
||||||
|
<input
|
||||||
|
name="selected_program"
|
||||||
|
value={selected_program}
|
||||||
|
onChange={handleProgramChange}
|
||||||
|
list="program-suggestions"
|
||||||
|
placeholder="Enter program name..."
|
||||||
|
/>
|
||||||
|
<datalist id="program-suggestions">
|
||||||
|
{programSuggestions.map((prog, idx) => (
|
||||||
|
<option
|
||||||
|
key={idx}
|
||||||
|
value={prog}
|
||||||
|
onClick={() => handleProgramSelect(prog)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<label>Program Type*</label>
|
||||||
|
<select
|
||||||
|
name="program_type"
|
||||||
|
value={program_type}
|
||||||
|
onChange={handleProgramTypeSelect}
|
||||||
|
>
|
||||||
|
<option value="">Select Program Type</option>
|
||||||
|
{availableProgramTypes.map((t, i) => (
|
||||||
|
<option key={i} value={t}>{t}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{(program_type === 'Graduate/Professional Certificate' ||
|
||||||
|
program_type === 'First Professional Degree' ||
|
||||||
|
program_type === 'Doctoral Degree') && (
|
||||||
|
<>
|
||||||
|
<label>Credit Hours Required</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="credit_hours_required"
|
||||||
|
value={credit_hours_required}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="e.g. 30"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label>Credit Hours Per Year</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="credit_hours_per_year"
|
||||||
|
value={credit_hours_per_year}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="e.g. 24"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Yearly Tuition</label>
|
||||||
|
{/* If user typed a custom value => manualTuition, else autoTuition */}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={displayedTuition}
|
||||||
|
onChange={handleManualTuitionChange}
|
||||||
|
placeholder="Leave blank to use auto, or type an override"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Annual Financial Aid</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="annual_financial_aid"
|
||||||
|
value={annual_financial_aid}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="e.g. 2000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Existing College Loan Debt</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="existing_college_debt"
|
||||||
|
value={existing_college_debt}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="e.g. 2000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{college_enrollment_status === 'currently_enrolled' && (
|
||||||
|
<>
|
||||||
|
<label>Tuition Paid</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="tuition_paid"
|
||||||
|
value={tuition_paid}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="Already paid"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Hours Completed</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="hours_completed"
|
||||||
|
value={hours_completed}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="Credit hours done"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Program Length</label>
|
||||||
|
{/* If user typed a custom => manualProgramLength, else autoProgramLength */}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={displayedProgramLength}
|
||||||
|
onChange={handleManualProgramLengthChange}
|
||||||
|
placeholder="Leave blank to use auto, or type an override"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label>Expected Graduation</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="expected_graduation"
|
||||||
|
value={expected_graduation}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Loan Interest Rate (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="interest_rate"
|
||||||
|
value={interest_rate}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="e.g. 5.5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Loan Term (years)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="loan_term"
|
||||||
|
value={loan_term}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="e.g. 10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Extra Monthly Payment</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="extra_payment"
|
||||||
|
value={extra_payment}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="Optional"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Expected Salary After Graduation</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="expected_salary"
|
||||||
|
value={expected_salary}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
placeholder="e.g. 65000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="is_in_district"
|
||||||
|
checked={is_in_district}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
In District?
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="is_in_state"
|
||||||
|
checked={is_in_state}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
In State Tuition?
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="is_online"
|
||||||
|
checked={is_online}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
Program is Fully Online
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="loan_deferral_until_graduation"
|
||||||
|
checked={loan_deferral_until_graduation}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
Defer Loan Payments until Graduation?
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>Not currently enrolled or prospective student. Skipping college onboarding.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button onClick={prevStep} style={{ marginRight: '1rem' }}>← Previous</button>
|
||||||
|
<button onClick={handleSubmit}>Finish Onboarding</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CollegeOnboarding;
|
export default CollegeOnboarding;
|
||||||
|
@ -1,25 +1,138 @@
|
|||||||
// FinancialOnboarding.js
|
import React from 'react';
|
||||||
import React, { useState } 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) => {
|
const handleChange = (e) => {
|
||||||
setFinancialData({ ...financialData, [e.target.name]: e.target.value });
|
const { name, value } = e.target;
|
||||||
|
let val = parseFloat(value) || 0;
|
||||||
|
|
||||||
|
if (name === 'extra_cash_emergency_pct') {
|
||||||
|
val = Math.min(Math.max(val, 0), 100);
|
||||||
|
setData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
extra_cash_emergency_pct: val,
|
||||||
|
extra_cash_retirement_pct: 100 - val
|
||||||
|
}));
|
||||||
|
} else if (name === 'extra_cash_retirement_pct') {
|
||||||
|
val = Math.min(Math.max(val, 0), 100);
|
||||||
|
setData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
extra_cash_retirement_pct: val,
|
||||||
|
extra_cash_emergency_pct: 100 - val
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
[name]: val
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Financial Details</h2>
|
<h2>Financial Details</h2>
|
||||||
<input name="salary" placeholder="Annual Salary" type="number" onChange={handleChange} />
|
|
||||||
<input name="expenses" placeholder="Monthly Expenses" type="number" onChange={handleChange} />
|
{currently_working === 'yes' && (
|
||||||
<input name="savings" placeholder="Total Savings" type="number" onChange={handleChange} />
|
<>
|
||||||
<input name="debts" placeholder="Outstanding Debts" type="number" onChange={handleChange} />
|
<input
|
||||||
|
name="current_salary"
|
||||||
|
type="number"
|
||||||
|
placeholder="Current Annual Salary"
|
||||||
|
value={current_salary || ''} // controlled
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="additional_income"
|
||||||
|
type="number"
|
||||||
|
placeholder="Additional Annual Income (Investments, annuitites, additional jobs, etc. - optional)"
|
||||||
|
value={additional_income || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="monthly_expenses"
|
||||||
|
type="number"
|
||||||
|
placeholder="Monthly Expenses"
|
||||||
|
value={monthly_expenses || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="monthly_debt_payments"
|
||||||
|
type="number"
|
||||||
|
placeholder="Monthly Debt Payments (optional)"
|
||||||
|
value={monthly_debt_payments || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="retirement_savings"
|
||||||
|
type="number"
|
||||||
|
placeholder="Retirement Savings"
|
||||||
|
value={retirement_savings || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="retirement_contribution"
|
||||||
|
type="number"
|
||||||
|
placeholder="Monthly Retirement Contribution"
|
||||||
|
value={retirement_contribution || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="emergency_fund"
|
||||||
|
type="number"
|
||||||
|
placeholder="Emergency Fund Savings"
|
||||||
|
value={emergency_fund || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name="emergency_contribution"
|
||||||
|
type="number"
|
||||||
|
placeholder="Monthly Emergency Fund Contribution (optional)"
|
||||||
|
value={emergency_contribution || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3>Extra Monthly Cash Allocation</h3>
|
||||||
|
<p>If you have extra money left each month after expenses, how would you like to allocate it? (Must add to 100%)</p>
|
||||||
|
|
||||||
|
<label>Extra Monthly Cash to Emergency Fund (%)</label>
|
||||||
|
<input
|
||||||
|
name="extra_cash_emergency_pct"
|
||||||
|
type="number"
|
||||||
|
placeholder="% to Emergency Savings (e.g., 30)"
|
||||||
|
value={extra_cash_emergency_pct}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Extra Monthly Cash to Retirement Fund (%)</label>
|
||||||
|
<input
|
||||||
|
name="extra_cash_retirement_pct"
|
||||||
|
type="number"
|
||||||
|
placeholder="% to Retirement Savings (e.g., 70)"
|
||||||
|
value={extra_cash_retirement_pct}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
<button onClick={prevStep}>← Previous: Career</button>
|
<button onClick={prevStep}>← Previous: Career</button>
|
||||||
<button onClick={nextStep}>Next: College →</button>
|
<button onClick={nextStep}>Next: College →</button>
|
||||||
|
@ -1,28 +1,91 @@
|
|||||||
// OnboardingContainer.js
|
// OnboardingContainer.js
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PremiumWelcome from './PremiumWelcome.js';
|
import PremiumWelcome from './PremiumWelcome.js';
|
||||||
import CareerOnboarding from './CareerOnboarding.js';
|
import CareerOnboarding from './CareerOnboarding.js';
|
||||||
import FinancialOnboarding from './FinancialOnboarding.js';
|
import FinancialOnboarding from './FinancialOnboarding.js';
|
||||||
import CollegeOnboarding from './CollegeOnboarding.js';
|
import CollegeOnboarding from './CollegeOnboarding.js';
|
||||||
|
import authFetch from '../../utils/authFetch.js';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const OnboardingContainer = () => {
|
const OnboardingContainer = () => {
|
||||||
|
console.log('OnboardingContainer MOUNT');
|
||||||
|
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
|
const [careerData, setCareerData] = useState({});
|
||||||
|
const [financialData, setFinancialData] = useState({});
|
||||||
|
const [collegeData, setCollegeData] = useState({});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const nextStep = () => setStep(step + 1);
|
const nextStep = () => setStep(step + 1);
|
||||||
const prevStep = () => setStep(step - 1);
|
const prevStep = () => setStep(step - 1);
|
||||||
|
|
||||||
|
const submitData = async () => {
|
||||||
|
await authFetch('/api/premium/career-profile', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(careerData),
|
||||||
|
});
|
||||||
|
|
||||||
|
await authFetch('/api/premium/financial-profile', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(financialData),
|
||||||
|
});
|
||||||
|
|
||||||
|
await authFetch('/api/premium/college-profile', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(collegeData),
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate('/milestone-tracker');
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('collegeData to submit:', collegeData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => console.log('OnboardingContainer UNMOUNT');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Merge the parent's collegeData with the override from careerData
|
||||||
|
const mergedCollegeData = {
|
||||||
|
...collegeData,
|
||||||
|
// If careerData has a truthy enrollment_status, override
|
||||||
|
college_enrollment_status:
|
||||||
|
careerData.college_enrollment_status ?? collegeData.college_enrollment_status
|
||||||
|
};
|
||||||
|
|
||||||
const onboardingSteps = [
|
const onboardingSteps = [
|
||||||
<PremiumWelcome nextStep={nextStep} />,
|
<PremiumWelcome nextStep={nextStep} />,
|
||||||
<CareerOnboarding nextStep={nextStep} prevStep={prevStep} />,
|
|
||||||
<FinancialOnboarding nextStep={nextStep} prevStep={prevStep} />,
|
<CareerOnboarding
|
||||||
<CollegeOnboarding nextStep={nextStep} prevStep={prevStep} />,
|
nextStep={nextStep}
|
||||||
|
data={careerData}
|
||||||
|
setData={setCareerData}
|
||||||
|
/>,
|
||||||
|
|
||||||
|
<FinancialOnboarding
|
||||||
|
nextStep={nextStep}
|
||||||
|
prevStep={prevStep}
|
||||||
|
data={{
|
||||||
|
...financialData,
|
||||||
|
currently_working: careerData.currently_working,
|
||||||
|
}}
|
||||||
|
setData={setFinancialData}
|
||||||
|
/>,
|
||||||
|
|
||||||
|
<CollegeOnboarding
|
||||||
|
nextStep={submitData}
|
||||||
|
prevStep={prevStep}
|
||||||
|
|
||||||
|
// Pass the merged data so that college_enrollment_status is never lost
|
||||||
|
data={mergedCollegeData}
|
||||||
|
setData={setCollegeData}
|
||||||
|
/>
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return <div>{onboardingSteps[step]}</div>;
|
||||||
<div className="onboarding-container">
|
|
||||||
{onboardingSteps[step]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OnboardingContainer;
|
export default OnboardingContainer;
|
||||||
|
@ -1,137 +1,318 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
// Function to simulate monthly financial projection
|
// Example fields in userProfile that matter here:
|
||||||
// src/utils/FinancialProjectionService.js
|
// - academicCalendar: 'semester' | 'quarter' | 'trimester' | 'monthly'
|
||||||
|
// - annualFinancialAid: amount of scholarships/grants per year
|
||||||
|
// - inCollege, loanDeferralUntilGraduation, graduationDate, etc.
|
||||||
|
//
|
||||||
|
// Additional logic now for lumps instead of monthly tuition payments.
|
||||||
|
|
||||||
export function simulateFinancialProjection(userProfile) {
|
export function simulateFinancialProjection(userProfile) {
|
||||||
const {
|
const {
|
||||||
currentSalary,
|
// Income & expenses
|
||||||
monthlyExpenses,
|
currentSalary = 0,
|
||||||
monthlyDebtPayments,
|
monthlyExpenses = 0,
|
||||||
studentLoanAmount,
|
monthlyDebtPayments = 0,
|
||||||
interestRate, // ✅ Corrected
|
partTimeIncome = 0,
|
||||||
loanTerm, // ✅ Corrected
|
extraPayment = 0,
|
||||||
extraPayment,
|
|
||||||
expectedSalary,
|
|
||||||
emergencySavings,
|
|
||||||
retirementSavings,
|
|
||||||
monthlyRetirementContribution,
|
|
||||||
monthlyEmergencyContribution,
|
|
||||||
gradDate,
|
|
||||||
fullTimeCollegeStudent: inCollege,
|
|
||||||
partTimeIncome,
|
|
||||||
startDate,
|
|
||||||
programType,
|
|
||||||
isFullyOnline,
|
|
||||||
creditHoursPerYear,
|
|
||||||
calculatedTuition,
|
|
||||||
hoursCompleted,
|
|
||||||
loanDeferralUntilGraduation,
|
|
||||||
programLength
|
|
||||||
} = userProfile;
|
|
||||||
|
|
||||||
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
// Loan info
|
||||||
|
studentLoanAmount = 0,
|
||||||
|
interestRate = 5, // %
|
||||||
|
loanTerm = 10, // years
|
||||||
|
loanDeferralUntilGraduation = false,
|
||||||
|
|
||||||
let totalEmergencySavings = emergencySavings;
|
// College & tuition
|
||||||
let totalRetirementSavings = retirementSavings;
|
inCollege = false,
|
||||||
let loanBalance = studentLoanAmount;
|
programType,
|
||||||
let projectionData = [];
|
hoursCompleted = 0,
|
||||||
|
creditHoursPerYear = 30,
|
||||||
|
calculatedTuition = 10000, // e.g. annual tuition
|
||||||
|
gradDate, // known graduation date, or null
|
||||||
|
startDate, // when sim starts
|
||||||
|
academicCalendar = 'monthly', // new
|
||||||
|
annualFinancialAid = 0,
|
||||||
|
|
||||||
const graduationDate = gradDate ? new Date(gradDate) : null;
|
// Salary after graduation
|
||||||
let milestoneIndex = 0;
|
expectedSalary = 0,
|
||||||
let loanPaidOffMonth = null;
|
|
||||||
|
|
||||||
// Dynamic credit hours based on the program type
|
// Savings
|
||||||
let requiredCreditHours;
|
emergencySavings = 0,
|
||||||
switch (programType) {
|
retirementSavings = 0,
|
||||||
case "Associate Degree":
|
|
||||||
requiredCreditHours = 60;
|
// Monthly contributions
|
||||||
break;
|
monthlyRetirementContribution = 0,
|
||||||
case "Bachelor's Degree":
|
monthlyEmergencyContribution = 0,
|
||||||
requiredCreditHours = 120;
|
|
||||||
break;
|
// Surplus allocation
|
||||||
case "Master's Degree":
|
surplusEmergencyAllocation = 50,
|
||||||
requiredCreditHours = 30;
|
surplusRetirementAllocation = 50,
|
||||||
break;
|
|
||||||
case "Doctoral Degree":
|
// Potential override
|
||||||
requiredCreditHours = 60;
|
programLength
|
||||||
break;
|
} = userProfile;
|
||||||
default:
|
|
||||||
requiredCreditHours = 120;
|
// 1. Calculate standard monthly loan payment
|
||||||
|
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
||||||
|
|
||||||
|
// 2. Determine how many credit hours remain
|
||||||
|
let requiredCreditHours = 120;
|
||||||
|
switch (programType) {
|
||||||
|
case "Associate's Degree":
|
||||||
|
requiredCreditHours = 60;
|
||||||
|
break;
|
||||||
|
case "Bachelor's Degree":
|
||||||
|
requiredCreditHours = 120;
|
||||||
|
break;
|
||||||
|
case "Master's Degree":
|
||||||
|
requiredCreditHours = 30;
|
||||||
|
break;
|
||||||
|
case "Doctoral Degree":
|
||||||
|
requiredCreditHours = 60;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
requiredCreditHours = 120;
|
||||||
|
}
|
||||||
|
const remainingCreditHours = Math.max(0, requiredCreditHours - hoursCompleted);
|
||||||
|
const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear);
|
||||||
|
const finalProgramLength = programLength || dynamicProgramLength;
|
||||||
|
|
||||||
|
// 3. Net annual tuition after financial aid
|
||||||
|
const netAnnualTuition = Math.max(0, calculatedTuition - annualFinancialAid);
|
||||||
|
const totalTuitionCost = netAnnualTuition * finalProgramLength;
|
||||||
|
|
||||||
|
// 4. Setup lumps per year based on academicCalendar
|
||||||
|
let lumpsPerYear = 12; // monthly fallback
|
||||||
|
let lumpsSchedule = []; // which months from start of academic year
|
||||||
|
|
||||||
|
// We'll store an array of month offsets in a single year (0-based)
|
||||||
|
// for semester, quarter, trimester
|
||||||
|
switch (academicCalendar) {
|
||||||
|
case 'semester':
|
||||||
|
lumpsPerYear = 2;
|
||||||
|
lumpsSchedule = [0, 6]; // months 0 & 6 from start of each academic year
|
||||||
|
break;
|
||||||
|
case 'quarter':
|
||||||
|
lumpsPerYear = 4;
|
||||||
|
lumpsSchedule = [0, 3, 6, 9];
|
||||||
|
break;
|
||||||
|
case 'trimester':
|
||||||
|
lumpsPerYear = 3;
|
||||||
|
lumpsSchedule = [0, 4, 8];
|
||||||
|
break;
|
||||||
|
case 'monthly':
|
||||||
|
default:
|
||||||
|
lumpsPerYear = 12;
|
||||||
|
lumpsSchedule = [...Array(12).keys()]; // 0..11
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each academic year is 12 months, for finalProgramLength years => totalAcademicMonths
|
||||||
|
const totalAcademicMonths = finalProgramLength * 12;
|
||||||
|
// Each lump sum = totalTuitionCost / (lumpsPerYear * finalProgramLength)
|
||||||
|
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
|
||||||
|
|
||||||
|
// 5. We'll loop for up to 20 years
|
||||||
|
const maxMonths = 240;
|
||||||
|
let date = startDate ? new Date(startDate) : new Date();
|
||||||
|
let loanBalance = studentLoanAmount;
|
||||||
|
let loanPaidOffMonth = null;
|
||||||
|
let currentEmergencySavings = emergencySavings;
|
||||||
|
let currentRetirementSavings = retirementSavings;
|
||||||
|
let projectionData = [];
|
||||||
|
|
||||||
|
// Convert gradDate to actual if present
|
||||||
|
const graduationDate = gradDate ? new Date(gradDate) : null;
|
||||||
|
|
||||||
|
for (let month = 0; month < maxMonths; month++) {
|
||||||
|
date.setMonth(date.getMonth() + 1);
|
||||||
|
|
||||||
|
// If loan is fully paid, record if not done already
|
||||||
|
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
||||||
|
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingCreditHours = requiredCreditHours - hoursCompleted;
|
// Are we still in college? We either trust gradDate or approximate finalProgramLength
|
||||||
const programDuration = Math.ceil(remainingCreditHours / creditHoursPerYear);
|
let stillInCollege = false;
|
||||||
const tuitionCost = calculatedTuition;
|
if (inCollege) {
|
||||||
const totalTuitionCost = tuitionCost * programDuration;
|
if (graduationDate) {
|
||||||
|
stillInCollege = date < graduationDate;
|
||||||
|
} else {
|
||||||
|
// approximate by how many months since start
|
||||||
|
const simStart = startDate ? new Date(startDate) : new Date();
|
||||||
|
const elapsedMonths =
|
||||||
|
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
||||||
|
(date.getMonth() - simStart.getMonth());
|
||||||
|
stillInCollege = (elapsedMonths < totalAcademicMonths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const date = new Date(startDate);
|
// 6. If we pay lumps: check if this is a "lump" month within the user's academic year
|
||||||
for (let month = 0; month < 240; month++) {
|
// We'll find how many academic years have passed since they started
|
||||||
date.setMonth(date.getMonth() + 1);
|
let tuitionCostThisMonth = 0;
|
||||||
|
if (stillInCollege && lumpsPerYear > 0) {
|
||||||
|
const simStart = startDate ? new Date(startDate) : new Date();
|
||||||
|
const elapsedMonths =
|
||||||
|
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
||||||
|
(date.getMonth() - simStart.getMonth());
|
||||||
|
|
||||||
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
// Which academic year index are we in?
|
||||||
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
||||||
}
|
// Within that year, which month are we in? (0..11)
|
||||||
|
const monthInYear = elapsedMonths % 12;
|
||||||
|
|
||||||
let tuitionCostThisMonth = 0;
|
// If we find monthInYear in lumpsSchedule, then lumps are due
|
||||||
if (inCollege && !loanDeferralUntilGraduation) {
|
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
||||||
tuitionCostThisMonth = totalTuitionCost / programDuration / 12;
|
tuitionCostThisMonth = lumpAmount;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let thisMonthLoanPayment = 0;
|
// 7. Decide if user defers or pays out of pocket
|
||||||
|
// If deferring, add lumps to loan
|
||||||
|
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||||
|
// Instead of user paying out of pocket, add to loan
|
||||||
|
if (tuitionCostThisMonth > 0) {
|
||||||
|
loanBalance += tuitionCostThisMonth;
|
||||||
|
tuitionCostThisMonth = 0; // paid by the loan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loanDeferralUntilGraduation && graduationDate && date < graduationDate) {
|
// 8. monthly income
|
||||||
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here
|
let monthlyIncome = 0;
|
||||||
loanBalance += interestForMonth;
|
if (!inCollege || !stillInCollege) {
|
||||||
} else if (loanBalance > 0) {
|
// user has graduated or never in college
|
||||||
const interestForMonth = loanBalance * (interestRate / 100 / 12); // ✅ Corrected here
|
monthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
|
||||||
const principalForMonth = Math.min(loanBalance, monthlyLoanPayment + extraPayment - interestForMonth);
|
} else {
|
||||||
loanBalance -= principalForMonth;
|
// in college => currentSalary + partTimeIncome
|
||||||
loanBalance = Math.max(loanBalance, 0);
|
monthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
|
||||||
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const salaryNow = graduationDate && date >= graduationDate ? expectedSalary : currentSalary;
|
// 9. mandatory expenses (excluding student loan if deferring)
|
||||||
|
let thisMonthLoanPayment = 0;
|
||||||
|
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth;
|
||||||
|
|
||||||
const totalMonthlyExpenses = monthlyExpenses
|
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||||
+ tuitionCostThisMonth
|
// Accrue interest only
|
||||||
+ monthlyDebtPayments
|
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||||
+ thisMonthLoanPayment;
|
loanBalance += interestForMonth;
|
||||||
|
} else {
|
||||||
|
// Normal loan repayment if loan > 0
|
||||||
|
if (loanBalance > 0) {
|
||||||
|
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||||
|
const principalForMonth = Math.min(
|
||||||
|
loanBalance,
|
||||||
|
(monthlyLoanPayment + extraPayment) - interestForMonth
|
||||||
|
);
|
||||||
|
loanBalance -= principalForMonth;
|
||||||
|
loanBalance = Math.max(loanBalance, 0);
|
||||||
|
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
|
||||||
|
totalMonthlyExpenses += thisMonthLoanPayment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const monthlyIncome = salaryNow / 12;
|
// 10. leftover after mandatory expenses
|
||||||
|
let leftover = monthlyIncome - totalMonthlyExpenses;
|
||||||
|
if (leftover < 0) {
|
||||||
|
leftover = 0; // can't do partial negative leftover; they simply can't afford it
|
||||||
|
}
|
||||||
|
|
||||||
let extraCash = monthlyIncome - totalMonthlyExpenses - monthlyRetirementContribution - monthlyEmergencyContribution;
|
// Baseline monthly contributions
|
||||||
extraCash = Math.max(extraCash, 0);
|
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
|
||||||
|
let effectiveRetirementContribution = 0;
|
||||||
|
let effectiveEmergencyContribution = 0;
|
||||||
|
|
||||||
// update savings explicitly with contributions first
|
if (leftover >= baselineContributions) {
|
||||||
totalEmergencySavings += monthlyEmergencyContribution + (extraCash * 0.3);
|
effectiveRetirementContribution = monthlyRetirementContribution;
|
||||||
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7);
|
effectiveEmergencyContribution = monthlyEmergencyContribution;
|
||||||
totalRetirementSavings *= (1 + 0.07 / 12);
|
leftover -= baselineContributions;
|
||||||
|
} else {
|
||||||
|
// not enough leftover
|
||||||
|
// for real life, we typically set them to 0 if we can't afford them
|
||||||
|
// or reduce proportionally. We'll do the simpler approach: set them to 0
|
||||||
|
// as requested.
|
||||||
|
effectiveRetirementContribution = 0;
|
||||||
|
effectiveEmergencyContribution = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// netSavings calculation fixed
|
// 11. Now see if leftover is negative => shortfall from mandatory expenses
|
||||||
projectionData.push({
|
// Actually we zeroed leftover if it was negative. So let's check if the user
|
||||||
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
// truly can't afford mandatoryExpenses
|
||||||
salary: salaryNow,
|
const totalMandatoryPlusContrib = monthlyIncome - leftover;
|
||||||
monthlyIncome: monthlyIncome,
|
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||||
expenses: totalMonthlyExpenses,
|
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||||
loanPayment: thisMonthLoanPayment,
|
let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
|
||||||
retirementContribution: monthlyRetirementContribution,
|
if (shortfall > 0) {
|
||||||
emergencyContribution: monthlyEmergencyContribution,
|
// We can reduce from emergency savings
|
||||||
netSavings: monthlyIncome - totalMonthlyExpenses, // Exclude contributions here explicitly!
|
const canCover = Math.min(shortfall, currentEmergencySavings);
|
||||||
totalEmergencySavings,
|
currentEmergencySavings -= canCover;
|
||||||
totalRetirementSavings,
|
shortfall -= canCover;
|
||||||
loanBalance
|
if (shortfall > 0) {
|
||||||
});
|
// user is effectively bankrupt
|
||||||
|
// we can break out or keep going to show negative net worth
|
||||||
|
// For demonstration, let's break
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
// 12. If leftover > 0 after baseline contributions, allocate surplus
|
||||||
|
// (we do it after we've handled shortfall)
|
||||||
|
const newLeftover = leftover; // leftover not used for baseline
|
||||||
return { projectionData, loanPaidOffMonth, emergencySavings };
|
let surplusUsed = 0;
|
||||||
}
|
if (newLeftover > 0) {
|
||||||
|
// Allocate by percent
|
||||||
|
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
|
||||||
|
const emergencyPortion = newLeftover * (surplusEmergencyAllocation / totalPct);
|
||||||
|
const retirementPortion = newLeftover * (surplusRetirementAllocation / totalPct);
|
||||||
|
|
||||||
function calculateLoanPayment(principal, annualRate, years) {
|
currentEmergencySavings += emergencyPortion;
|
||||||
const monthlyRate = annualRate / 100 / 12;
|
currentRetirementSavings += retirementPortion;
|
||||||
const numPayments = years * 12;
|
surplusUsed = newLeftover;
|
||||||
return (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -numPayments));
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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