563 lines
18 KiB
JavaScript
563 lines
18 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import authFetch from '../../utils/authFetch.js';
|
||
|
||
|
||
function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId }) {
|
||
// CIP / iPEDS local states (purely for CIP data and suggestions)
|
||
const [schoolData, setSchoolData] = useState([]);
|
||
const [icTuitionData, setIcTuitionData] = useState([]);
|
||
const [schoolSuggestions, setSchoolSuggestions] = useState([]);
|
||
const [programSuggestions, setProgramSuggestions] = useState([]);
|
||
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
|
||
|
||
// ---- DESCTRUCTURE PARENT DATA FOR ALL FIELDS EXCEPT TUITION/PROGRAM_LENGTH ----
|
||
// We'll store user "typed" values for tuition/program_length in local states,
|
||
// but everything else comes directly from `data`.
|
||
const {
|
||
college_enrollment_status = '',
|
||
selected_school = '',
|
||
selected_program = '',
|
||
program_type = '',
|
||
academic_calendar = 'semester',
|
||
annual_financial_aid = '',
|
||
is_online = false,
|
||
existing_college_debt = '',
|
||
expected_graduation = '',
|
||
interest_rate = 5.5,
|
||
loan_term = 10,
|
||
extra_payment = '',
|
||
expected_salary = '',
|
||
is_in_state = true,
|
||
is_in_district = false,
|
||
loan_deferral_until_graduation = false,
|
||
credit_hours_per_year = '',
|
||
hours_completed = '',
|
||
credit_hours_required = '',
|
||
tuition_paid = '',
|
||
// We do NOT consume data.tuition or data.program_length directly here
|
||
// because we store them in local states (manualTuition, manualProgramLength).
|
||
} = data;
|
||
|
||
// ---- 1. LOCAL STATES for auto/manual logic on TWO fields ----
|
||
// manualTuition: user typed override
|
||
// autoTuition: iPEDS calculation
|
||
const [manualTuition, setManualTuition] = useState(''); // '' means no manual override
|
||
const [autoTuition, setAutoTuition] = useState(0);
|
||
|
||
// same approach for program_length
|
||
const [manualProgramLength, setManualProgramLength] = useState('');
|
||
const [autoProgramLength, setAutoProgramLength] = useState('0.00');
|
||
|
||
// ------------------------------------------
|
||
// Universal handleChange for all parent fields
|
||
// ------------------------------------------
|
||
const handleParentFieldChange = (e) => {
|
||
const { name, value, type, checked } = e.target;
|
||
let val = value;
|
||
if (type === 'checkbox') {
|
||
val = checked;
|
||
}
|
||
// parse numeric fields that are NOT tuition or program_length
|
||
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 }));
|
||
};
|
||
|
||
// ------------------------------------------
|
||
// handleManualTuition, handleManualProgramLength
|
||
// for local fields
|
||
// ------------------------------------------
|
||
const handleManualTuitionChange = (e) => {
|
||
// user typed something => override
|
||
setManualTuition(e.target.value); // store as string for partial typing
|
||
};
|
||
|
||
const handleManualProgramLengthChange = (e) => {
|
||
setManualProgramLength(e.target.value);
|
||
};
|
||
|
||
// ------------------------------------------
|
||
// CIP Data fetch once
|
||
// ------------------------------------------
|
||
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 fetch once
|
||
// ------------------------------------------
|
||
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();
|
||
}, []);
|
||
|
||
// ------------------------------------------
|
||
// handleSchoolChange, handleProgramChange, etc. => update parent fields
|
||
// ------------------------------------------
|
||
const handleSchoolChange = (e) => {
|
||
const value = e.target.value;
|
||
setData(prev => ({
|
||
...prev,
|
||
selected_school: value,
|
||
selected_program: '',
|
||
program_type: '',
|
||
credit_hours_required: '',
|
||
}));
|
||
// CIP suggestions
|
||
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([]);
|
||
};
|
||
|
||
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(''); // reset manual override
|
||
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 => store in local autoTuition
|
||
// ------------------------------------------
|
||
useEffect(() => {
|
||
// do we have enough to calc?
|
||
if (!icTuitionData.length) return;
|
||
if (!selected_school || !program_type || !credit_hours_per_year) return;
|
||
|
||
// find row
|
||
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;
|
||
|
||
// grad or undergrad
|
||
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;
|
||
// threshold
|
||
if (chpy < 24 && partTimeRate) {
|
||
estimate = partTimeRate * chpy;
|
||
} else {
|
||
estimate = fullTimeTuition;
|
||
}
|
||
|
||
setAutoTuition(Math.round(estimate));
|
||
// We do NOT auto-update parent's data. We'll do that in handleSubmit or if you prefer, you can store it in parent's data anyway.
|
||
}, [
|
||
icTuitionData, selected_school, program_type,
|
||
credit_hours_per_year, is_in_state, is_in_district, schoolData
|
||
]);
|
||
|
||
// ------------------------------------------
|
||
// Auto-calc Program Length => store in local autoProgramLength
|
||
// ------------------------------------------
|
||
useEffect(() => {
|
||
if (!program_type) return;
|
||
if (!hours_completed || !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;
|
||
}
|
||
|
||
const remain = required - (parseInt(hours_completed, 10) || 0);
|
||
const yrs = remain / (parseFloat(credit_hours_per_year) || 1);
|
||
const calcLength = yrs.toFixed(2);
|
||
|
||
setAutoProgramLength(calcLength);
|
||
}, [program_type, hours_completed, credit_hours_per_year, credit_hours_required]);
|
||
|
||
// ------------------------------------------
|
||
// handleSubmit => merges final chosen values
|
||
// ------------------------------------------
|
||
const handleSubmit = () => {
|
||
const chosenTuition = manualTuition.trim() === ''
|
||
? autoTuition
|
||
: parseFloat(manualTuition);
|
||
const chosenProgramLength = manualProgramLength.trim() === ''
|
||
? autoProgramLength
|
||
: manualProgramLength;
|
||
|
||
// Update parent’s data (collegeData)
|
||
setData(prev => ({
|
||
...prev,
|
||
tuition: chosenTuition, // match name used by parent or server
|
||
program_length: chosenProgramLength // match name used by parent
|
||
}));
|
||
|
||
// Then go to the next step in the parent’s wizard
|
||
nextStep();
|
||
};
|
||
|
||
// The displayed tuition => (manualTuition !== '' ? manualTuition : autoTuition)
|
||
const displayedTuition = (manualTuition.trim() === '' ? autoTuition : manualTuition);
|
||
|
||
// The displayed program length => (manualProgramLength !== '' ? manualProgramLength : autoProgramLength)
|
||
const displayedProgramLength = (manualProgramLength.trim() === '' ? autoProgramLength : manualProgramLength);
|
||
|
||
|
||
return (
|
||
<div>
|
||
<h2>College Details</h2>
|
||
|
||
{(college_enrollment_status === 'currently_enrolled' ||
|
||
college_enrollment_status === 'prospective_student') ? (
|
||
<>
|
||
<label>School Name*</label>
|
||
<input
|
||
name="selected_school"
|
||
value={selected_school}
|
||
onChange={handleSchoolChange}
|
||
list="school-suggestions"
|
||
placeholder="Enter school name..."
|
||
/>
|
||
<datalist id="school-suggestions">
|
||
{schoolSuggestions.map((sch, idx) => (
|
||
<option
|
||
key={idx}
|
||
value={sch}
|
||
onClick={() => handleSchoolSelect(sch)}
|
||
/>
|
||
))}
|
||
</datalist>
|
||
|
||
<label>Program Name*</label>
|
||
<input
|
||
name="selected_program"
|
||
value={selected_program}
|
||
onChange={handleProgramChange}
|
||
list="program-suggestions"
|
||
placeholder="Enter program name..."
|
||
/>
|
||
<datalist id="program-suggestions">
|
||
{programSuggestions.map((prog, idx) => (
|
||
<option
|
||
key={idx}
|
||
value={prog}
|
||
onClick={() => handleProgramSelect(prog)}
|
||
/>
|
||
))}
|
||
</datalist>
|
||
|
||
<label>Program Type*</label>
|
||
<select
|
||
name="program_type"
|
||
value={program_type}
|
||
onChange={handleProgramTypeSelect}
|
||
>
|
||
<option value="">Select Program Type</option>
|
||
{availableProgramTypes.map((t, i) => (
|
||
<option key={i} value={t}>{t}</option>
|
||
))}
|
||
</select>
|
||
|
||
{(program_type === 'Graduate/Professional Certificate' ||
|
||
program_type === 'First Professional Degree' ||
|
||
program_type === 'Doctoral Degree') && (
|
||
<>
|
||
<label>Credit Hours Required</label>
|
||
<input
|
||
type="number"
|
||
name="credit_hours_required"
|
||
value={credit_hours_required}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="e.g. 30"
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
<label>Credit Hours Per Year</label>
|
||
<input
|
||
type="number"
|
||
name="credit_hours_per_year"
|
||
value={credit_hours_per_year}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="e.g. 24"
|
||
/>
|
||
|
||
<label>Yearly Tuition</label>
|
||
{/* If user typed a custom value => manualTuition, else autoTuition */}
|
||
<input
|
||
type="number"
|
||
value={displayedTuition}
|
||
onChange={handleManualTuitionChange}
|
||
placeholder="Leave blank to use auto, or type an override"
|
||
/>
|
||
|
||
<label>Annual Financial Aid</label>
|
||
<input
|
||
type="number"
|
||
name="annual_financial_aid"
|
||
value={annual_financial_aid}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="e.g. 2000"
|
||
/>
|
||
|
||
<label>Existing College Loan Debt</label>
|
||
<input
|
||
type="number"
|
||
name="existing_college_debt"
|
||
value={existing_college_debt}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="e.g. 2000"
|
||
/>
|
||
|
||
{college_enrollment_status === 'currently_enrolled' && (
|
||
<>
|
||
<label>Tuition Paid</label>
|
||
<input
|
||
type="number"
|
||
name="tuition_paid"
|
||
value={tuition_paid}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="Already paid"
|
||
/>
|
||
|
||
<label>Hours Completed</label>
|
||
<input
|
||
type="number"
|
||
name="hours_completed"
|
||
value={hours_completed}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="Credit hours done"
|
||
/>
|
||
|
||
<label>Program Length</label>
|
||
{/* If user typed a custom => manualProgramLength, else autoProgramLength */}
|
||
<input
|
||
type="number"
|
||
value={displayedProgramLength}
|
||
onChange={handleManualProgramLengthChange}
|
||
placeholder="Leave blank to use auto, or type an override"
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
<label>Expected Graduation</label>
|
||
<input
|
||
type="date"
|
||
name="expected_graduation"
|
||
value={expected_graduation}
|
||
onChange={handleParentFieldChange}
|
||
/>
|
||
|
||
<label>Loan Interest Rate (%)</label>
|
||
<input
|
||
type="number"
|
||
name="interest_rate"
|
||
value={interest_rate}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="e.g. 5.5"
|
||
/>
|
||
|
||
<label>Loan Term (years)</label>
|
||
<input
|
||
type="number"
|
||
name="loan_term"
|
||
value={loan_term}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="e.g. 10"
|
||
/>
|
||
|
||
<label>Extra Monthly Payment</label>
|
||
<input
|
||
type="number"
|
||
name="extra_payment"
|
||
value={extra_payment}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="Optional"
|
||
/>
|
||
|
||
<label>Expected Salary After Graduation</label>
|
||
<input
|
||
type="number"
|
||
name="expected_salary"
|
||
value={expected_salary}
|
||
onChange={handleParentFieldChange}
|
||
placeholder="e.g. 65000"
|
||
/>
|
||
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
name="is_in_district"
|
||
checked={is_in_district}
|
||
onChange={handleParentFieldChange}
|
||
/>
|
||
In District?
|
||
</label>
|
||
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
name="is_in_state"
|
||
checked={is_in_state}
|
||
onChange={handleParentFieldChange}
|
||
/>
|
||
In State Tuition?
|
||
</label>
|
||
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
name="is_online"
|
||
checked={is_online}
|
||
onChange={handleParentFieldChange}
|
||
/>
|
||
Program is Fully Online
|
||
</label>
|
||
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
name="loan_deferral_until_graduation"
|
||
checked={loan_deferral_until_graduation}
|
||
onChange={handleParentFieldChange}
|
||
/>
|
||
Defer Loan Payments until Graduation?
|
||
</label>
|
||
</>
|
||
) : (
|
||
<p>Not currently enrolled or prospective student. Skipping college onboarding.</p>
|
||
)}
|
||
|
||
<button onClick={prevStep} style={{ marginRight: '1rem' }}>← Previous</button>
|
||
<button onClick={handleSubmit}>Finish Onboarding</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default CollegeOnboarding;
|