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
@ -585,8 +585,12 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
|
|||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const id = uuidv4();
|
|
||||||
const user_id = req.userId;
|
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(`
|
await db.run(`
|
||||||
INSERT INTO college_profiles (
|
INSERT INTO college_profiles (
|
||||||
id,
|
id,
|
||||||
@ -616,66 +620,95 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
|
|||||||
tuition_paid,
|
tuition_paid,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
)
|
||||||
?, -- id
|
VALUES (
|
||||||
?, -- user_id
|
:id,
|
||||||
?, -- career_path_id
|
:user_id,
|
||||||
?, -- selected_school
|
:career_path_id,
|
||||||
?, -- selected_program
|
:selected_school,
|
||||||
?, -- program_type
|
:selected_program,
|
||||||
?, -- is_in_state
|
:program_type,
|
||||||
?, -- is_in_district
|
:is_in_state,
|
||||||
?, -- college_enrollment_status
|
:is_in_district,
|
||||||
?, -- annual_financial_aid
|
:college_enrollment_status,
|
||||||
?, -- is_online
|
:annual_financial_aid,
|
||||||
?, -- credit_hours_per_year
|
:is_online,
|
||||||
?, -- hours_completed
|
:credit_hours_per_year,
|
||||||
?, -- program_length
|
:hours_completed,
|
||||||
?, -- credit_hours_required
|
:program_length,
|
||||||
?, -- expected_graduation
|
:credit_hours_required,
|
||||||
?, -- existing_college_debt
|
:expected_graduation,
|
||||||
?, -- interest_rate
|
:existing_college_debt,
|
||||||
?, -- loan_term
|
:interest_rate,
|
||||||
?, -- loan_deferral_until_graduation
|
:loan_term,
|
||||||
?, -- extra_payment
|
:loan_deferral_until_graduation,
|
||||||
?, -- expected_salary
|
:extra_payment,
|
||||||
?, -- academic_calendar
|
:expected_salary,
|
||||||
?, -- tuition
|
:academic_calendar,
|
||||||
?, -- tuition_paid
|
:tuition,
|
||||||
|
:tuition_paid,
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`, [
|
|
||||||
id,
|
-- The magic:
|
||||||
user_id,
|
ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type)
|
||||||
career_path_id,
|
DO UPDATE SET
|
||||||
selected_school,
|
is_in_state = excluded.is_in_state,
|
||||||
selected_program,
|
is_in_district = excluded.is_in_district,
|
||||||
program_type || null,
|
college_enrollment_status = excluded.college_enrollment_status,
|
||||||
is_in_state ? 1 : 0,
|
annual_financial_aid = excluded.annual_financial_aid,
|
||||||
is_in_district ? 1 : 0,
|
is_online = excluded.is_online,
|
||||||
college_enrollment_status || null,
|
credit_hours_per_year = excluded.credit_hours_per_year,
|
||||||
annual_financial_aid || 0,
|
hours_completed = excluded.hours_completed,
|
||||||
is_online ? 1 : 0,
|
program_length = excluded.program_length,
|
||||||
credit_hours_per_year || 0,
|
credit_hours_required = excluded.credit_hours_required,
|
||||||
hours_completed || 0,
|
expected_graduation = excluded.expected_graduation,
|
||||||
program_length || 0,
|
existing_college_debt = excluded.existing_college_debt,
|
||||||
credit_hours_required || 0,
|
interest_rate = excluded.interest_rate,
|
||||||
expected_graduation || null,
|
loan_term = excluded.loan_term,
|
||||||
existing_college_debt || 0,
|
loan_deferral_until_graduation = excluded.loan_deferral_until_graduation,
|
||||||
interest_rate || 0,
|
extra_payment = excluded.extra_payment,
|
||||||
loan_term || 10,
|
expected_salary = excluded.expected_salary,
|
||||||
loan_deferral_until_graduation ? 1 : 0,
|
academic_calendar = excluded.academic_calendar,
|
||||||
extra_payment || 0,
|
tuition = excluded.tuition,
|
||||||
expected_salary || 0,
|
tuition_paid = excluded.tuition_paid,
|
||||||
academic_calendar || 'semester',
|
updated_at = CURRENT_TIMESTAMP
|
||||||
tuition || 0,
|
;
|
||||||
tuition_paid || 0
|
`, {
|
||||||
]);
|
':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({
|
res.status(201).json({
|
||||||
message: 'College profile saved.',
|
message: 'College profile upsert done.',
|
||||||
collegeProfileId: id
|
// You might do an extra SELECT here to find which ID the final row uses if you need it
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving college profile:', error);
|
console.error('Error saving college profile:', error);
|
||||||
@ -683,6 +716,21 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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 || {});
|
||||||
|
});
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
FINANCIAL PROJECTIONS
|
FINANCIAL PROJECTIONS
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
@ -12,13 +12,11 @@ import CareerSearch from './CareerSearch.js';
|
|||||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
import MilestoneTimeline from './MilestoneTimeline.js';
|
||||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||||
import './MilestoneTracker.css';
|
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';
|
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 MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -28,24 +26,29 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
||||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
const [activeView, setActiveView] = useState("Career");
|
const [activeView, setActiveView] = useState("Career");
|
||||||
const [financialProfile, setFinancialProfile] = useState(null); // Store the financial profile
|
|
||||||
const {
|
// Store each profile separately
|
||||||
projectionData: initialProjectionData = [],
|
const [financialProfile, setFinancialProfile] = useState(null);
|
||||||
loanPayoffMonth: initialLoanPayoffMonth = null,
|
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||||
} = location.state || {};
|
|
||||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(initialLoanPayoffMonth);
|
// For the chart
|
||||||
const [projectionData, setProjectionData] = useState(initialProjectionData);
|
const [projectionData, setProjectionData] = useState([]);
|
||||||
|
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
||||||
|
|
||||||
const apiURL = process.env.REACT_APP_API_URL;
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchCareerPaths = async () => {
|
const fetchCareerPaths = async () => {
|
||||||
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
||||||
if (!res) return;
|
if (!res || !res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const { careerPaths } = data;
|
setExistingCareerPaths(data.careerPaths);
|
||||||
setExistingCareerPaths(careerPaths);
|
|
||||||
|
|
||||||
const fromPopout = location.state?.selectedCareer;
|
const fromPopout = location.state?.selectedCareer;
|
||||||
if (fromPopout) {
|
if (fromPopout) {
|
||||||
@ -53,7 +56,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
setCareerPathId(fromPopout.career_path_id);
|
setCareerPathId(fromPopout.career_path_id);
|
||||||
} else if (!selectedCareer) {
|
} else if (!selectedCareer) {
|
||||||
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
||||||
if (latest) {
|
if (latest && latest.ok) {
|
||||||
const latestData = await latest.json();
|
const latestData = await latest.json();
|
||||||
if (latestData?.id) {
|
if (latestData?.id) {
|
||||||
setSelectedCareer(latestData);
|
setSelectedCareer(latestData);
|
||||||
@ -67,110 +70,156 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
const res = await authFetch(`${apiURL}/premium/financial-profile`);
|
const res = await authFetch(`${apiURL}/premium/financial-profile`);
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setFinancialProfile(data); // Set the financial profile in state
|
setFinancialProfile(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCareerPaths();
|
fetchCareerPaths();
|
||||||
fetchFinancialProfile();
|
fetchFinancialProfile();
|
||||||
}, []);
|
}, [apiURL, location.state, selectedCareer]);
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// 2. Fetch the college profile for the selected careerPathId
|
||||||
|
// ----------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (financialProfile && selectedCareer) {
|
if (!careerPathId) {
|
||||||
const { projectionData, loanPaidOffMonth, emergencySavings } = simulateFinancialProjection({
|
setCollegeProfile(null);
|
||||||
currentSalary: financialProfile.current_salary,
|
return;
|
||||||
monthlyExpenses: financialProfile.monthly_expenses,
|
}
|
||||||
|
|
||||||
|
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,
|
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
||||||
studentLoanAmount: financialProfile.college_loan_total,
|
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,
|
||||||
|
|
||||||
interestRate: financialProfile.interest_rate || 5.5,
|
// From collegeProfile
|
||||||
loanTerm: financialProfile.loan_term || 10,
|
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
||||||
extraPayment: financialProfile.extra_payment || 0,
|
interestRate: collegeProfile.interest_rate || 5,
|
||||||
expectedSalary: financialProfile.expected_salary || financialProfile.current_salary,
|
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,
|
||||||
|
|
||||||
emergencySavings: financialProfile.emergency_fund,
|
// Are they in college?
|
||||||
retirementSavings: financialProfile.retirement_savings,
|
inCollege: (collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||||
monthlyRetirementContribution: financialProfile.retirement_contribution,
|
collegeProfile.college_enrollment_status === 'prospective_student'),
|
||||||
monthlyEmergencyContribution: 0,
|
// If they've graduated or not in college, false
|
||||||
gradDate: financialProfile.expected_graduation,
|
startDate: new Date().toISOString(),
|
||||||
fullTimeCollegeStudent: financialProfile.in_college,
|
// Future logic could set expectedSalary if there's a difference
|
||||||
partTimeIncome: financialProfile.part_time_income,
|
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
||||||
startDate: new Date(),
|
};
|
||||||
|
|
||||||
programType: financialProfile.program_type,
|
const result = simulateFinancialProjection(mergedProfile);
|
||||||
isFullyOnline: financialProfile.is_online,
|
console.log("mergedProfile for simulation:", mergedProfile);
|
||||||
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 { projectionData, loanPaidOffMonth } = result;
|
||||||
|
|
||||||
|
// If you want to accumulate net savings:
|
||||||
|
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
||||||
const cumulativeProjectionData = projectionData.map(month => {
|
const cumulativeProjectionData = projectionData.map(month => {
|
||||||
cumulativeSavings += month.netSavings || 0;
|
cumulativeSavings += (month.netSavings || 0);
|
||||||
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only update if we have real projection data
|
|
||||||
if (cumulativeProjectionData.length > 0) {
|
if (cumulativeProjectionData.length > 0) {
|
||||||
setProjectionData(cumulativeProjectionData);
|
setProjectionData(cumulativeProjectionData);
|
||||||
setLoanPayoffMonth(loanPaidOffMonth);
|
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(
|
console.log(
|
||||||
'First 5 items of projectionData:',
|
'First 5 items of projectionData:',
|
||||||
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
|
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ...
|
||||||
const handleConfirmCareerSelection = async () => {
|
// The remainder of your component: timeline, chart, AISuggestedMilestones, etc.
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-tracker">
|
<div className="milestone-tracker">
|
||||||
|
|
||||||
<CareerSelectDropdown
|
<CareerSelectDropdown
|
||||||
existingCareerPaths={existingCareerPaths}
|
existingCareerPaths={existingCareerPaths}
|
||||||
selectedCareer={selectedCareer}
|
selectedCareer={selectedCareer}
|
||||||
onChange={handleCareerChange}
|
onChange={(selected) => {
|
||||||
|
setSelectedCareer(selected);
|
||||||
|
setCareerPathId(selected?.id || null);
|
||||||
|
}}
|
||||||
loading={!existingCareerPaths.length}
|
loading={!existingCareerPaths.length}
|
||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MilestoneTimeline careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} setActiveView={setActiveView} />
|
<MilestoneTimeline
|
||||||
{console.log('Passing careerPathId to MilestoneTimeline:', careerPathId)}
|
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 && (
|
{projectionData.length > 0 && (
|
||||||
<div className="bg-white p-4 mt-6 rounded shadow">
|
<div className="bg-white p-4 mt-6 rounded shadow">
|
||||||
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
|
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
|
||||||
<Line
|
<Line
|
||||||
@ -178,12 +227,12 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
labels: projectionData.map(p => p.month),
|
labels: projectionData.map(p => p.month),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Total Savings', // ✅ Changed label to clarify
|
label: 'Total Savings',
|
||||||
data: projectionData.map(p => p.cumulativeNetSavings),
|
data: projectionData.map(p => p.cumulativeNetSavings),
|
||||||
borderColor: 'rgba(54, 162, 235, 1)',
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: true
|
fill: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Loan Balance',
|
label: 'Loan Balance',
|
||||||
@ -193,13 +242,13 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: {
|
fill: {
|
||||||
target: 'origin',
|
target: 'origin',
|
||||||
above: 'rgba(255,99,132,0.3)', // loan debt
|
above: 'rgba(255,99,132,0.3)',
|
||||||
below: 'transparent' // don't show below 0
|
below: 'transparent'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Retirement Savings',
|
label: 'Retirement Savings',
|
||||||
data: projectionData.map(p => p.totalRetirementSavings),
|
data: projectionData.map(p => p.retirementSavings),
|
||||||
borderColor: 'rgba(75, 192, 192, 1)',
|
borderColor: 'rgba(75, 192, 192, 1)',
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
@ -228,9 +277,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
position: 'end',
|
position: 'end',
|
||||||
backgroundColor: 'rgba(255, 206, 86, 0.8)',
|
backgroundColor: 'rgba(255, 206, 86, 0.8)',
|
||||||
color: '#000',
|
color: '#000',
|
||||||
font: {
|
font: { size: 12 },
|
||||||
size: 12
|
|
||||||
},
|
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
yAdjust: -10
|
yAdjust: -10
|
||||||
}
|
}
|
||||||
@ -259,7 +306,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{pendingCareerForModal && (
|
{pendingCareerForModal && (
|
||||||
<button onClick={handleConfirmCareerSelection}>
|
<button onClick={() => {
|
||||||
|
// handleConfirmCareerSelection logic
|
||||||
|
}}>
|
||||||
Confirm Career Change to {pendingCareerForModal}
|
Confirm Career Change to {pendingCareerForModal}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -90,12 +90,17 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
alert("Please complete all required fields before continuing.");
|
alert("Please complete all required fields before continuing.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const isInCollege = (
|
||||||
|
collegeEnrollmentStatus === 'currently_enrolled' ||
|
||||||
|
collegeEnrollmentStatus === 'prospective_student'
|
||||||
|
);
|
||||||
|
|
||||||
setData(prevData => ({
|
setData(prevData => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
career_name: selectedCareer,
|
career_name: selectedCareer,
|
||||||
college_enrollment_status: collegeEnrollmentStatus,
|
college_enrollment_status: collegeEnrollmentStatus,
|
||||||
currently_working: currentlyWorking,
|
currently_working: currentlyWorking,
|
||||||
|
inCollege: isInCollege,
|
||||||
status: prevData.status || 'planned',
|
status: prevData.status || 'planned',
|
||||||
start_date: prevData.start_date || new Date().toISOString(),
|
start_date: prevData.start_date || new Date().toISOString(),
|
||||||
projected_end_date: prevData.projected_end_date || null,
|
projected_end_date: prevData.projected_end_date || null,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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)
|
// CIP / iPEDS local states (purely for CIP data and suggestions)
|
||||||
const [schoolData, setSchoolData] = useState([]);
|
const [schoolData, setSchoolData] = useState([]);
|
||||||
const [icTuitionData, setIcTuitionData] = useState([]);
|
const [icTuitionData, setIcTuitionData] = useState([]);
|
||||||
@ -301,20 +303,21 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
// handleSubmit => merges final chosen values
|
// handleSubmit => merges final chosen values
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
// If user typed a manual value, we use that. If they left it blank,
|
const chosenTuition = manualTuition.trim() === ''
|
||||||
// we use the autoTuition.
|
? autoTuition
|
||||||
const chosenTuition = (manualTuition.trim() === '' ? autoTuition : parseFloat(manualTuition));
|
: parseFloat(manualTuition);
|
||||||
|
const chosenProgramLength = manualProgramLength.trim() === ''
|
||||||
|
? autoProgramLength
|
||||||
|
: manualProgramLength;
|
||||||
|
|
||||||
// Same for program length
|
// Update parent’s data (collegeData)
|
||||||
const chosenProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
|
||||||
|
|
||||||
// Write them into parent's data
|
|
||||||
setData(prev => ({
|
setData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
tuition: chosenTuition,
|
tuition: chosenTuition, // match name used by parent or server
|
||||||
program_length: chosenProgramLength
|
program_length: chosenProgramLength // match name used by parent
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Then go to the next step in the parent’s wizard
|
||||||
nextStep();
|
nextStep();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -324,6 +327,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
|
|||||||
// The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength)
|
// The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength)
|
||||||
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>College Details</h2>
|
<h2>College Details</h2>
|
||||||
|
@ -6,6 +6,7 @@ import FinancialOnboarding from './FinancialOnboarding.js';
|
|||||||
import CollegeOnboarding from './CollegeOnboarding.js';
|
import CollegeOnboarding from './CollegeOnboarding.js';
|
||||||
import authFetch from '../../utils/authFetch.js';
|
import authFetch from '../../utils/authFetch.js';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import ReviewPage from './ReviewPage.js';
|
||||||
|
|
||||||
const OnboardingContainer = () => {
|
const OnboardingContainer = () => {
|
||||||
console.log('OnboardingContainer MOUNT');
|
console.log('OnboardingContainer MOUNT');
|
||||||
@ -20,40 +21,54 @@ const OnboardingContainer = () => {
|
|||||||
const nextStep = () => setStep(step + 1);
|
const nextStep = () => setStep(step + 1);
|
||||||
const prevStep = () => setStep(step - 1);
|
const prevStep = () => setStep(step - 1);
|
||||||
|
|
||||||
const submitData = async () => {
|
console.log("Final collegeData in OnboardingContainer:", collegeData);
|
||||||
await authFetch('/api/premium/career-profile', {
|
|
||||||
|
// 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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(careerData),
|
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/financial-profile', {
|
// 2) POST financial-profile
|
||||||
|
const financialRes = await authFetch('/api/premium/financial-profile', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(financialData),
|
body: JSON.stringify(financialData),
|
||||||
});
|
});
|
||||||
|
if (!financialRes.ok) throw new Error('Failed to save financial profile');
|
||||||
|
|
||||||
await authFetch('/api/premium/college-profile', {
|
// 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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(collegeData),
|
body: JSON.stringify(mergedCollege),
|
||||||
});
|
});
|
||||||
|
if (!collegeRes.ok) throw new Error('Failed to save college profile');
|
||||||
|
|
||||||
|
// Done => navigate away
|
||||||
navigate('/milestone-tracker');
|
navigate('/milestone-tracker');
|
||||||
};
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
console.log('collegeData to submit:', collegeData);
|
// (optionally show error to user)
|
||||||
|
}
|
||||||
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 = [
|
||||||
@ -76,13 +91,23 @@ const OnboardingContainer = () => {
|
|||||||
/>,
|
/>,
|
||||||
|
|
||||||
<CollegeOnboarding
|
<CollegeOnboarding
|
||||||
nextStep={submitData}
|
|
||||||
prevStep={prevStep}
|
prevStep={prevStep}
|
||||||
|
nextStep={nextStep}
|
||||||
// Pass the merged data so that college_enrollment_status is never lost
|
data={{
|
||||||
data={mergedCollegeData}
|
...collegeData,
|
||||||
|
// ensure we keep the enrollment status from career if that matters:
|
||||||
|
college_enrollment_status: careerData.college_enrollment_status
|
||||||
|
}}
|
||||||
setData={setCollegeData}
|
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>;
|
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++) {
|
for (let month = 0; month < maxMonths; month++) {
|
||||||
date.setMonth(date.getMonth() + 1);
|
date.setMonth(date.getMonth() + 1);
|
||||||
|
|
||||||
|
|
||||||
// If loan is fully paid, record if not done already
|
// If loan is fully paid, record if not done already
|
||||||
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
||||||
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
@ -145,12 +146,14 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
(date.getMonth() - simStart.getMonth());
|
(date.getMonth() - simStart.getMonth());
|
||||||
stillInCollege = (elapsedMonths < totalAcademicMonths);
|
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
|
// 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
|
// We'll find how many academic years have passed since they started
|
||||||
let tuitionCostThisMonth = 0;
|
let tuitionCostThisMonth = 0;
|
||||||
if (stillInCollege && lumpsPerYear > 0) {
|
if (stillInCollege && lumpsPerYear > 0) {
|
||||||
|
|
||||||
const simStart = startDate ? new Date(startDate) : new Date();
|
const simStart = startDate ? new Date(startDate) : new Date();
|
||||||
const elapsedMonths =
|
const elapsedMonths =
|
||||||
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
||||||
@ -160,7 +163,7 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
||||||
// Within that year, which month are we in? (0..11)
|
// Within that year, which month are we in? (0..11)
|
||||||
const monthInYear = elapsedMonths % 12;
|
const monthInYear = elapsedMonths % 12;
|
||||||
|
console.log(" lumps logic check: academicYearIndex=", academicYearIndex, "monthInYear=", monthInYear);
|
||||||
// If we find monthInYear in lumpsSchedule, then lumps are due
|
// If we find monthInYear in lumpsSchedule, then lumps are due
|
||||||
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
||||||
tuitionCostThisMonth = lumpAmount;
|
tuitionCostThisMonth = lumpAmount;
|
||||||
@ -170,8 +173,10 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
// 7. Decide if user defers or pays out of pocket
|
// 7. Decide if user defers or pays out of pocket
|
||||||
// If deferring, add lumps to loan
|
// If deferring, add lumps to loan
|
||||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||||
|
console.log(" deferral is on, lumps => loan?");
|
||||||
// Instead of user paying out of pocket, add to loan
|
// Instead of user paying out of pocket, add to loan
|
||||||
if (tuitionCostThisMonth > 0) {
|
if (tuitionCostThisMonth > 0) {
|
||||||
|
console.log(" tuitionCostThisMonth=", tuitionCostThisMonth);
|
||||||
loanBalance += tuitionCostThisMonth;
|
loanBalance += tuitionCostThisMonth;
|
||||||
tuitionCostThisMonth = 0; // paid by the loan
|
tuitionCostThisMonth = 0; // paid by the loan
|
||||||
}
|
}
|
||||||
@ -241,15 +246,13 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||||
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||||
let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
|
let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
|
||||||
|
console.log(" end of month: loanBal=", loanBalance, " shortfall=", shortfall);
|
||||||
if (shortfall > 0) {
|
if (shortfall > 0) {
|
||||||
// We can reduce from emergency savings
|
console.log(" Breaking out - bankrupt scenario");
|
||||||
const canCover = Math.min(shortfall, currentEmergencySavings);
|
const canCover = Math.min(shortfall, currentEmergencySavings);
|
||||||
currentEmergencySavings -= canCover;
|
currentEmergencySavings -= canCover;
|
||||||
shortfall -= canCover;
|
shortfall -= canCover;
|
||||||
if (shortfall > 0) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user