Fixed college_enrollment_status, career_path_id, inCollege check to get to the simulation through the 3 onboarding steps.
This commit is contained in:
parent
6f01c1c9ae
commit
ab7e318492
@ -557,130 +557,178 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
|
||||
COLLEGE PROFILES
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
|
||||
const {
|
||||
career_path_id,
|
||||
selected_school,
|
||||
selected_program,
|
||||
program_type,
|
||||
is_in_state,
|
||||
is_in_district,
|
||||
college_enrollment_status,
|
||||
is_online,
|
||||
credit_hours_per_year,
|
||||
credit_hours_required,
|
||||
hours_completed,
|
||||
program_length,
|
||||
expected_graduation,
|
||||
existing_college_debt,
|
||||
interest_rate,
|
||||
loan_term,
|
||||
loan_deferral_until_graduation,
|
||||
extra_payment,
|
||||
expected_salary,
|
||||
academic_calendar,
|
||||
annual_financial_aid,
|
||||
tuition,
|
||||
tuition_paid
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
const id = uuidv4();
|
||||
const user_id = req.userId;
|
||||
await db.run(`
|
||||
INSERT INTO college_profiles (
|
||||
id,
|
||||
user_id,
|
||||
career_path_id,
|
||||
selected_school,
|
||||
selected_program,
|
||||
program_type,
|
||||
is_in_state,
|
||||
is_in_district,
|
||||
college_enrollment_status,
|
||||
annual_financial_aid,
|
||||
is_online,
|
||||
credit_hours_per_year,
|
||||
hours_completed,
|
||||
program_length,
|
||||
credit_hours_required,
|
||||
expected_graduation,
|
||||
existing_college_debt,
|
||||
interest_rate,
|
||||
loan_term,
|
||||
loan_deferral_until_graduation,
|
||||
extra_payment,
|
||||
expected_salary,
|
||||
academic_calendar,
|
||||
tuition,
|
||||
tuition_paid,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, -- id
|
||||
?, -- user_id
|
||||
?, -- career_path_id
|
||||
?, -- selected_school
|
||||
?, -- selected_program
|
||||
?, -- program_type
|
||||
?, -- is_in_state
|
||||
?, -- is_in_district
|
||||
?, -- college_enrollment_status
|
||||
?, -- annual_financial_aid
|
||||
?, -- is_online
|
||||
?, -- credit_hours_per_year
|
||||
?, -- hours_completed
|
||||
?, -- program_length
|
||||
?, -- credit_hours_required
|
||||
?, -- expected_graduation
|
||||
?, -- existing_college_debt
|
||||
?, -- interest_rate
|
||||
?, -- loan_term
|
||||
?, -- loan_deferral_until_graduation
|
||||
?, -- extra_payment
|
||||
?, -- expected_salary
|
||||
?, -- academic_calendar
|
||||
?, -- tuition
|
||||
?, -- tuition_paid
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
`, [
|
||||
id,
|
||||
user_id,
|
||||
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
|
||||
const {
|
||||
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
|
||||
]);
|
||||
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;
|
||||
|
||||
res.status(201).json({
|
||||
message: 'College profile saved.',
|
||||
collegeProfileId: id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving college profile:', error);
|
||||
res.status(500).json({ error: 'Failed to save college profile.' });
|
||||
}
|
||||
try {
|
||||
const user_id = req.userId;
|
||||
// For upsert, we either generate a new ID or (optionally) do a lookup for the old row's ID if you want to preserve it
|
||||
// For simplicity, let's generate a new ID each time. We'll handle the conflict resolution below.
|
||||
const newId = uuidv4();
|
||||
|
||||
// Now do an INSERT ... ON CONFLICT(...fields...). In SQLite, we reference 'excluded' for the new values.
|
||||
await db.run(`
|
||||
INSERT INTO college_profiles (
|
||||
id,
|
||||
user_id,
|
||||
career_path_id,
|
||||
selected_school,
|
||||
selected_program,
|
||||
program_type,
|
||||
is_in_state,
|
||||
is_in_district,
|
||||
college_enrollment_status,
|
||||
annual_financial_aid,
|
||||
is_online,
|
||||
credit_hours_per_year,
|
||||
hours_completed,
|
||||
program_length,
|
||||
credit_hours_required,
|
||||
expected_graduation,
|
||||
existing_college_debt,
|
||||
interest_rate,
|
||||
loan_term,
|
||||
loan_deferral_until_graduation,
|
||||
extra_payment,
|
||||
expected_salary,
|
||||
academic_calendar,
|
||||
tuition,
|
||||
tuition_paid,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
:id,
|
||||
:user_id,
|
||||
:career_path_id,
|
||||
:selected_school,
|
||||
:selected_program,
|
||||
:program_type,
|
||||
:is_in_state,
|
||||
:is_in_district,
|
||||
:college_enrollment_status,
|
||||
:annual_financial_aid,
|
||||
:is_online,
|
||||
:credit_hours_per_year,
|
||||
:hours_completed,
|
||||
:program_length,
|
||||
:credit_hours_required,
|
||||
:expected_graduation,
|
||||
:existing_college_debt,
|
||||
:interest_rate,
|
||||
:loan_term,
|
||||
:loan_deferral_until_graduation,
|
||||
:extra_payment,
|
||||
:expected_salary,
|
||||
:academic_calendar,
|
||||
:tuition,
|
||||
:tuition_paid,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
|
||||
-- The magic:
|
||||
ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type)
|
||||
DO UPDATE SET
|
||||
is_in_state = excluded.is_in_state,
|
||||
is_in_district = excluded.is_in_district,
|
||||
college_enrollment_status = excluded.college_enrollment_status,
|
||||
annual_financial_aid = excluded.annual_financial_aid,
|
||||
is_online = excluded.is_online,
|
||||
credit_hours_per_year = excluded.credit_hours_per_year,
|
||||
hours_completed = excluded.hours_completed,
|
||||
program_length = excluded.program_length,
|
||||
credit_hours_required = excluded.credit_hours_required,
|
||||
expected_graduation = excluded.expected_graduation,
|
||||
existing_college_debt = excluded.existing_college_debt,
|
||||
interest_rate = excluded.interest_rate,
|
||||
loan_term = excluded.loan_term,
|
||||
loan_deferral_until_graduation = excluded.loan_deferral_until_graduation,
|
||||
extra_payment = excluded.extra_payment,
|
||||
expected_salary = excluded.expected_salary,
|
||||
academic_calendar = excluded.academic_calendar,
|
||||
tuition = excluded.tuition,
|
||||
tuition_paid = excluded.tuition_paid,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
;
|
||||
`, {
|
||||
':id': newId,
|
||||
':user_id': user_id,
|
||||
':career_path_id': career_path_id,
|
||||
':selected_school': selected_school,
|
||||
':selected_program': selected_program,
|
||||
':program_type': program_type || null,
|
||||
':is_in_state': is_in_state ? 1 : 0,
|
||||
':is_in_district': is_in_district ? 1 : 0,
|
||||
':college_enrollment_status': college_enrollment_status || null,
|
||||
':annual_financial_aid': annual_financial_aid || 0,
|
||||
':is_online': is_online ? 1 : 0,
|
||||
':credit_hours_per_year': credit_hours_per_year || 0,
|
||||
':hours_completed': hours_completed || 0,
|
||||
':program_length': program_length || 0,
|
||||
':credit_hours_required': credit_hours_required || 0,
|
||||
':expected_graduation': expected_graduation || null,
|
||||
':existing_college_debt': existing_college_debt || 0,
|
||||
':interest_rate': interest_rate || 0,
|
||||
':loan_term': loan_term || 10,
|
||||
':loan_deferral_until_graduation': loan_deferral_until_graduation ? 1 : 0,
|
||||
':extra_payment': extra_payment || 0,
|
||||
':expected_salary': expected_salary || 0,
|
||||
':academic_calendar': academic_calendar || 'semester',
|
||||
':tuition': tuition || 0,
|
||||
':tuition_paid': tuition_paid || 0
|
||||
});
|
||||
|
||||
// If it was a conflict, the existing row is updated.
|
||||
// If not, a new row is inserted with ID = newId.
|
||||
|
||||
res.status(201).json({
|
||||
message: 'College profile upsert done.',
|
||||
// You might do an extra SELECT here to find which ID the final row uses if you need it
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving college profile:', error);
|
||||
res.status(500).json({ error: 'Failed to save college profile.' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
|
||||
const { careerPathId } = req.query;
|
||||
// find row
|
||||
const row = await db.get(`
|
||||
SELECT *
|
||||
FROM college_profiles
|
||||
WHERE user_id = ?
|
||||
AND career_path_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, [req.userId, careerPathId]);
|
||||
res.json(row || {});
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
|
@ -12,12 +12,10 @@ import CareerSearch from './CareerSearch.js';
|
||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||
import './MilestoneTracker.css';
|
||||
import './MilestoneTimeline.css'; // Ensure this file contains styles for timeline-line and milestone-dot
|
||||
import './MilestoneTimeline.css';
|
||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||
|
||||
ChartJS.register( LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin );
|
||||
|
||||
|
||||
ChartJS.register(LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin);
|
||||
|
||||
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
const location = useLocation();
|
||||
@ -28,24 +26,29 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||
const [activeView, setActiveView] = useState("Career");
|
||||
const [financialProfile, setFinancialProfile] = useState(null); // Store the financial profile
|
||||
const {
|
||||
projectionData: initialProjectionData = [],
|
||||
loanPayoffMonth: initialLoanPayoffMonth = null,
|
||||
} = location.state || {};
|
||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(initialLoanPayoffMonth);
|
||||
const [projectionData, setProjectionData] = useState(initialProjectionData);
|
||||
|
||||
// Store each profile separately
|
||||
const [financialProfile, setFinancialProfile] = useState(null);
|
||||
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||
|
||||
// For the chart
|
||||
const [projectionData, setProjectionData] = useState([]);
|
||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
||||
|
||||
const apiURL = process.env.REACT_APP_API_URL;
|
||||
|
||||
// Possibly loaded from location.state
|
||||
const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {};
|
||||
|
||||
// ----------------------------
|
||||
// 1. Fetch career paths + financialProfile
|
||||
// ----------------------------
|
||||
useEffect(() => {
|
||||
const fetchCareerPaths = async () => {
|
||||
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
||||
if (!res) return;
|
||||
if (!res || !res.ok) return;
|
||||
const data = await res.json();
|
||||
const { careerPaths } = data;
|
||||
setExistingCareerPaths(careerPaths);
|
||||
setExistingCareerPaths(data.careerPaths);
|
||||
|
||||
const fromPopout = location.state?.selectedCareer;
|
||||
if (fromPopout) {
|
||||
@ -53,7 +56,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
setCareerPathId(fromPopout.career_path_id);
|
||||
} else if (!selectedCareer) {
|
||||
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
||||
if (latest) {
|
||||
if (latest && latest.ok) {
|
||||
const latestData = await latest.json();
|
||||
if (latestData?.id) {
|
||||
setSelectedCareer(latestData);
|
||||
@ -67,199 +70,245 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
const res = await authFetch(`${apiURL}/premium/financial-profile`);
|
||||
if (res && res.ok) {
|
||||
const data = await res.json();
|
||||
setFinancialProfile(data); // Set the financial profile in state
|
||||
setFinancialProfile(data);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCareerPaths();
|
||||
fetchFinancialProfile();
|
||||
}, []);
|
||||
}, [apiURL, location.state, selectedCareer]);
|
||||
|
||||
// ----------------------------
|
||||
// 2. Fetch the college profile for the selected careerPathId
|
||||
// ----------------------------
|
||||
useEffect(() => {
|
||||
if (financialProfile && selectedCareer) {
|
||||
const { projectionData, loanPaidOffMonth, emergencySavings } = simulateFinancialProjection({
|
||||
currentSalary: financialProfile.current_salary,
|
||||
monthlyExpenses: financialProfile.monthly_expenses,
|
||||
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
||||
studentLoanAmount: financialProfile.college_loan_total,
|
||||
if (!careerPathId) {
|
||||
setCollegeProfile(null);
|
||||
return;
|
||||
}
|
||||
|
||||
interestRate: financialProfile.interest_rate || 5.5,
|
||||
loanTerm: financialProfile.loan_term || 10,
|
||||
extraPayment: financialProfile.extra_payment || 0,
|
||||
expectedSalary: financialProfile.expected_salary || financialProfile.current_salary,
|
||||
|
||||
emergencySavings: financialProfile.emergency_fund,
|
||||
retirementSavings: financialProfile.retirement_savings,
|
||||
monthlyRetirementContribution: financialProfile.retirement_contribution,
|
||||
monthlyEmergencyContribution: 0,
|
||||
gradDate: financialProfile.expected_graduation,
|
||||
fullTimeCollegeStudent: financialProfile.in_college,
|
||||
partTimeIncome: financialProfile.part_time_income,
|
||||
startDate: new Date(),
|
||||
|
||||
programType: financialProfile.program_type,
|
||||
isFullyOnline: financialProfile.is_online,
|
||||
creditHoursPerYear: financialProfile.credit_hours_per_year,
|
||||
calculatedTuition: financialProfile.tuition,
|
||||
hoursCompleted: financialProfile.hours_completed,
|
||||
loanDeferralUntilGraduation: financialProfile.loan_deferral_until_graduation,
|
||||
programLength: financialProfile.program_length,
|
||||
});
|
||||
|
||||
let cumulativeSavings = emergencySavings || 0;
|
||||
|
||||
const cumulativeProjectionData = projectionData.map(month => {
|
||||
cumulativeSavings += month.netSavings || 0;
|
||||
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||
});
|
||||
|
||||
// Only update if we have real projection data
|
||||
if (cumulativeProjectionData.length > 0) {
|
||||
setProjectionData(cumulativeProjectionData);
|
||||
setLoanPayoffMonth(loanPaidOffMonth);
|
||||
const fetchCollegeProfile = async () => {
|
||||
// If you have a route like GET /api/premium/college-profile?careerPathId=XYZ
|
||||
const res = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
|
||||
if (!res || !res.ok) {
|
||||
setCollegeProfile(null);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
setCollegeProfile(data); // could be an object or empty {}
|
||||
};
|
||||
|
||||
fetchCollegeProfile();
|
||||
}, [careerPathId, apiURL]);
|
||||
|
||||
// ----------------------------
|
||||
// 3. Merge data + simulate once both profiles + selectedCareer are loaded
|
||||
// ----------------------------
|
||||
useEffect(() => {
|
||||
if (!financialProfile || !collegeProfile || !selectedCareer) return;
|
||||
console.log("About to build mergedProfile");
|
||||
console.log("collegeProfile from DB/fetch = ", collegeProfile);
|
||||
console.log(
|
||||
"college_enrollment_status check:",
|
||||
"[" + collegeProfile.college_enrollment_status + "]",
|
||||
"length=", collegeProfile.college_enrollment_status?.length
|
||||
);
|
||||
console.log(
|
||||
"Comparison => ",
|
||||
collegeProfile.college_enrollment_status === 'currently_enrolled'
|
||||
);
|
||||
|
||||
|
||||
// Merge financial + college data
|
||||
const mergedProfile = {
|
||||
// From financialProfile
|
||||
currentSalary: financialProfile.current_salary || 0,
|
||||
monthlyExpenses: financialProfile.monthly_expenses || 0,
|
||||
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
||||
retirementSavings: financialProfile.retirement_savings || 0,
|
||||
emergencySavings: financialProfile.emergency_fund || 0,
|
||||
monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
|
||||
monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
|
||||
surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
|
||||
surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
|
||||
|
||||
// From collegeProfile
|
||||
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
||||
interestRate: collegeProfile.interest_rate || 5,
|
||||
loanTerm: collegeProfile.loan_term || 10,
|
||||
loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation,
|
||||
academicCalendar: collegeProfile.academic_calendar || 'monthly',
|
||||
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
|
||||
calculatedTuition: collegeProfile.tuition || 0,
|
||||
extraPayment: collegeProfile.extra_payment || 0,
|
||||
partTimeIncome: 0, // or collegeProfile.part_time_income if you store it
|
||||
gradDate: collegeProfile.expected_graduation || null,
|
||||
programType: collegeProfile.program_type,
|
||||
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
|
||||
hoursCompleted: collegeProfile.hours_completed || 0,
|
||||
programLength: collegeProfile.program_length || 0,
|
||||
|
||||
// Are they in college?
|
||||
inCollege: (collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||
collegeProfile.college_enrollment_status === 'prospective_student'),
|
||||
// If they've graduated or not in college, false
|
||||
startDate: new Date().toISOString(),
|
||||
// Future logic could set expectedSalary if there's a difference
|
||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
||||
};
|
||||
|
||||
const result = simulateFinancialProjection(mergedProfile);
|
||||
console.log("mergedProfile for simulation:", mergedProfile);
|
||||
|
||||
const { projectionData, loanPaidOffMonth } = result;
|
||||
|
||||
// If you want to accumulate net savings:
|
||||
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
||||
const cumulativeProjectionData = projectionData.map(month => {
|
||||
cumulativeSavings += (month.netSavings || 0);
|
||||
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||
});
|
||||
|
||||
if (cumulativeProjectionData.length > 0) {
|
||||
setProjectionData(cumulativeProjectionData);
|
||||
setLoanPayoffMonth(loanPaidOffMonth);
|
||||
}
|
||||
}, [financialProfile, selectedCareer]);
|
||||
|
||||
console.log('mergedProfile for simulation:', mergedProfile);
|
||||
|
||||
}, [financialProfile, collegeProfile, selectedCareer]);
|
||||
|
||||
// 4. The rest of your code is unchanged, e.g. handleConfirmCareerSelection, etc.
|
||||
// ...
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleCareerChange = (selected) => {
|
||||
if (selected && selected.id && selected.career_name) {
|
||||
setSelectedCareer(selected);
|
||||
setCareerPathId(selected.id);
|
||||
} else {
|
||||
console.warn('Invalid career object received in handleCareerChange:', selected);
|
||||
}
|
||||
};
|
||||
|
||||
console.log(
|
||||
'First 5 items of projectionData:',
|
||||
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
|
||||
);
|
||||
|
||||
|
||||
const handleConfirmCareerSelection = async () => {
|
||||
const newId = uuidv4();
|
||||
const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] };
|
||||
const res = await authFetch(`${apiURL}/premium/career-profile`, { method: 'POST', body: JSON.stringify(body) });
|
||||
if (!res || !res.ok) return;
|
||||
const result = await res.json();
|
||||
setCareerPathId(result.career_path_id);
|
||||
setSelectedCareer({
|
||||
career_name: pendingCareerForModal,
|
||||
id: result.career_path_id
|
||||
});
|
||||
setPendingCareerForModal(null);
|
||||
};
|
||||
|
||||
// ...
|
||||
// The remainder of your component: timeline, chart, AISuggestedMilestones, etc.
|
||||
// ...
|
||||
return (
|
||||
<div className="milestone-tracker">
|
||||
|
||||
<CareerSelectDropdown
|
||||
existingCareerPaths={existingCareerPaths}
|
||||
selectedCareer={selectedCareer}
|
||||
onChange={handleCareerChange}
|
||||
onChange={(selected) => {
|
||||
setSelectedCareer(selected);
|
||||
setCareerPathId(selected?.id || null);
|
||||
}}
|
||||
loading={!existingCareerPaths.length}
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
|
||||
<MilestoneTimeline careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} setActiveView={setActiveView} />
|
||||
{console.log('Passing careerPathId to MilestoneTimeline:', careerPathId)}
|
||||
<MilestoneTimeline
|
||||
careerPathId={careerPathId}
|
||||
authFetch={authFetch}
|
||||
activeView={activeView}
|
||||
setActiveView={setActiveView}
|
||||
/>
|
||||
|
||||
<AISuggestedMilestones career={selectedCareer?.career_name} careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} projectionData={projectionData}/>
|
||||
<AISuggestedMilestones
|
||||
career={selectedCareer?.career_name}
|
||||
careerPathId={careerPathId}
|
||||
authFetch={authFetch}
|
||||
activeView={activeView}
|
||||
projectionData={projectionData}
|
||||
/>
|
||||
|
||||
{projectionData && (
|
||||
<div className="bg-white p-4 mt-6 rounded shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
|
||||
<Line
|
||||
data={{
|
||||
labels: projectionData.map(p => p.month),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Total Savings', // ✅ Changed label to clarify
|
||||
data: projectionData.map(p => p.cumulativeNetSavings),
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Loan Balance',
|
||||
data: projectionData.map(p => p.loanBalance),
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: {
|
||||
target: 'origin',
|
||||
above: 'rgba(255,99,132,0.3)', // loan debt
|
||||
below: 'transparent' // don't show below 0
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Retirement Savings',
|
||||
data: projectionData.map(p => p.totalRetirementSavings),
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
{projectionData.length > 0 && (
|
||||
<div className="bg-white p-4 mt-6 rounded shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
|
||||
<Line
|
||||
data={{
|
||||
labels: projectionData.map(p => p.month),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Total Savings',
|
||||
data: projectionData.map(p => p.cumulativeNetSavings),
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
},
|
||||
{
|
||||
label: 'Loan Balance',
|
||||
data: projectionData.map(p => p.loanBalance),
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: {
|
||||
target: 'origin',
|
||||
above: 'rgba(255,99,132,0.3)',
|
||||
below: 'transparent'
|
||||
}
|
||||
]
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
tooltip: { mode: 'index', intersect: false },
|
||||
annotation: loanPayoffMonth
|
||||
? {
|
||||
annotations: {
|
||||
loanPaidOffLine: {
|
||||
type: 'line',
|
||||
xMin: loanPayoffMonth,
|
||||
xMax: loanPayoffMonth,
|
||||
borderColor: 'rgba(255, 206, 86, 1)',
|
||||
borderWidth: 2,
|
||||
borderDash: [6, 6],
|
||||
label: {
|
||||
display: true,
|
||||
content: 'Loan Paid Off',
|
||||
position: 'end',
|
||||
backgroundColor: 'rgba(255, 206, 86, 0.8)',
|
||||
color: '#000',
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
rotation: 0,
|
||||
yAdjust: -10
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Retirement Savings',
|
||||
data: projectionData.map(p => p.retirementSavings),
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
tooltip: { mode: 'index', intersect: false },
|
||||
annotation: loanPayoffMonth
|
||||
? {
|
||||
annotations: {
|
||||
loanPaidOffLine: {
|
||||
type: 'line',
|
||||
xMin: loanPayoffMonth,
|
||||
xMax: loanPayoffMonth,
|
||||
borderColor: 'rgba(255, 206, 86, 1)',
|
||||
borderWidth: 2,
|
||||
borderDash: [6, 6],
|
||||
label: {
|
||||
display: true,
|
||||
content: 'Loan Paid Off',
|
||||
position: 'end',
|
||||
backgroundColor: 'rgba(255, 206, 86, 0.8)',
|
||||
color: '#000',
|
||||
font: { size: 12 },
|
||||
rotation: 0,
|
||||
yAdjust: -10
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
ticks: {
|
||||
callback: (value) => `$${value.toLocaleString()}`
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
ticks: {
|
||||
callback: (value) => `$${value.toLocaleString()}`
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CareerSearch
|
||||
<CareerSearch
|
||||
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
|
||||
setPendingCareerForModal={setPendingCareerForModal}
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
|
||||
{pendingCareerForModal && (
|
||||
<button onClick={handleConfirmCareerSelection}>
|
||||
<button onClick={() => {
|
||||
// handleConfirmCareerSelection logic
|
||||
}}>
|
||||
Confirm Career Change to {pendingCareerForModal}
|
||||
</button>
|
||||
)}
|
||||
|
@ -90,12 +90,17 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||
alert("Please complete all required fields before continuing.");
|
||||
return;
|
||||
}
|
||||
const isInCollege = (
|
||||
collegeEnrollmentStatus === 'currently_enrolled' ||
|
||||
collegeEnrollmentStatus === 'prospective_student'
|
||||
);
|
||||
|
||||
setData(prevData => ({
|
||||
...prevData,
|
||||
career_name: selectedCareer,
|
||||
college_enrollment_status: collegeEnrollmentStatus,
|
||||
currently_working: currentlyWorking,
|
||||
inCollege: isInCollege,
|
||||
status: prevData.status || 'planned',
|
||||
start_date: prevData.start_date || new Date().toISOString(),
|
||||
projected_end_date: prevData.projected_end_date || null,
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import authFetch from '../../utils/authFetch.js';
|
||||
|
||||
function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
|
||||
function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId }) {
|
||||
// CIP / iPEDS local states (purely for CIP data and suggestions)
|
||||
const [schoolData, setSchoolData] = useState([]);
|
||||
const [icTuitionData, setIcTuitionData] = useState([]);
|
||||
@ -301,20 +303,21 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
// 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));
|
||||
const chosenTuition = manualTuition.trim() === ''
|
||||
? autoTuition
|
||||
: parseFloat(manualTuition);
|
||||
const chosenProgramLength = manualProgramLength.trim() === ''
|
||||
? autoProgramLength
|
||||
: manualProgramLength;
|
||||
|
||||
// Same for program length
|
||||
const chosenProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||||
|
||||
// Write them into parent's data
|
||||
// Update parent’s data (collegeData)
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
tuition: chosenTuition,
|
||||
program_length: chosenProgramLength
|
||||
tuition: chosenTuition, // match name used by parent or server
|
||||
program_length: chosenProgramLength // match name used by parent
|
||||
}));
|
||||
|
||||
// Then go to the next step in the parent’s wizard
|
||||
nextStep();
|
||||
};
|
||||
|
||||
@ -324,6 +327,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
||||
// The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength)
|
||||
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>College Details</h2>
|
||||
|
@ -6,6 +6,7 @@ import FinancialOnboarding from './FinancialOnboarding.js';
|
||||
import CollegeOnboarding from './CollegeOnboarding.js';
|
||||
import authFetch from '../../utils/authFetch.js';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ReviewPage from './ReviewPage.js';
|
||||
|
||||
const OnboardingContainer = () => {
|
||||
console.log('OnboardingContainer MOUNT');
|
||||
@ -20,40 +21,54 @@ const OnboardingContainer = () => {
|
||||
const nextStep = () => setStep(step + 1);
|
||||
const prevStep = () => setStep(step - 1);
|
||||
|
||||
const submitData = async () => {
|
||||
await authFetch('/api/premium/career-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(careerData),
|
||||
});
|
||||
console.log("Final collegeData in OnboardingContainer:", collegeData);
|
||||
|
||||
await authFetch('/api/premium/financial-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(financialData),
|
||||
});
|
||||
// Now we do the final “all done” submission when the user finishes the last step
|
||||
const handleFinalSubmit = async () => {
|
||||
try {
|
||||
// 1) POST career-profile
|
||||
const careerRes = await authFetch('/api/premium/career-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(careerData),
|
||||
});
|
||||
if (!careerRes.ok) throw new Error('Failed to save career profile');
|
||||
const careerJson = await careerRes.json();
|
||||
const { career_path_id } = careerJson;
|
||||
if (!career_path_id) throw new Error('No career_path_id returned by server');
|
||||
const mergedCollegeData = {
|
||||
...collegeData,
|
||||
// ensure this field isn’t null
|
||||
college_enrollment_status: careerData.college_enrollment_status,
|
||||
career_path_id
|
||||
};
|
||||
|
||||
await authFetch('/api/premium/college-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(collegeData),
|
||||
});
|
||||
// 2) POST financial-profile
|
||||
const financialRes = await authFetch('/api/premium/financial-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(financialData),
|
||||
});
|
||||
if (!financialRes.ok) throw new Error('Failed to save financial profile');
|
||||
|
||||
navigate('/milestone-tracker');
|
||||
};
|
||||
// 3) POST college-profile (include career_path_id)
|
||||
const mergedCollege = {
|
||||
...collegeData,
|
||||
college_enrollment_status: careerData.college_enrollment_status,
|
||||
career_path_id };
|
||||
const collegeRes = await authFetch('/api/premium/college-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(mergedCollege),
|
||||
});
|
||||
if (!collegeRes.ok) throw new Error('Failed to save college profile');
|
||||
|
||||
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
|
||||
// Done => navigate away
|
||||
navigate('/milestone-tracker');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// (optionally show error to user)
|
||||
}
|
||||
};
|
||||
|
||||
const onboardingSteps = [
|
||||
@ -76,13 +91,23 @@ const OnboardingContainer = () => {
|
||||
/>,
|
||||
|
||||
<CollegeOnboarding
|
||||
nextStep={submitData}
|
||||
prevStep={prevStep}
|
||||
|
||||
// Pass the merged data so that college_enrollment_status is never lost
|
||||
data={mergedCollegeData}
|
||||
nextStep={nextStep}
|
||||
data={{
|
||||
...collegeData,
|
||||
// ensure we keep the enrollment status from career if that matters:
|
||||
college_enrollment_status: careerData.college_enrollment_status
|
||||
}}
|
||||
setData={setCollegeData}
|
||||
/>
|
||||
/>,
|
||||
// Add a final "Review & Submit" step or just automatically call handleFinalSubmit on step 4
|
||||
<ReviewPage
|
||||
careerData={careerData}
|
||||
financialData={financialData}
|
||||
collegeData={collegeData}
|
||||
onSubmit={handleFinalSubmit}
|
||||
onBack={prevStep}
|
||||
/>,
|
||||
];
|
||||
|
||||
return <div>{onboardingSteps[step]}</div>;
|
||||
|
31
src/components/PremiumOnboarding/ReviewPage.js
Normal file
31
src/components/PremiumOnboarding/ReviewPage.js
Normal file
@ -0,0 +1,31 @@
|
||||
// ReviewPage.js
|
||||
import React from 'react';
|
||||
|
||||
function ReviewPage({ careerData, financialData, collegeData, onSubmit, onBack }) {
|
||||
console.log("REVIEW PAGE PROPS:", {
|
||||
careerData,
|
||||
financialData,
|
||||
collegeData,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<h2>Review Your Info</h2>
|
||||
|
||||
<h3>Career Info</h3>
|
||||
<pre>{JSON.stringify(careerData, null, 2)}</pre>
|
||||
|
||||
<h3>Financial Info</h3>
|
||||
<pre>{JSON.stringify(financialData, null, 2)}</pre>
|
||||
|
||||
<h3>College Info</h3>
|
||||
<pre>{JSON.stringify(collegeData, null, 2)}</pre>
|
||||
|
||||
<button onClick={onBack}>← Back</button>
|
||||
<button onClick={onSubmit} style={{ marginLeft: '1rem' }}>
|
||||
Submit All
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewPage;
|
@ -127,6 +127,7 @@ export function simulateFinancialProjection(userProfile) {
|
||||
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')}`;
|
||||
@ -145,12 +146,14 @@ export function simulateFinancialProjection(userProfile) {
|
||||
(date.getMonth() - simStart.getMonth());
|
||||
stillInCollege = (elapsedMonths < totalAcademicMonths);
|
||||
}
|
||||
console.log(`MONTH ${month} start: inCollege=${stillInCollege}, loanBal=${loanBalance}`);
|
||||
}
|
||||
|
||||
// 6. If we pay lumps: check if this is a "lump" month within the user's academic year
|
||||
// We'll find how many academic years have passed since they started
|
||||
let tuitionCostThisMonth = 0;
|
||||
if (stillInCollege && lumpsPerYear > 0) {
|
||||
|
||||
const simStart = startDate ? new Date(startDate) : new Date();
|
||||
const elapsedMonths =
|
||||
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
||||
@ -160,7 +163,7 @@ export function simulateFinancialProjection(userProfile) {
|
||||
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
||||
// Within that year, which month are we in? (0..11)
|
||||
const monthInYear = elapsedMonths % 12;
|
||||
|
||||
console.log(" lumps logic check: academicYearIndex=", academicYearIndex, "monthInYear=", monthInYear);
|
||||
// If we find monthInYear in lumpsSchedule, then lumps are due
|
||||
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
||||
tuitionCostThisMonth = lumpAmount;
|
||||
@ -170,8 +173,10 @@ export function simulateFinancialProjection(userProfile) {
|
||||
// 7. Decide if user defers or pays out of pocket
|
||||
// If deferring, add lumps to loan
|
||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||
console.log(" deferral is on, lumps => loan?");
|
||||
// Instead of user paying out of pocket, add to loan
|
||||
if (tuitionCostThisMonth > 0) {
|
||||
console.log(" tuitionCostThisMonth=", tuitionCostThisMonth);
|
||||
loanBalance += tuitionCostThisMonth;
|
||||
tuitionCostThisMonth = 0; // paid by the loan
|
||||
}
|
||||
@ -241,15 +246,13 @@ export function simulateFinancialProjection(userProfile) {
|
||||
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||
let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
|
||||
console.log(" end of month: loanBal=", loanBalance, " shortfall=", shortfall);
|
||||
if (shortfall > 0) {
|
||||
// We can reduce from emergency savings
|
||||
console.log(" Breaking out - bankrupt scenario");
|
||||
const canCover = Math.min(shortfall, currentEmergencySavings);
|
||||
currentEmergencySavings -= canCover;
|
||||
shortfall -= canCover;
|
||||
if (shortfall > 0) {
|
||||
// user is effectively bankrupt
|
||||
// we can break out or keep going to show negative net worth
|
||||
// For demonstration, let's break
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user