Fixed college onboarding call/Review Page.
This commit is contained in:
parent
f9177fbf38
commit
0e62ac4708
@ -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) => (
|
||||
<span
|
||||
className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-full bg-blue-500 text-white text-xs cursor-help"
|
||||
title={msg}
|
||||
>
|
||||
i
|
||||
</span>
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (['interest_rate','loan_term','extra_payment','expected_salary'].includes(name)) {
|
||||
val = parseFloat(val) || 0;
|
||||
} else if (
|
||||
['annual_financial_aid','existing_college_debt','credit_hours_per_year',
|
||||
'hours_completed','credit_hours_required','tuition_paid'].includes(name)
|
||||
) {
|
||||
val = val === '' ? '' : parseFloat(val);
|
||||
}
|
||||
setData(prev => ({ ...prev, [name]: val }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 }));
|
||||
}
|
||||
};
|
||||
|
||||
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 user hasn't selected a program type or credit_hours_per_year is missing, skip
|
||||
if (!program_type) return;
|
||||
if (!hours_completed || !credit_hours_per_year) return;
|
||||
if (!credit_hours_per_year) return;
|
||||
|
||||
// If hours_completed is blank, treat as 0
|
||||
const completed = parseInt(hours_completed, 10) || 0;
|
||||
const perYear = parseFloat(credit_hours_per_year) || 1;
|
||||
|
||||
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 "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":
|
||||
required = parseInt(credit_hours_required, 10) || 0; break;
|
||||
// Possibly read from credit_hours_required
|
||||
required = parseInt(credit_hours_required, 10) || 0;
|
||||
break;
|
||||
default:
|
||||
required = parseInt(credit_hours_required, 10) || 0; break;
|
||||
// For any other program type, use whatever is in credit_hours_required
|
||||
required = parseInt(credit_hours_required, 10) || 0;
|
||||
break;
|
||||
}
|
||||
|
||||
const remain = required - (parseInt(hours_completed, 10) || 0);
|
||||
const yrs = remain / (parseFloat(credit_hours_per_year) || 1);
|
||||
// Subtract however many credits they've already completed (that count)
|
||||
const remain = required - completed;
|
||||
const yrs = remain / perYear;
|
||||
const calcLength = yrs.toFixed(2);
|
||||
|
||||
setAutoProgramLength(calcLength);
|
||||
}, [program_type, hours_completed, credit_hours_per_year, credit_hours_required]);
|
||||
}, [
|
||||
program_type,
|
||||
hours_completed,
|
||||
credit_hours_per_year,
|
||||
credit_hours_required
|
||||
]);
|
||||
|
||||
// final handleSubmit
|
||||
|
||||
|
||||
|
||||
// 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"
|
||||
/>
|
||||
<label className="font-medium">In District?</label>
|
||||
<label className="font-medium">In District? {infoIcon("Used by Community Colleges usually - local discounts")}</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -321,7 +383,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
onChange={handleParentFieldChange}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label className="font-medium">In State Tuition?</label>
|
||||
<label className="font-medium">In State Tuition? {infoIcon("Students can qualify for discounted tuition if they are a resident of the same state as the college - private institutions rarely have this discounts")}</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -332,7 +394,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
onChange={handleParentFieldChange}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label className="font-medium">Program is Fully Online</label>
|
||||
<label className="font-medium">Program is Fully Online {infoIcon("Tuition rates for fully online programs are usually different than traditional")}</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -343,12 +405,12 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
onChange={handleParentFieldChange}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label className="font-medium">Defer Loan Payments until Graduation?</label>
|
||||
<label className="font-medium">Defer Loan Payments until Graduation? {infoIcon("You can delay paying tuition loans while still in college, but they will still accrue interest during this time")}</label>
|
||||
</div>
|
||||
|
||||
{/* School / Program */}
|
||||
{/* School */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">School Name*</label>
|
||||
<label className="block font-medium">School Name* {infoIcon("Start typing and click from auto-suggest")}</label>
|
||||
<input
|
||||
name="selected_school"
|
||||
value={selected_school}
|
||||
@ -369,7 +431,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Program Name*</label>
|
||||
<label className="block font-medium">Marjor/Program Name* {infoIcon("Search and click from auto-suggest. If for some reason your major isn't listed, please send us a note.")}</label>
|
||||
<input
|
||||
name="selected_program"
|
||||
value={selected_program}
|
||||
@ -390,7 +452,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Program Type*</label>
|
||||
<label className="block font-medium">Degree Type* {infoIcon("What level of degree are you/will you be persuing?")}</label>
|
||||
<select
|
||||
name="program_type"
|
||||
value={program_type}
|
||||
@ -404,9 +466,9 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Academic Calendar (just re-added) */}
|
||||
{/* Academic Calendar */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Academic Calendar</label>
|
||||
<label className="block font-medium">Academic Calendar {infoIcon("What annual calendar type does the college operate under - pick semester if you are unsure.")}</label>
|
||||
<select
|
||||
name="academic_calendar"
|
||||
value={academic_calendar}
|
||||
@ -420,25 +482,25 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 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') && (
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Credit Hours Required</label>
|
||||
<label className="block font-medium">Credit Hours Required {infoIcon("Some Graduate and Professional Programs differ on number of hours required.")}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="credit_hours_required"
|
||||
value={credit_hours_required}
|
||||
onChange={handleParentFieldChange}
|
||||
placeholder="e.g. 30"
|
||||
placeholder="e.g. 120"
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Credit Hours Per Year</label>
|
||||
<label className="block font-medium">Credit Hours Per Year {infoIcon("How many hours do you plan to take each year? 24 is usually considered full-time, but to finish a Bachelor's in 4 you need to take 30.")}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="credit_hours_per_year"
|
||||
@ -456,14 +518,14 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
type="number"
|
||||
value={displayedTuition}
|
||||
onChange={handleManualTuitionChange}
|
||||
placeholder="Leave blank to use auto, or type an override"
|
||||
placeholder="Leave blank to use auto-calculated, or type an override"
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Annual Financial Aid with "Need Help?" Wizard button */}
|
||||
{/* Annual Financial Aid */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">(Estimated) Annual Financial Aid</label>
|
||||
<label className="block font-medium">(Estimated) Annual Financial Aid {infoIcon("You usually won't know this exact value until right before you start college. Use estimates in AptivaAI.")}</label>
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
@ -476,7 +538,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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?
|
||||
</button>
|
||||
@ -484,7 +546,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Existing College Loan Debt</label>
|
||||
<label className="block font-medium">Existing College Loan Debt {infoIcon("If you have existing student loans, enter the value here. Estimates are just fine here, but detailed forecasts require detailed inputs.")}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="existing_college_debt"
|
||||
@ -495,24 +557,27 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
/>
|
||||
</div>
|
||||
|
||||
{college_enrollment_status === 'prospective_student' && (
|
||||
{/* Show Program Length for both "currently_enrolled" & "prospective_student" */}
|
||||
{(college_enrollment_status === 'currently_enrolled' ||
|
||||
college_enrollment_status === 'prospective_student') && (
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Intended Start Date</label>
|
||||
<label className="block font-medium">Program Length {infoIcon("How many years it will take you to complete the degree/program given the number of hours you plan to take")}</label>
|
||||
<input
|
||||
type="date"
|
||||
name="enrollment_date"
|
||||
value={enrollment_date || ''}
|
||||
onChange={handleParentFieldChange}
|
||||
type="number"
|
||||
value={displayedProgramLength}
|
||||
onChange={handleManualProgramLengthChange}
|
||||
placeholder="Leave blank to use auto, or type an override"
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* If "currently_enrolled" show Hours Completed + Program Length */}
|
||||
{/* If currently_enrolled => hours_completed */}
|
||||
{college_enrollment_status === 'currently_enrolled' && (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Completed Credit Hours (that count towards program completion)</label>
|
||||
<label className="block font-medium">
|
||||
Completed Credit Hours (that count towards program completion) {infoIcon("If you have already completed some college credits that apply to the selected Program above, enter them here. Bachelor's = 120")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="hours_completed"
|
||||
@ -522,22 +587,10 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Program Length</label>
|
||||
<input
|
||||
type="number"
|
||||
value={displayedProgramLength}
|
||||
onChange={handleManualProgramLengthChange}
|
||||
placeholder="Leave blank to use auto, or type an override"
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Expected Graduation</label>
|
||||
<label className="block font-medium">Expected Graduation {infoIcon("If you don't know the exact date, that's fine - just enter the targeted month")}</label>
|
||||
<input
|
||||
type="date"
|
||||
name="expected_graduation"
|
||||
@ -548,7 +601,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Loan Interest Rate (%)</label>
|
||||
<label className="block font-medium">Loan Interest Rate (%) {infoIcon("These can vary, enter the best blended rate you can approximate if you have multiple.")}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="interest_rate"
|
||||
@ -560,7 +613,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Loan Term (years)</label>
|
||||
<label className="block font-medium">Loan Term (years) {infoIcon("Education loans typically have a 10-year payback period, but can vary.")}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="loan_term"
|
||||
@ -572,7 +625,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Extra Monthly Payment</label>
|
||||
<label className="block font-medium">Extra Monthly Payment {infoIcon("Extra money you plan to pay towards the loans each month.")}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="extra_payment"
|
||||
@ -584,7 +637,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Expected Salary After Graduation</label>
|
||||
<label className="block font-medium">Expected Salary After Graduation {infoIcon("If you're just starting out, expect towards the 10th percentile values for your targeted career.")}</label>
|
||||
<input
|
||||
type="number"
|
||||
name="expected_salary"
|
||||
@ -615,12 +668,10 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* RENDER THE MODAL WITH FINANCIAL AID WIZARD IF showAidWizard === true */}
|
||||
{showAidWizard && (
|
||||
<Modal onClose={() => setShowAidWizard(false)}>
|
||||
<FinancialAidWizard
|
||||
onAidEstimated={(estimate) => {
|
||||
// Update the annual_financial_aid with the wizard's result
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
annual_financial_aid: estimate
|
||||
|
@ -15,27 +15,52 @@ 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(() => {
|
||||
// 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);
|
||||
// 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);
|
||||
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(() => {
|
||||
const stateToStore = {
|
||||
@ -48,78 +73,126 @@ 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 {
|
||||
// -- 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
|
||||
};
|
||||
|
||||
// 1) POST career-profile (scenario)
|
||||
const careerRes = await authFetch('/api/premium/career-profile', {
|
||||
const scenarioRes = await authFetch('/api/premium/career-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(scenarioPayload),
|
||||
});
|
||||
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) {
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// 2) POST financial-profile
|
||||
// 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');
|
||||
if (!financialRes.ok) {
|
||||
throw new Error('Failed to save financial profile');
|
||||
}
|
||||
|
||||
// 3) Possibly POST college-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'
|
||||
) {
|
||||
const mergedCollege = {
|
||||
// Build an object that has all the correct property names
|
||||
const mergedCollegeData = {
|
||||
...collegeData,
|
||||
career_profile_id,
|
||||
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,
|
||||
};
|
||||
|
||||
// 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(mergedCollege),
|
||||
body: JSON.stringify(mergedCollegeData),
|
||||
});
|
||||
if (!collegeRes.ok) throw new Error('Failed to save college profile');
|
||||
if (!collegeRes.ok) {
|
||||
throw new Error('Failed to save college profile');
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping college-profile upsert because user is not enrolled/planning.');
|
||||
console.log(
|
||||
'Skipping college-profile upsert; user not in or planning college.'
|
||||
);
|
||||
}
|
||||
|
||||
// 4) Clear localStorage so next onboarding starts fresh (optional)
|
||||
localStorage.removeItem('premiumOnboardingState');
|
||||
|
||||
// 5) Navigate away
|
||||
// Navigate somewhere
|
||||
navigate('/career-roadmap');
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// Optionally show error to user
|
||||
console.error('Error in final submit =>', err);
|
||||
alert(err.message || 'Failed to finalize onboarding.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 5. Array of steps
|
||||
const onboardingSteps = [
|
||||
<PremiumWelcome nextStep={nextStep} />,
|
||||
|
@ -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 = {},
|
||||
@ -89,25 +94,45 @@ function ReviewPage({
|
||||
{inOrPlanningCollege && (
|
||||
<div className="p-4 border rounded-md space-y-2">
|
||||
<h3 className="text-xl font-semibold">College Info</h3>
|
||||
<div><strong>College Name:</strong> {collegeData.selected_school || 'N/A'}</div>
|
||||
<div><strong>Major:</strong> {collegeData.selected_program || 'N/A'}</div>
|
||||
<div><strong>Program Type:</strong> {collegeData.program_type || 'N/A'}</div>
|
||||
<div><strong>Yearly Tuition:</strong> {formatNum(collegeData.tuition)}</div>
|
||||
<div><strong>Program Length (years):</strong> {formatNum(collegeData.program_length)}</div>
|
||||
<div><strong>Credit Hours Per Year:</strong> {formatNum(collegeData.credit_hours_per_year)}</div>
|
||||
|
||||
<div><strong>College Name:</strong> {formatNum(collegeData.selected_school)}</div>
|
||||
<div><strong>Major</strong> {formatNum(collegeData.selected_program)}</div>
|
||||
<div><strong>Program Type</strong> {formatNum(collegeData.program_type)}</div>
|
||||
<div><strong>Yearly Tuition</strong> {formatNum(collegeData.tuition)}</div>
|
||||
<div><strong>Program Length (years)</strong> {formatNum(collegeData.program_length)}</div>
|
||||
<div><strong>Credit Hours Per Year</strong> {formatNum(collegeData.credit_hours_per_year)}</div>
|
||||
<div><strong>Credit Hours Required</strong> {formatNum(collegeData.credit_hours_required)}</div>
|
||||
<div><strong>Hours Completed</strong> {formatNum(collegeData.hours_completed)}</div>
|
||||
<div><strong>Is In State?</strong> {formatNum(collegeData.is_in_state)}</div>
|
||||
<div><strong>Loan Deferral Until Graduation?</strong> {formatNum(collegeData.loan_deferral_until_graduation)}</div>
|
||||
<div><strong>Annual Financial Aid</strong> {formatNum(collegeData.annual_financial_aid)}</div>
|
||||
<div><strong>Existing College Debt</strong> {formatNum(collegeData.existing_college_debt)}</div>
|
||||
<div><strong>Extra Monthly Payment</strong> {formatNum(collegeData.extra_payment)}</div>
|
||||
<div><strong>Expected Graduation</strong> {formatNum(collegeData.expected_graduation)}</div>
|
||||
<div><strong>Expected Salary</strong> {formatNum(collegeData.expected_salary)}</div>
|
||||
{/*
|
||||
Only render "Credit Hours Required" for
|
||||
Doctoral / First Professional / Graduate/Professional Certificate
|
||||
*/}
|
||||
{[
|
||||
"Doctoral Degree",
|
||||
"First Professional Degree",
|
||||
"Graduate/Professional Certificate"
|
||||
].includes(collegeData.program_type) && (
|
||||
<div>
|
||||
<strong>Credit Hours Required:</strong> {formatNum(collegeData.credit_hours_required)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Only render Hours Completed if "currently_enrolled" */}
|
||||
{careerData.college_enrollment_status === 'currently_enrolled' && (
|
||||
<div>
|
||||
<strong>Hours Completed:</strong> {formatNum(collegeData.hours_completed)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div><strong>Is In State?:</strong> {formatYesNo(collegeData.is_in_state)}</div>
|
||||
<div><strong>Loan Deferral Until Graduation?:</strong> {formatYesNo(collegeData.loan_deferral_until_graduation)}</div>
|
||||
<div><strong>Annual Financial Aid:</strong> {formatNum(collegeData.annual_financial_aid)}</div>
|
||||
<div><strong>Existing College Debt:</strong> {formatNum(collegeData.existing_college_debt)}</div>
|
||||
<div><strong>Extra Monthly Payment:</strong> {formatNum(collegeData.extra_payment)}</div>
|
||||
<div><strong>Expected Graduation:</strong> {collegeData.expected_graduation || 'N/A'}</div>
|
||||
<div><strong>Expected Salary:</strong> {formatNum(collegeData.expected_salary)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* --- ACTION BUTTONS --- */}
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="secondary" onClick={onBack}>
|
||||
|
Loading…
Reference in New Issue
Block a user