From 0e62ac4708a6e48b4711395025bfd68ee089d39d Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 4 Jun 2025 14:54:24 +0000 Subject: [PATCH] Fixed college onboarding call/Review Page. --- .../PremiumOnboarding/CollegeOnboarding.js | 241 +++++++++++------- .../PremiumOnboarding/OnboardingContainer.js | 213 +++++++++++----- .../PremiumOnboarding/ReviewPage.js | 59 +++-- 3 files changed, 331 insertions(+), 182 deletions(-) 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 */}
- +
- +
- +
- {/* 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 --- */}