import React, { useState, useEffect, useRef } from 'react'; import Modal from '../../components/ui/modal.js'; import FinancialAidWizard from '../../components/FinancialAidWizard.js'; import { useLocation } from 'react-router-dom'; import api from '../../auth/apiClient.js'; import { loadDraft, clearDraft, saveDraft } from '../../utils/onboardingDraftApi.js'; const Req = () => *; function CollegeOnboarding({ nextStep, prevStep, data, setData }) { const [schoolSuggestions, setSchoolSuggestions] = useState([]); const schoolPrevRef = useRef(''); 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 || '' ); const [selectedUnitId, setSelectedUnitId] = useState(null); const [expectedGraduation, setExpectedGraduation] = useState(data.expected_graduation || ''); const [showAidWizard, setShowAidWizard] = useState(false); const location = useLocation(); const navSelectedSchoolObj = location.state?.selectedSchool ?? location.state?.premiumOnboardingState?.selectedSchool; const [selectedSchool, setSelectedSchool] = useState(() => { if (navSelectedSchoolObj && typeof navSelectedSchoolObj === 'object') { return { INSTNM: navSelectedSchoolObj.INSTNM, CIPDESC: navSelectedSchoolObj.CIPDESC || '', CREDDESC: navSelectedSchoolObj.CREDDESC || '' }; } if (data.selected_school) { return { INSTNM: data.selected_school, CIPDESC: data.selected_program || '', CREDDESC: data.program_type || '' }; } return null; }); 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 = '', 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]); // Backfill from cookie-backed draft if props aren't populated yet useEffect(() => { // if props already have values, do nothing if (data?.selected_school || data?.selected_program || data?.program_type) return; let cancelled = false; (async () => { let draft; try { draft = await loadDraft(); } catch { draft = null; } const cd = draft?.data?.collegeData; if (!cd) return; if (cancelled) return; // 1) write into parent data (so inputs prefill) setData(prev => ({ ...prev, selected_school : cd.selected_school ?? prev.selected_school ?? '', selected_program: cd.selected_program ?? prev.selected_program ?? '', program_type : cd.program_type ?? prev.program_type ?? '' })); // 2) set local selectedSchool object (triggers your selectedSchool→data effect too) setSelectedSchool({ INSTNM : cd.selected_school || '', CIPDESC : cd.selected_program || '', CREDDESC: cd.program_type || '' }); })(); return () => { cancelled = true; }; // run once on mount; we don't want to fight subsequent user edits // eslint-disable-next-line react-hooks/exhaustive-deps }, []); 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: field, value, type, checked } = e.target; let val = value; if (type === 'checkbox') { val = checked; setData(prev => ({ ...prev, [field]: val })); return; } // If the user typed an empty string, store '' so they can see it's blank if (val.trim() === '') { setData(prev => ({ ...prev, [field]: '' })); return; } // Otherwise, parse it if it's one of the numeric fields if (['interest_rate', 'loan_term', 'extra_payment', 'expected_salary'].includes(field)) { const parsed = parseFloat(val); // If parse fails => store '' (or fallback to old value) if (isNaN(parsed)) { setData(prev => ({ ...prev, [field]: '' })); } else { setData(prev => ({ ...prev, [field]: parsed })); } } else if ([ 'annual_financial_aid','existing_college_debt','credit_hours_per_year', 'hours_completed','credit_hours_required','tuition_paid' ].includes(field)) { const parsed = parseFloat(val); setData(prev => ({ ...prev, [field]: isNaN(parsed) ? '' : parsed })); } else { // For non-numeric or strings setData(prev => ({ ...prev, [field]: val })); } }; const handleManualTuitionChange = (e) => { setManualTuition(e.target.value); }; const handleManualProgramLengthChange = (e) => { setManualProgramLength(e.target.value); }; 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 = async (e) => { const value = e.target.value || ''; setData(prev => ({ ...prev, selected_school: value, selected_program: '', program_type: '', credit_hours_required: '' })); if (!value.trim()) { setSchoolSuggestions([]); return; } const it = e?.nativeEvent?.inputType; // Chromium: 'insertReplacementText' on datalist pick const replacement = it === 'insertReplacementText'; const bigJump = Math.abs(value.length - (schoolPrevRef.current || '').length) > 1; try { const resp = await api.get('/api/schools/suggest', { params: { query: value, limit: 10 }}); const opts = Array.isArray(resp.data) ? resp.data : []; setSchoolSuggestions(opts); // if user actually picked from dropdown → commit now (sets UNITID) const exact = opts.find(o => (o.name || '').toLowerCase() === value.toLowerCase()); if (exact && (replacement || bigJump)) { handleSchoolSelect(exact); // sets selectedUnitId + clears suggestions } } catch { setSchoolSuggestions([]); } schoolPrevRef.current = value; }; const handleSchoolSelect = (schoolObj) => { const name = schoolObj?.name || schoolObj || ''; const uid = schoolObj?.unitId || null; setSelectedUnitId(uid); setData(prev => ({ ...prev, selected_school: name, selected_program: '', program_type: '', credit_hours_required: '' })); setSchoolSuggestions([]); setProgramSuggestions([]); setAvailableProgramTypes([]); saveDraft({ collegeData: { selected_school: name } }).catch(() => {}); }; // Program const handleProgramChange = async (e) => { const value = e.target.value; setData(prev => ({ ...prev, selected_program: value })); if (!value || !selected_school) { setProgramSuggestions([]); return; } try { const { data } = await api.get('/api/programs/suggest', { params: { school: selected_school, query: value, limit: 10 } }); setProgramSuggestions(Array.isArray(data) ? data : []); // [{ program }] } catch { setProgramSuggestions([]); } }; const handleProgramSelect = (prog) => { setData(prev => ({ ...prev, selected_program: prog })); setProgramSuggestions([]); saveDraft({ collegeData: { selected_program: prog } }).catch(() => {}); }; const handleProgramTypeSelect = (e) => { const val = e.target.value; setData(prev => ({ ...prev, program_type: val, credit_hours_required: '', })); setManualProgramLength(''); setAutoProgramLength('0.00'); saveDraft({ collegeData: { program_type: val } }).catch(() => {}); }; // once we have school+program => load possible program types useEffect(() => { if (!selected_program || !selected_school) { setAvailableProgramTypes([]); return; } (async () => { try { const { data } = await api.get('/api/programs/types', { params: { school: selected_school, program: selected_program }}); setAvailableProgramTypes(Array.isArray(data?.types) ? data.types : []); } catch { setAvailableProgramTypes([]); } })(); }, [selected_program, selected_school]); /*Auto Calc Tuition*/ useEffect(() => { (async () => { if (!selectedUnitId || !program_type || !credit_hours_per_year) return; try { const { data } = await api.get('/api/tuition/estimate', { params: { unitId: String(selectedUnitId), programType: program_type, inState: is_in_state ? 1 : 0, inDistrict: is_in_district ? 1 : 0, creditHoursPerYear: Number(credit_hours_per_year) || 0 } }); setAutoTuition(Number.isFinite(data?.estimate) ? data.estimate : 0); } catch { setAutoTuition(0); } })(); }, [selectedUnitId, program_type, credit_hours_per_year, is_in_state, is_in_district]); // 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, ]); useEffect(() => { const hasSchool = !!data.selected_school; const hasAnyProgram = !!data.selected_program || !!data.program_type; if (!hasSchool && !hasAnyProgram) return; setSelectedSchool(prev => { const next = { INSTNM : data.selected_school || '', CIPDESC : data.selected_program || '', CREDDESC: data.program_type || '' }; // avoid useless state churn if (prev && prev.INSTNM === next.INSTNM && prev.CIPDESC === next.CIPDESC && prev.CREDDESC=== next.CREDDESC) return prev; return next; }); }, [data.selected_school, data.selected_program, data.program_type]); /* ------------------------------------------------------------------ */ /* 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, interest_rate, loan_term, 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.
)}