import React, { useState, useEffect } from 'react'; import Modal from '../../components/ui/modal.js'; import FinancialAidWizard from '../../components/FinancialAidWizard.js'; import { useLocation } from 'react-router-dom'; const Req = () => *; function CollegeOnboarding({ nextStep, prevStep, data, setData }) { // CIP / iPEDS local states const [schoolData, setSchoolData] = useState([]); const [icTuitionData, setIcTuitionData] = useState([]); const [schoolSuggestions, setSchoolSuggestions] = useState([]); const [programSuggestions, setProgramSuggestions] = useState([]); const [availableProgramTypes, setAvailableProgramTypes] = useState([]); const [schoolValid, setSchoolValid] = useState(false); const [programValid, setProgramValid] = useState(false); const [enrollmentDate, setEnrollmentDate] = useState( data.enrollment_date || '' // carry forward if the user goes back ); const [expectedGraduation, setExpectedGraduation] = useState(data.expected_graduation || ''); // Show/hide the financial aid wizard const [showAidWizard, setShowAidWizard] = useState(false); const location = useLocation(); const navSelectedSchoolRaw = location.state?.selectedSchool; const navSelectedSchool = toSchoolName(navSelectedSchoolRaw); function dehydrate(schObj) { if (!schObj || typeof schObj !== 'object') return null; /* keep only the fields you really need */ const { INSTNM, CIPDESC, CREDDESC, ...rest } = schObj; return { INSTNM, CIPDESC, CREDDESC, ...rest }; } const [selectedSchool, setSelectedSchool] = useState(() => dehydrate(navSelectedSchool) || dehydrate(JSON.parse(localStorage.getItem('premiumOnboardingState') || '{}' ).collegeData?.selectedSchool) ); function toSchoolName(objOrStr) { if (!objOrStr) return ''; if (typeof objOrStr === 'object') return objOrStr.INSTNM || ''; return objOrStr; // already a string } const infoIcon = (msg) => ( i ); // Destructure parent data const { college_enrollment_status = '', selected_school = selectedSchool, selected_program = '', program_type = '', academic_calendar = 'semester', annual_financial_aid = '', is_online = false, existing_college_debt = '', enrollment_date = '', expected_graduation = '', interest_rate = 5.5, loan_term = 10, extra_payment = '', expected_salary = '', is_in_state = false, is_in_district = false, loan_deferral_until_graduation = false, credit_hours_per_year = '', hours_completed = '', credit_hours_required = '', tuition_paid = '', } = data; // 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); const inSchool = ['currently_enrolled','prospective_student'] .includes(college_enrollment_status); useEffect(() => { if (selectedSchool) { setData(prev => ({ ...prev, selected_school : selectedSchool.INSTNM, selected_program: selectedSchool.CIPDESC || prev.selected_program, program_type : selectedSchool.CREDDESC || prev.program_type })); } }, [selectedSchool, setData]); useEffect(() => { if (data.expected_graduation && !expectedGraduation) setExpectedGraduation(data.expected_graduation); }, [data.expected_graduation]); /** * 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; 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) => { setManualTuition(e.target.value); }; const handleManualProgramLengthChange = (e) => { setManualProgramLength(e.target.value); }; // CIP data useEffect(() => { async function fetchCipData() { try { const res = await fetch('/cip_institution_mapping_new.json'); const text = await res.text(); const lines = text.split('\n'); const parsed = lines.map(line => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean); setSchoolData(parsed); } catch (err) { console.error("Failed to load CIP data:", err); } } fetchCipData(); }, []); // iPEDS data useEffect(() => { async function fetchIpedsData() { try { const res = await fetch('/ic2023_ay.csv'); const text = await res.text(); const rows = text.split('\n').map(line => line.split(',')); const headers = rows[0]; const dataRows = rows.slice(1).map(row => Object.fromEntries(row.map((val, idx) => [headers[idx], val])) ); setIcTuitionData(dataRows); } catch (err) { console.error("Failed to load iPEDS data:", err); } } fetchIpedsData(); }, []); useEffect(() => { if (college_enrollment_status !== 'prospective_student') return; const lenYears = Number(data.program_length || ''); if (!enrollmentDate || !lenYears) return; const start = new Date(enrollmentDate); const est = new Date(start.getFullYear() + lenYears, start.getMonth(), start.getDate()); const iso = firstOfNextMonth(est); setExpectedGraduation(iso); setData(prev => ({ ...prev, expected_graduation: iso })); }, [college_enrollment_status, enrollmentDate, data.program_length, setData]); // School Name const handleSchoolChange = (eOrVal) => { const value = typeof eOrVal === 'string' ? eOrVal : eOrVal?.target?.value || ''; setData(prev => ({ ...prev, selected_school: value, selected_program: '', program_type: '', credit_hours_required: '', })); const filtered = schoolData.filter(s => s.INSTNM.toLowerCase().includes(value.toLowerCase()) ); const uniqueSchools = [...new Set(filtered.map(s => s.INSTNM))]; setSchoolSuggestions(uniqueSchools.slice(0, 10)); setProgramSuggestions([]); setAvailableProgramTypes([]); }; const handleSchoolSelect = (schoolName) => { setData(prev => ({ ...prev, selected_school: schoolName, selected_program: '', program_type: '', credit_hours_required: '', })); setSchoolSuggestions([]); setProgramSuggestions([]); setAvailableProgramTypes([]); }; // Program const handleProgramChange = (e) => { const value = e.target.value; setData(prev => ({ ...prev, selected_program: value })); if (!value) { setProgramSuggestions([]); return; } const filtered = schoolData.filter( s => s.INSTNM.toLowerCase() === selected_school.toLowerCase() && s.CIPDESC.toLowerCase().includes(value.toLowerCase()) ); const uniquePrograms = [...new Set(filtered.map(s => s.CIPDESC))]; setProgramSuggestions(uniquePrograms.slice(0, 10)); }; const handleProgramSelect = (prog) => { setData(prev => ({ ...prev, selected_program: prog })); setProgramSuggestions([]); }; const handleProgramTypeSelect = (e) => { const val = e.target.value; setData(prev => ({ ...prev, program_type: val, credit_hours_required: '', })); setManualProgramLength(''); setAutoProgramLength('0.00'); }; // once we have school+program => load possible program types useEffect(() => { if (!selected_program || !selected_school || !schoolData.length) return; const possibleTypes = schoolData .filter( s => s.INSTNM.toLowerCase() === selected_school.toLowerCase() && s.CIPDESC === selected_program ) .map(s => s.CREDDESC); setAvailableProgramTypes([...new Set(possibleTypes)]); }, [selected_program, selected_school, schoolData]); // auto-calc tuition useEffect(() => { if (!icTuitionData.length) return; if (!selected_school || !program_type || !credit_hours_per_year) return; const found = schoolData.find( s => s.INSTNM.toLowerCase() === selected_school.toLowerCase() ); if (!found) return; const unitId = found.UNITID; if (!unitId) return; const match = icTuitionData.find(row => row.UNITID === unitId); if (!match) return; const isGradOrProf = [ "Master's Degree", "Doctoral Degree", "Graduate/Professional Certificate", "First Professional Degree" ].includes(program_type); let partTimeRate = 0; let fullTimeTuition = 0; if (isGradOrProf) { if (is_in_district) { partTimeRate = parseFloat(match.HRCHG5 || 0); fullTimeTuition = parseFloat(match.TUITION5 || 0); } else if (is_in_state) { partTimeRate = parseFloat(match.HRCHG6 || 0); fullTimeTuition = parseFloat(match.TUITION6 || 0); } else { partTimeRate = parseFloat(match.HRCHG7 || 0); fullTimeTuition = parseFloat(match.TUITION7 || 0); } } else { // undergrad if (is_in_district) { partTimeRate = parseFloat(match.HRCHG1 || 0); fullTimeTuition = parseFloat(match.TUITION1 || 0); } else if (is_in_state) { partTimeRate = parseFloat(match.HRCHG2 || 0); fullTimeTuition = parseFloat(match.TUITION2 || 0); } else { partTimeRate = parseFloat(match.HRCHG3 || 0); fullTimeTuition = parseFloat(match.TUITION3 || 0); } } const chpy = parseFloat(credit_hours_per_year) || 0; let estimate = 0; if (chpy < 24 && partTimeRate) { estimate = partTimeRate * chpy; } else { estimate = fullTimeTuition; } setAutoTuition(Math.round(estimate)); }, [ icTuitionData, selected_school, program_type, credit_hours_per_year, is_in_state, is_in_district, schoolData ]); // auto-calc program length useEffect(() => { if (!program_type || !credit_hours_per_year) return; 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 = 180; break; case "Doctoral Degree": required = 240; break; case "First Professional Degree": required = 180; break; case "Graduate/Professional Certificate": required = parseInt(credit_hours_required, 10) || 0; break; case "Undergraduate Certificate or Diploma": required = parseInt(credit_hours_required, 10) || 30; // sensible default break; default: required = parseInt(credit_hours_required, 10) || 0; } /* never negative */ const remain = Math.max(0, required - completed); const yrs = remain / perYear; setAutoProgramLength(parseFloat(yrs.toFixed(2))); }, [ program_type, hours_completed, credit_hours_per_year, credit_hours_required, ]); /* ------------------------------------------------------------------ */ /* Whenever the user changes enrollmentDate OR programLength */ /* (program_length is already in parent data), compute grad date. */ /* ------------------------------------------------------------------ */ useEffect(() => { /* decide which “length” the user is looking at right now */ const lenRaw = manualProgramLength.trim() !== '' ? manualProgramLength : autoProgramLength; const len = parseFloat(lenRaw); // years (may be fractional) const startISO = pickStartDate(); // '' or yyyy‑mm‑dd if (!startISO || !len) return; // nothing to do yet const start = new Date(startISO); /* naïve add – assuming program_length is years; * * adjust if you store months instead */ /* 1 year = 12 months ‑‑ preserve fractions (e.g. 1.75 y = 21 m) */ const monthsToAdd = Math.round(len * 12); const estGrad = new Date(start); // clone estGrad.setMonth(estGrad.getMonth() + monthsToAdd); const gradISO = firstOfNextMonth(estGrad); setExpectedGraduation(gradISO); setData(prev => ({ ...prev, expected_graduation: gradISO })); }, [college_enrollment_status, enrollmentDate, manualProgramLength, autoProgramLength, setData]); // final handleSubmit => we store chosen tuition + program_length, then move on const handleSubmit = () => { const chosenTuition = manualTuition.trim() === '' ? autoTuition : parseFloat(manualTuition); const chosenProgramLength = manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength; setData(prev => ({ ...prev, tuition: chosenTuition, program_length: chosenProgramLength })); nextStep(); }; // displayedTuition / displayedProgramLength const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition); const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength); function pickStartDate() { if (college_enrollment_status === 'prospective_student') { return enrollmentDate; // may still be '' } if (college_enrollment_status === 'currently_enrolled') { return firstOfNextMonth(new Date()); // today → 1st next month } return ''; // anybody else } function firstOfNextMonth(dateObj) { return new Date(dateObj.getFullYear(), dateObj.getMonth() + 1, 1) .toISOString() .slice(0, 10); // yyyy‑mm‑dd } const ready = (!inSchool || expectedGraduation) && // grad date iff in school selected_school && program_type; return (
Not currently enrolled or prospective student. Skipping college onboarding.
)}