diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js
index 820adf4..f82c642 100644
--- a/src/components/PremiumOnboarding/CollegeOnboarding.js
+++ b/src/components/PremiumOnboarding/CollegeOnboarding.js
@@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react';
import Modal from '../../components/ui/modal.js';
import FinancialAidWizard from '../../components/FinancialAidWizard.js';
-function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId }) {
- // CIP / iPEDS local states (purely for CIP data and suggestions)
+function CollegeOnboarding({ nextStep, prevStep, data, setData }) {
+ // CIP / iPEDS local states
const [schoolData, setSchoolData] = useState([]);
const [icTuitionData, setIcTuitionData] = useState([]);
const [schoolSuggestions, setSchoolSuggestions] = useState([]);
@@ -13,13 +13,22 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
// Show/hide the financial aid wizard
const [showAidWizard, setShowAidWizard] = useState(false);
+ const infoIcon = (msg) => (
+
+ i
+
+ );
+
// Destructure parent data
const {
college_enrollment_status = '',
selected_school = '',
selected_program = '',
program_type = '',
- academic_calendar = 'semester', // <-- ACADEMIC CALENDAR
+ academic_calendar = 'semester',
annual_financial_aid = '',
is_online = false,
existing_college_debt = '',
@@ -41,26 +50,49 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
// Local states for auto/manual logic on tuition & program length
const [manualTuition, setManualTuition] = useState('');
const [autoTuition, setAutoTuition] = useState(0);
-
const [manualProgramLength, setManualProgramLength] = useState('');
const [autoProgramLength, setAutoProgramLength] = useState('0.00');
- // -- universal handleChange for all parent fields except tuition/program_length
+ /**
+ * handleParentFieldChange
+ * If user leaves numeric fields blank, store '' in local state, not 0.
+ * Only parseFloat if there's an actual numeric value.
+ */
const handleParentFieldChange = (e) => {
const { name, value, type, checked } = e.target;
let val = value;
+
if (type === 'checkbox') {
- val = checked;
+ val = checked;
+ setData(prev => ({ ...prev, [name]: val }));
+ return;
}
- 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);
+
+ // If the user typed an empty string, store '' so they can see it's blank
+ if (val.trim() === '') {
+ setData(prev => ({ ...prev, [name]: '' }));
+ return;
+ }
+
+ // Otherwise, parse it if it's one of the numeric fields
+ if (['interest_rate', 'loan_term', 'extra_payment', 'expected_salary'].includes(name)) {
+ const parsed = parseFloat(val);
+ // If parse fails => store '' (or fallback to old value)
+ if (isNaN(parsed)) {
+ setData(prev => ({ ...prev, [name]: '' }));
+ } else {
+ setData(prev => ({ ...prev, [name]: parsed }));
+ }
+ } else if ([
+ 'annual_financial_aid','existing_college_debt','credit_hours_per_year',
+ 'hours_completed','credit_hours_required','tuition_paid'
+ ].includes(name)) {
+ const parsed = parseFloat(val);
+ setData(prev => ({ ...prev, [name]: isNaN(parsed) ? '' : parsed }));
+ } else {
+ // For non-numeric or strings
+ setData(prev => ({ ...prev, [name]: val }));
}
- setData(prev => ({ ...prev, [name]: val }));
};
const handleManualTuitionChange = (e) => {
@@ -71,7 +103,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
setManualProgramLength(e.target.value);
};
- // Fetch CIP data (example)
+ // CIP data
useEffect(() => {
async function fetchCipData() {
try {
@@ -89,7 +121,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
fetchCipData();
}, []);
- // Fetch iPEDS data (example)
+ // iPEDS data
useEffect(() => {
async function fetchIpedsData() {
try {
@@ -108,7 +140,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
fetchIpedsData();
}, []);
- // Handle school name input
+ // School Name
const handleSchoolChange = (e) => {
const value = e.target.value;
setData(prev => ({
@@ -140,6 +172,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
setAvailableProgramTypes([]);
};
+ // Program
const handleProgramChange = (e) => {
const value = e.target.value;
setData(prev => ({ ...prev, selected_program: value }));
@@ -171,7 +204,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
setAutoProgramLength('0.00');
};
- // once we have school + program, load possible program types
+ // once we have school+program => load possible program types
useEffect(() => {
if (!selected_program || !selected_school || !schoolData.length) return;
const possibleTypes = schoolData
@@ -249,30 +282,59 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
// auto-calc program length
useEffect(() => {
- if (!program_type) return;
- if (!hours_completed || !credit_hours_per_year) return;
+ // If user hasn't selected a program type or credit_hours_per_year is missing, skip
+ if (!program_type) return;
+ if (!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;
- }
+ // If hours_completed is blank, treat as 0
+ const completed = parseInt(hours_completed, 10) || 0;
+ const perYear = parseFloat(credit_hours_per_year) || 1;
- const remain = required - (parseInt(hours_completed, 10) || 0);
- const yrs = remain / (parseFloat(credit_hours_per_year) || 1);
- const calcLength = yrs.toFixed(2);
+ let required = 0;
+ switch (program_type) {
+ case "Associate's Degree":
+ required = 60; // total for an associate's
+ break;
+ case "Bachelor's Degree":
+ required = 120; // total for a bachelor's
+ break;
+ case "Master's Degree":
+ required = 180; // e.g. 120 undergrad + 60 grad
+ break;
+ case "Doctoral Degree":
+ required = 240; // e.g. 120 undergrad + 120 grad
+ break;
+ case "First Professional Degree":
+ // If you want 180 or 240, up to you
+ required = 180;
+ break;
+ case "Graduate/Professional Certificate":
+ // Possibly read from credit_hours_required
+ required = parseInt(credit_hours_required, 10) || 0;
+ break;
+ default:
+ // For any other program type, use whatever is in credit_hours_required
+ required = parseInt(credit_hours_required, 10) || 0;
+ break;
+ }
- setAutoProgramLength(calcLength);
- }, [program_type, hours_completed, credit_hours_per_year, credit_hours_required]);
+ // Subtract however many credits they've already completed (that count)
+ const remain = required - completed;
+ const yrs = remain / perYear;
+ const calcLength = yrs.toFixed(2);
- // final handleSubmit
+ setAutoProgramLength(calcLength);
+}, [
+ program_type,
+ hours_completed,
+ credit_hours_per_year,
+ credit_hours_required
+]);
+
+
+
+
+ // final handleSubmit => we store chosen tuition + program_length, then move on
const handleSubmit = () => {
const chosenTuition = manualTuition.trim() === ''
? autoTuition
@@ -310,7 +372,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
onChange={handleParentFieldChange}
className="h-4 w-4"
/>
-
+
@@ -321,7 +383,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
onChange={handleParentFieldChange}
className="h-4 w-4"
/>
-
+
@@ -332,7 +394,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
onChange={handleParentFieldChange}
className="h-4 w-4"
/>
-
+
@@ -343,12 +405,12 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
onChange={handleParentFieldChange}
className="h-4 w-4"
/>
-
+
- {/* School / Program */}
+ {/* School */}
-
+
-
+
-
+
- {/* Academic Calendar (just re-added) */}
+ {/* Academic Calendar */}
-
+
- {/* If Grad/Professional or other that needs credit_hours_required */}
+ {/* If Grad/Professional => credit_hours_required */}
{(program_type === 'Graduate/Professional Certificate' ||
program_type === 'First Professional Degree' ||
program_type === 'Doctoral Degree') && (
-
+
)}
-
+
- {/* Annual Financial Aid with "Need Help?" Wizard button */}
+ {/* Annual Financial Aid */}
-
+
setShowAidWizard(true)}
- className="bg-blue-600 text-center px-3 py-2 rounded"
+ className="bg-blue-600 text-center px-3 py-2 rounded text-white"
>
Need Help?
@@ -484,7 +546,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
-
+
- {college_enrollment_status === 'prospective_student' && (
+ {/* Show Program Length for both "currently_enrolled" & "prospective_student" */}
+ {(college_enrollment_status === 'currently_enrolled' ||
+ college_enrollment_status === 'prospective_student') && (
-
+
)}
- {/* If "currently_enrolled" show Hours Completed + Program Length */}
+ {/* If currently_enrolled => hours_completed */}
{college_enrollment_status === 'currently_enrolled' && (
- <>
-
-
-
-
-
-
-
-
-
- >
+
+
+
+
)}
-
+
-
+
-
+
-
+
-
+
- {/* RENDER THE MODAL WITH FINANCIAL AID WIZARD IF showAidWizard === true */}
{showAidWizard && (
setShowAidWizard(false)}>
{
- // Update the annual_financial_aid with the wizard's result
setData(prev => ({
...prev,
annual_financial_aid: estimate
diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js
index 4e5660a..72b5ec0 100644
--- a/src/components/PremiumOnboarding/OnboardingContainer.js
+++ b/src/components/PremiumOnboarding/OnboardingContainer.js
@@ -15,26 +15,51 @@ const OnboardingContainer = () => {
// 1. Local state for multi-step onboarding
const [step, setStep] = useState(0);
+
+ /**
+ * Suppose `careerData.career_profile_id` is how we store the existing profile's ID
+ * If it's blank/undefined, that means "create new." If it has a value, we do an update.
+ */
const [careerData, setCareerData] = useState({});
const [financialData, setFinancialData] = useState({});
const [collegeData, setCollegeData] = useState({});
+ const [lastSelectedCareerProfileId, setLastSelectedCareerProfileId] = useState();
- // 2. On mount, check if localStorage has onboarding data
- useEffect(() => {
- const stored = localStorage.getItem('premiumOnboardingState');
- if (stored) {
- try {
- const parsed = JSON.parse(stored);
- // Restore step and data if they exist
- if (parsed.step !== undefined) setStep(parsed.step);
- if (parsed.careerData) setCareerData(parsed.careerData);
- if (parsed.financialData) setFinancialData(parsed.financialData);
- if (parsed.collegeData) setCollegeData(parsed.collegeData);
- } catch (err) {
- console.warn('Failed to parse premiumOnboardingState:', err);
- }
+ useEffect(() => {
+ // 1) Load premiumOnboardingState
+ const stored = localStorage.getItem('premiumOnboardingState');
+ let localCareerData = {};
+ let localFinancialData = {};
+ let localCollegeData = {};
+ let localStep = 0;
+
+ if (stored) {
+ try {
+ const parsed = JSON.parse(stored);
+ if (parsed.step !== undefined) localStep = parsed.step;
+ if (parsed.careerData) localCareerData = parsed.careerData;
+ if (parsed.financialData) localFinancialData = parsed.financialData;
+ if (parsed.collegeData) localCollegeData = parsed.collegeData;
+ } catch (err) {
+ console.warn('Failed to parse premiumOnboardingState:', err);
}
- }, []);
+ }
+
+ // 2) If there's a "lastSelectedCareerProfileId", override or set the career_profile_id
+ const existingId = localStorage.getItem('lastSelectedCareerProfileId');
+ if (existingId) {
+ // Only override if there's no existing ID in localCareerData
+ // or if you specifically want to *always* use the lastSelected ID.
+ localCareerData.career_profile_id = existingId;
+ }
+
+ // 3) Finally set states once
+ setStep(localStep);
+ setCareerData(localCareerData);
+ setFinancialData(localFinancialData);
+ setCollegeData(localCollegeData);
+}, []);
+
// 3. Whenever any key pieces of state change, save to localStorage
useEffect(() => {
@@ -48,77 +73,125 @@ const OnboardingContainer = () => {
}, [step, careerData, financialData, collegeData]);
// Move user to next or previous step
- const nextStep = () => setStep((prev) => prev + 1);
- const prevStep = () => setStep((prev) => prev - 1);
+ const nextStep = () => setStep(prev => prev + 1);
+ const prevStep = () => setStep(prev => prev - 1);
+ // Helper: parse float or return null
function parseFloatOrNull(value) {
- if (value == null || value === '') {
- return null;
- }
+ if (value == null || value === '') return null;
const parsed = parseFloat(value);
return isNaN(parsed) ? null : parsed;
}
- console.log('Final collegeData in OnboardingContainer:', collegeData);
+ console.log('Current careerData:', careerData);
+ console.log('Current collegeData:', collegeData);
// 4. Final “all done” submission
const handleFinalSubmit = async () => {
- try {
- const scenarioPayload = {
- ...careerData,
+ try {
+ // -- 1) Upsert scenario (career-profile) --
+
+ // If we already have an existing career_profile_id, pass it as "id"
+ // so the server does "ON DUPLICATE KEY UPDATE" instead of generating a new one.
+ // Otherwise, leave it undefined/null so the server creates a new record.
+ const scenarioPayload = {
+ ...careerData,
+ id: careerData.career_profile_id || undefined
+ };
+
+ const scenarioRes = await authFetch('/api/premium/career-profile', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(scenarioPayload),
+ });
+
+ if (!scenarioRes.ok) {
+ throw new Error('Failed to save (or update) career profile');
+ }
+
+ const scenarioJson = await scenarioRes.json();
+ let finalCareerProfileId = scenarioJson.career_profile_id;
+ if (!finalCareerProfileId) {
+ // If the server returns no ID for some reason, bail out
+ throw new Error('No career_profile_id returned by server');
+ }
+
+ // Update local state so we have the correct career_profile_id going forward
+ setCareerData(prev => ({
+ ...prev,
+ career_profile_id: finalCareerProfileId
+ }));
+
+ // 2) Upsert financial-profile (optional)
+ 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');
+ }
+
+ // 3) If user is in or planning college => upsert college-profile
+ if (
+ careerData.college_enrollment_status === 'currently_enrolled' ||
+ careerData.college_enrollment_status === 'prospective_student'
+ ) {
+ // Build an object that has all the correct property names
+ const mergedCollegeData = {
+ ...collegeData,
+ career_profile_id: finalCareerProfileId,
+ college_enrollment_status: careerData.college_enrollment_status,
+ is_in_state: !!collegeData.is_in_state,
+ is_in_district: !!collegeData.is_in_district,
+ is_online: !!collegeData.is_online, // ensure it matches backend naming
+ loan_deferral_until_graduation: !!collegeData.loan_deferral_until_graduation,
};
- // 1) POST career-profile (scenario)
- const careerRes = await authFetch('/api/premium/career-profile', {
+ // Convert numeric fields
+ const numericFields = [
+ 'existing_college_debt',
+ 'extra_payment',
+ 'tuition',
+ 'tuition_paid',
+ 'interest_rate',
+ 'loan_term',
+ 'credit_hours_per_year',
+ 'credit_hours_required',
+ 'hours_completed',
+ 'program_length',
+ 'expected_salary',
+ 'annual_financial_aid'
+ ];
+ numericFields.forEach(field => {
+ const val = parseFloatOrNull(mergedCollegeData[field]);
+ // If you want them to be 0 when blank, do:
+ mergedCollegeData[field] = val ?? 0;
+ });
+
+ const collegeRes = await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(scenarioPayload),
+ body: JSON.stringify(mergedCollegeData),
});
- if (!careerRes.ok) throw new Error('Failed to save career profile');
- const careerJson = await careerRes.json();
- const { career_profile_id } = careerJson;
- if (!career_profile_id) {
- throw new Error('No career_profile_id returned by server');
+ if (!collegeRes.ok) {
+ throw new Error('Failed to save college profile');
}
-
- // 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');
-
- // 3) Possibly POST college-profile
- if (
- careerData.college_enrollment_status === 'currently_enrolled' ||
- careerData.college_enrollment_status === 'prospective_student'
- ) {
- const mergedCollege = {
- ...collegeData,
- career_profile_id,
- college_enrollment_status: careerData.college_enrollment_status,
- };
- 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');
- } else {
- console.log('Skipping college-profile upsert because user is not enrolled/planning.');
- }
-
- // 4) Clear localStorage so next onboarding starts fresh (optional)
- localStorage.removeItem('premiumOnboardingState');
-
- // 5) Navigate away
- navigate('/career-roadmap');
- } catch (err) {
- console.error(err);
- // Optionally show error to user
+ } else {
+ console.log(
+ 'Skipping college-profile upsert; user not in or planning college.'
+ );
}
- };
+
+ // Navigate somewhere
+ navigate('/career-roadmap');
+
+ } catch (err) {
+ console.error('Error in final submit =>', err);
+ alert(err.message || 'Failed to finalize onboarding.');
+ }
+};
+
// 5. Array of steps
const onboardingSteps = [
diff --git a/src/components/PremiumOnboarding/ReviewPage.js b/src/components/PremiumOnboarding/ReviewPage.js
index 5696934..893ec18 100644
--- a/src/components/PremiumOnboarding/ReviewPage.js
+++ b/src/components/PremiumOnboarding/ReviewPage.js
@@ -15,6 +15,11 @@ function formatNum(val) {
return val;
}
+function formatYesNo(val) {
+ if (val == null) return 'N/A';
+ return val === true || val === 'yes' ? 'Yes' : 'No';
+}
+
function ReviewPage({
careerData = {},
financialData = {},
@@ -86,28 +91,48 @@ function ReviewPage({
{/* --- COLLEGE SECTION --- */}
- {inOrPlanningCollege && (
+ {inOrPlanningCollege && (
College Info
+
College Name: {collegeData.selected_school || 'N/A'}
+
Major: {collegeData.selected_program || 'N/A'}
+
Program Type: {collegeData.program_type || 'N/A'}
+
Yearly Tuition: {formatNum(collegeData.tuition)}
+
Program Length (years): {formatNum(collegeData.program_length)}
+
Credit Hours Per Year: {formatNum(collegeData.credit_hours_per_year)}
-
College Name: {formatNum(collegeData.selected_school)}
-
Major {formatNum(collegeData.selected_program)}
-
Program Type {formatNum(collegeData.program_type)}
-
Yearly Tuition {formatNum(collegeData.tuition)}
-
Program Length (years) {formatNum(collegeData.program_length)}
-
Credit Hours Per Year {formatNum(collegeData.credit_hours_per_year)}
-
Credit Hours Required {formatNum(collegeData.credit_hours_required)}
-
Hours Completed {formatNum(collegeData.hours_completed)}
-
Is In State? {formatNum(collegeData.is_in_state)}
-
Loan Deferral Until Graduation? {formatNum(collegeData.loan_deferral_until_graduation)}
-
Annual Financial Aid {formatNum(collegeData.annual_financial_aid)}
-
Existing College Debt {formatNum(collegeData.existing_college_debt)}
-
Extra Monthly Payment {formatNum(collegeData.extra_payment)}
-
Expected Graduation {formatNum(collegeData.expected_graduation)}
-
Expected Salary {formatNum(collegeData.expected_salary)}
+ {/*
+ Only render "Credit Hours Required" for
+ Doctoral / First Professional / Graduate/Professional Certificate
+ */}
+ {[
+ "Doctoral Degree",
+ "First Professional Degree",
+ "Graduate/Professional Certificate"
+ ].includes(collegeData.program_type) && (
+
+ Credit Hours Required: {formatNum(collegeData.credit_hours_required)}
+
+ )}
+
+ {/* Only render Hours Completed if "currently_enrolled" */}
+ {careerData.college_enrollment_status === 'currently_enrolled' && (
+
+ Hours Completed: {formatNum(collegeData.hours_completed)}
+
+ )}
+
+
Is In State?: {formatYesNo(collegeData.is_in_state)}
+
Loan Deferral Until Graduation?: {formatYesNo(collegeData.loan_deferral_until_graduation)}
+
Annual Financial Aid: {formatNum(collegeData.annual_financial_aid)}
+
Existing College Debt: {formatNum(collegeData.existing_college_debt)}
+
Extra Monthly Payment: {formatNum(collegeData.extra_payment)}
+
Expected Graduation: {collegeData.expected_graduation || 'N/A'}
+
Expected Salary: {formatNum(collegeData.expected_salary)}
)}
+
{/* --- ACTION BUTTONS --- */}