// src/components/ScenarioEditModal.js import React, { useState, useEffect, useRef } from 'react'; import authFetch from '../utils/authFetch.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; // Or wherever your simulator is // JSON/CSV data paths const CIP_URL = '/cip_institution_mapping_new.json'; const IPEDS_URL = '/ic2023_ay.csv'; const CAREER_CLUSTERS_URL = '/career_clusters.json'; export default function ScenarioEditModal({ show, onClose, scenario, collegeProfile }) { /********************************************************* * 1) CIP / IPEDS data states *********************************************************/ const [schoolData, setSchoolData] = useState([]); const [icTuitionData, setIcTuitionData] = useState([]); /********************************************************* * 2) Suggestions & program types *********************************************************/ const [schoolSuggestions, setSchoolSuggestions] = useState([]); const [programSuggestions, setProgramSuggestions] = useState([]); const [availableProgramTypes, setAvailableProgramTypes] = useState([]); /********************************************************* * 3) Manual vs auto for tuition & program length *********************************************************/ const [manualTuition, setManualTuition] = useState(''); const [autoTuition, setAutoTuition] = useState(0); const [manualProgLength, setManualProgLength] = useState(''); const [autoProgLength, setAutoProgLength] = useState('0.00'); /********************************************************* * 4) Career auto-suggest *********************************************************/ const [allCareers, setAllCareers] = useState([]); const [careerSearchInput, setCareerSearchInput] = useState(''); const [careerMatches, setCareerMatches] = useState([]); const careerDropdownRef = useRef(null); /********************************************************* * 5) Combined formData => scenario + college *********************************************************/ const [formData, setFormData] = useState({}); /********************************************************* * 6) On show => load CIP, IPEDS, CAREERS *********************************************************/ useEffect(() => { if (!show) return; const loadCIP = async () => { try { const res = await fetch(CIP_URL); 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 loading CIP data:', err); } }; const loadIPEDS = async () => { try { const res = await fetch(IPEDS_URL); 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 loading IPEDS data:', err); } }; const loadCareers = async () => { try { const resp = await fetch(CAREER_CLUSTERS_URL); if (!resp.ok) { throw new Error(`Failed career_clusters fetch: ${resp.status}`); } const data = await resp.json(); const titlesSet = new Set(); for (const cluster of Object.keys(data)) { for (const sub of Object.keys(data[cluster])) { const arr = data[cluster][sub]; if (Array.isArray(arr)) { arr.forEach((cObj) => { if (cObj?.title) titlesSet.add(cObj.title); }); } } } setAllCareers([...titlesSet]); } catch (err) { console.error('Failed loading career_clusters:', err); } }; loadCIP(); loadIPEDS(); loadCareers(); }, [show]); /********************************************************* * 7) If scenario + collegeProfile => fill form *********************************************************/ useEffect(() => { if (!show || !scenario) return; const s = scenario || {}; const c = collegeProfile || {}; setFormData({ // scenario portion scenario_title: s.scenario_title || '', career_name: s.career_name || '', status: s.status || 'planned', start_date: s.start_date || '', projected_end_date: s.projected_end_date || '', college_enrollment_status: s.college_enrollment_status || 'not_enrolled', currently_working: s.currently_working || 'no', planned_monthly_expenses: s.planned_monthly_expenses ?? '', planned_monthly_debt_payments: s.planned_monthly_debt_payments ?? '', planned_monthly_retirement_contribution: s.planned_monthly_retirement_contribution ?? '', planned_monthly_emergency_contribution: s.planned_monthly_emergency_contribution ?? '', planned_surplus_emergency_pct: s.planned_surplus_emergency_pct ?? '', planned_surplus_retirement_pct: s.planned_surplus_retirement_pct ?? '', planned_additional_income: s.planned_additional_income ?? '', // college portion selected_school: c.selected_school || '', selected_program: c.selected_program || '', program_type: c.program_type || '', academic_calendar: c.academic_calendar || 'monthly', is_in_state: !!c.is_in_state, is_in_district: !!c.is_in_district, is_online: !!c.is_online, // This is the college row's enrollment status college_enrollment_status_db: c.college_enrollment_status || 'not_enrolled', annual_financial_aid: c.annual_financial_aid ?? '', existing_college_debt: c.existing_college_debt ?? '', tuition_paid: c.tuition_paid ?? 0, loan_deferral_until_graduation: !!c.loan_deferral_until_graduation, loan_term: c.loan_term ?? 10, interest_rate: c.interest_rate ?? 5, extra_payment: c.extra_payment ?? 0, credit_hours_per_year: c.credit_hours_per_year ?? '', hours_completed: c.hours_completed ?? '', program_length: c.program_length ?? '', credit_hours_required: c.credit_hours_required ?? '', expected_graduation: c.expected_graduation || '', expected_salary: c.expected_salary ?? '' }); if (c.tuition != null && c.tuition !== 0) { setManualTuition(String(c.tuition)); setAutoTuition(''); // So user sees the DB value, not auto } else { // Else we do our auto-calc, or just set 0 if you prefer const autoCalc = 12000; // or your IPEDS-based logic setAutoTuition(String(autoCalc)); setManualTuition(''); } if (c.program_length != null && c.program_length !== 0) { // DB has real program length setManualProgLength(String(c.program_length)); setAutoProgLength(''); // so we know user is seeing DB } else { // No real DB value => show auto const autoLen = 2.0; // or your own logic setAutoProgLength(String(autoLen)); setManualProgLength(''); } setCareerSearchInput(s.career_name || ''); }, [show, scenario, collegeProfile]); /********************************************************* * 8) Auto-calc tuition + program length => placeholders *********************************************************/ useEffect(() => { if (!show) return; }, [ show, formData.selected_school, formData.program_type, formData.credit_hours_per_year, formData.is_in_district, formData.is_in_state, schoolData, icTuitionData ]); useEffect(() => { if (!show) return; }, [ show, formData.program_type, formData.hours_completed, formData.credit_hours_per_year, formData.credit_hours_required ]); /********************************************************* * 9) Career auto-suggest *********************************************************/ useEffect(() => { if (!show) return; if (!careerSearchInput.trim()) { setCareerMatches([]); return; } const lower = careerSearchInput.toLowerCase(); const partials = allCareers .filter((title) => title.toLowerCase().includes(lower)) .slice(0, 15); setCareerMatches(partials); }, [show, careerSearchInput, allCareers]); /********************************************************* * 9.5) Program Type from CIP * => Populate availableProgramTypes by matching CIP rows for * (selected_school, selected_program) => (CREDDESC). *********************************************************/ useEffect(() => { if (!show) return; if (!formData.selected_school || !formData.selected_program) { setAvailableProgramTypes([]); return; } const filtered = schoolData.filter( (row) => row.INSTNM.toLowerCase() === formData.selected_school.toLowerCase() && row.CIPDESC === formData.selected_program ); const possibleTypes = [...new Set(filtered.map((r) => r.CREDDESC))]; setAvailableProgramTypes(possibleTypes); }, [ show, formData.selected_school, formData.selected_program, schoolData ]); /********************************************************* * 10) Handlers *********************************************************/ function handleFormChange(e) { const { name, type, checked, value } = e.target; let val = value; if (type === 'checkbox') val = checked; setFormData((prev) => ({ ...prev, [name]: val })); } function handleCareerInputChange(e) { const val = e.target.value; setCareerSearchInput(val); if (allCareers.includes(val)) { setFormData((prev) => ({ ...prev, career_name: val })); } } function handleSelectCareer(title) { setCareerSearchInput(title); setFormData((prev) => ({ ...prev, career_name: title })); setCareerMatches([]); } function handleSchoolChange(e) { const val = e.target.value; setFormData((prev) => ({ ...prev, selected_school: val, selected_program: '', program_type: '', credit_hours_required: '' })); if (!val) { setSchoolSuggestions([]); setProgramSuggestions([]); setAvailableProgramTypes([]); return; } const filtered = schoolData.filter((s) => s.INSTNM.toLowerCase().includes(val.toLowerCase()) ); const unique = [...new Set(filtered.map((s) => s.INSTNM))]; setSchoolSuggestions(unique.slice(0, 10)); } function handleSchoolSelect(sch) { setFormData((prev) => ({ ...prev, selected_school: sch, selected_program: '', program_type: '', credit_hours_required: '' })); setSchoolSuggestions([]); } function handleProgramChange(e) { const val = e.target.value; setFormData((prev) => ({ ...prev, selected_program: val })); if (!val) { setProgramSuggestions([]); return; } const filtered = schoolData.filter( (row) => row.INSTNM.toLowerCase() === formData.selected_school.toLowerCase() && row.CIPDESC.toLowerCase().includes(val.toLowerCase()) ); const unique = [...new Set(filtered.map((r) => r.CIPDESC))]; setProgramSuggestions(unique.slice(0, 10)); } function handleProgramSelect(prog) { setFormData((prev) => ({ ...prev, selected_program: prog })); setProgramSuggestions([]); } function handleProgramTypeSelect(e) { setFormData((prev) => ({ ...prev, program_type: e.target.value, credit_hours_required: '' })); setManualProgLength(''); setAutoProgLength('0.00'); } function handleManualTuitionChange(e) { setManualTuition(e.target.value); } function handleManualProgLengthChange(e) { setManualProgLength(e.target.value); } /********************************************************* * 11) After saving, we want to re-fetch scenario & college * and then pass inCollege to the simulator *********************************************************/ const [projectionData, setProjectionData] = useState([]); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); // aggregator function buildMergedUserProfile(scenarioRow, collegeRow, financialData) { // Make sure we read the final updated scenario row's enrollment const enrollment = scenarioRow.college_enrollment_status; const inCollege = (enrollment === 'currently_enrolled' || enrollment === 'prospective_student'); return { currentSalary: financialData.current_salary || 0, monthlyExpenses: scenarioRow.planned_monthly_expenses ?? financialData.monthly_expenses ?? 0, monthlyDebtPayments: scenarioRow.planned_monthly_debt_payments ?? financialData.monthly_debt_payments ?? 0, partTimeIncome: scenarioRow.planned_additional_income ?? financialData.additional_income ?? 0, emergencySavings: financialData.emergency_fund ?? 0, retirementSavings: financialData.retirement_savings ?? 0, monthlyRetirementContribution: scenarioRow.planned_monthly_retirement_contribution ?? financialData.retirement_contribution ?? 0, monthlyEmergencyContribution: scenarioRow.planned_monthly_emergency_contribution ?? financialData.emergency_contribution ?? 0, surplusEmergencyAllocation: scenarioRow.planned_surplus_emergency_pct ?? financialData.extra_cash_emergency_pct ?? 50, surplusRetirementAllocation: scenarioRow.planned_surplus_retirement_pct ?? financialData.extra_cash_retirement_pct ?? 50, // college inCollege, studentLoanAmount: collegeRow.existing_college_debt || 0, interestRate: collegeRow.interest_rate || 5, loanTerm: collegeRow.loan_term || 10, loanDeferralUntilGraduation: !!collegeRow.loan_deferral_until_graduation, academicCalendar: collegeRow.academic_calendar || 'monthly', annualFinancialAid: collegeRow.annual_financial_aid || 0, calculatedTuition: collegeRow.tuition || 0, extraPayment: collegeRow.extra_payment || 0, gradDate: collegeRow.expected_graduation || null, programType: collegeRow.program_type || null, hoursCompleted: collegeRow.hours_completed || 0, creditHoursPerYear: collegeRow.credit_hours_per_year || 0, programLength: collegeRow.program_length || 0, expectedSalary: collegeRow.expected_salary || financialData.current_salary || 0, startDate: scenarioRow.start_date || new Date().toISOString(), simulationYears: 20, milestoneImpacts: [] }; } /********************************************************* * 12) handleSave => upsert scenario & college * => Then re-fetch scenario, college, financial => aggregator => simulate *********************************************************/ async function handleSave() { try { // --- Helper functions for partial update: --- function parseNumberIfGiven(val) { if (val == null) return undefined; // skip if null/undefined // Convert to string before trimming const valStr = String(val).trim(); if (valStr === '') { return undefined; // skip if empty } const num = Number(valStr); return isNaN(num) ? undefined : num; } function parseStringIfGiven(val) { if (val == null) return undefined; const trimmed = String(val).trim(); return trimmed === '' ? undefined : trimmed; } // Let’s handle your manualTuition / manualProgLength logic too const chosenTuitionVal = manualTuition.trim() !== '' ? Number(manualTuition) : undefined; const chosenProgLengthVal = manualProgLength.trim() !== '' ? Number(manualProgLength) : undefined; // The user sets scenario.college_enrollment_status => "currently_enrolled" // We'll explicitly set the college row's status to match let finalCollegeStatus = formData.college_enrollment_status_db; if ( formData.college_enrollment_status === 'currently_enrolled' || formData.college_enrollment_status === 'prospective_student' ) { finalCollegeStatus = formData.college_enrollment_status; } else { finalCollegeStatus = 'not_enrolled'; } // --- Build scenarioPayload with partial updates --- const scenarioPayload = {}; // (A) Some fields you always want to set: scenarioPayload.college_enrollment_status = finalCollegeStatus; scenarioPayload.currently_working = formData.currently_working || 'no'; // (B) scenario_title, career_name, status => only if typed const scenarioTitle = parseStringIfGiven(formData.scenario_title); if (scenarioTitle !== undefined) { scenarioPayload.scenario_title = scenarioTitle; } const careerName = parseStringIfGiven(formData.career_name); if (careerName !== undefined) { scenarioPayload.career_name = careerName; } const scenarioStatus = parseStringIfGiven(formData.status); if (scenarioStatus !== undefined) { scenarioPayload.status = scenarioStatus; } // (C) Dates if (formData.start_date && formData.start_date.trim() !== '') { scenarioPayload.start_date = formData.start_date.trim(); } if (formData.projected_end_date && formData.projected_end_date.trim() !== '') { scenarioPayload.projected_end_date = formData.projected_end_date.trim(); } // (D) Numeric overrides const pme = parseNumberIfGiven(formData.planned_monthly_expenses); if (pme !== undefined) scenarioPayload.planned_monthly_expenses = pme; const pmdp = parseNumberIfGiven(formData.planned_monthly_debt_payments); if (pmdp !== undefined) scenarioPayload.planned_monthly_debt_payments = pmdp; const pmrc = parseNumberIfGiven(formData.planned_monthly_retirement_contribution); if (pmrc !== undefined) scenarioPayload.planned_monthly_retirement_contribution = pmrc; const pmec = parseNumberIfGiven(formData.planned_monthly_emergency_contribution); if (pmec !== undefined) scenarioPayload.planned_monthly_emergency_contribution = pmec; const psep = parseNumberIfGiven(formData.planned_surplus_emergency_pct); if (psep !== undefined) scenarioPayload.planned_surplus_emergency_pct = psep; const psrp = parseNumberIfGiven(formData.planned_surplus_retirement_pct); if (psrp !== undefined) scenarioPayload.planned_surplus_retirement_pct = psrp; const pai = parseNumberIfGiven(formData.planned_additional_income); if (pai !== undefined) scenarioPayload.planned_additional_income = pai; // 1) Upsert scenario row const scenRes = await authFetch('/api/premium/career-profile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(scenarioPayload) }); if (!scenRes.ok) { const msg = await scenRes.text(); throw new Error(`Scenario upsert failed: ${msg}`); } const scenData = await scenRes.json(); const updatedScenarioId = scenData.career_path_id; // --- Build collegePayload with partial updates --- const collegePayload = { career_path_id: updatedScenarioId, // We always sync these booleans or statuses college_enrollment_status: finalCollegeStatus, is_in_state: formData.is_in_state ? 1 : 0, is_in_district: formData.is_in_district ? 1 : 0, is_in_online: formData.is_in_online ? 1 : 0 }; // Strings const selSchool = parseStringIfGiven(formData.selected_school); if (selSchool !== undefined) collegePayload.selected_school = selSchool; const selProg = parseStringIfGiven(formData.selected_program); if (selProg !== undefined) collegePayload.selected_program = selProg; const progType = parseStringIfGiven(formData.program_type); if (progType !== undefined) collegePayload.program_type = progType; const acCal = parseStringIfGiven(formData.academic_calendar); if (acCal !== undefined) collegePayload.academic_calendar = acCal; // If user typed a date for expected_graduation if (formData.expected_graduation && formData.expected_graduation.trim() !== '') { collegePayload.expected_graduation = formData.expected_graduation.trim(); } // Numeric fields const afa = parseNumberIfGiven(formData.annual_financial_aid); if (afa !== undefined) collegePayload.annual_financial_aid = afa; const ecd = parseNumberIfGiven(formData.existing_college_debt); if (ecd !== undefined) collegePayload.existing_college_debt = ecd; const tp = parseNumberIfGiven(formData.tuition_paid); if (tp !== undefined) collegePayload.tuition_paid = tp; // Chosen tuition if user typed manualTuition if (chosenTuitionVal !== undefined && !isNaN(chosenTuitionVal)) { collegePayload.tuition = chosenTuitionVal; } // chosenProgLength if user typed manualProgLength if (chosenProgLengthVal !== undefined && !isNaN(chosenProgLengthVal)) { collegePayload.program_length = chosenProgLengthVal; } const ltg = parseNumberIfGiven(formData.loan_term); if (ltg !== undefined) collegePayload.loan_term = ltg; const ir = parseNumberIfGiven(formData.interest_rate); if (ir !== undefined) collegePayload.interest_rate = ir; const ep = parseNumberIfGiven(formData.extra_payment); if (ep !== undefined) collegePayload.extra_payment = ep; const chpy = parseNumberIfGiven(formData.credit_hours_per_year); if (chpy !== undefined) collegePayload.credit_hours_per_year = chpy; const hc = parseNumberIfGiven(formData.hours_completed); if (hc !== undefined) collegePayload.hours_completed = hc; const chr = parseNumberIfGiven(formData.credit_hours_required); if (chr !== undefined) collegePayload.credit_hours_required = chr; const esal = parseNumberIfGiven(formData.expected_salary); if (esal !== undefined) collegePayload.expected_salary = esal; // Defer Loan if (formData.loan_deferral_until_graduation) { collegePayload.loan_deferral_until_graduation = 1; } // 2) Upsert the college row const colRes = await authFetch('/api/premium/college-profile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(collegePayload) }); if (!colRes.ok) { const msg2 = await colRes.text(); throw new Error(`College upsert failed: ${msg2}`); } // 3) Re-fetch scenario, college, & financial => aggregator => simulate const [scenResp2, colResp2, finResp] = await Promise.all([ authFetch(`/api/premium/career-profile/${updatedScenarioId}`), authFetch(`/api/premium/college-profile?careerPathId=${updatedScenarioId}`), authFetch(`/api/premium/financial-profile`) ]); if (!scenResp2.ok || !colResp2.ok || !finResp.ok) { console.error('One re-fetch failed after upsert.', { scenarioStatus: scenResp2.status, collegeStatus: colResp2.status, financialStatus: finResp.status }); onClose(); // or show an error return; } const [finalScenarioRow, finalCollegeRaw, finalFinancial] = await Promise.all([ scenResp2.json(), colResp2.json(), finResp.json() ]); let finalCollegeRow = Array.isArray(finalCollegeRaw) ? finalCollegeRaw[0] || {} : finalCollegeRaw; // 4) Build the aggregator and run the simulation const userProfile = buildMergedUserProfile( finalScenarioRow, finalCollegeRow, finalFinancial ); const results = simulateFinancialProjection(userProfile); setProjectionData(results.projectionData); setLoanPayoffMonth(results.loanPaidOffMonth); // 5) Now close the modal automatically onClose(); window.location.reload(); } catch (err) { console.error('Error saving scenario + college:', err); alert(err.message || 'Failed to save scenario data.'); } } /********************************************************* * 13) Render *********************************************************/ if (!show) return null; const displayedTuition = manualTuition.trim() === '' ? autoTuition : manualTuition; const displayedProgLength = manualProgLength.trim() === '' ? autoProgLength : manualProgLength; return (
Current Career: {formData.career_name || '(none)'}
{/* -- SCENARIO FINANCIAL OVERRIDES -- */}Not currently enrolled or prospective. Minimal college fields only.
)} {/* final actions */}{JSON.stringify(projectionData.slice(0,5), null, 2)}{loanPayoffMonth && (
Loan Payoff Month: {loanPayoffMonth}
)}