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 }) { // CIP / iPEDS local states const [schoolData, setSchoolData] = useState([]); const [icTuitionData, setIcTuitionData] = useState([]); const [schoolSuggestions, setSchoolSuggestions] = useState([]); const [programSuggestions, setProgramSuggestions] = useState([]); const [availableProgramTypes, setAvailableProgramTypes] = useState([]); // 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', 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.00'); /** * 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(); }, []); // School Name const handleSchoolChange = (e) => { const value = e.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 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; // 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; // 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; } // 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 ]); // 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); return (
Not currently enrolled or prospective student. Skipping college onboarding.
)}