import React, { useState, useEffect, useRef } from 'react'; import authFetch from '../utils/authFetch.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import { Button } from './ui/button.js'; import parseFloatOrZero from '../utils/ParseFloatorZero.js'; import InfoTooltip from "./ui/infoTooltip.js"; // 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, financialProfile }) { /********************************************************* * 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({}); const [showCollegeForm, setShowCollegeForm] = useState(false); /********************************************************* * Auto-expand the college section each time the modal opens. * -------------------------------------------------------- * ❑ The effect runs exactly once per modal–open (`show` → true). * ❑ If the saved scenario already says the user is * ‘currently_enrolled’ or ‘prospective_student’ * we open the section so they immediately see their data. * ❑ Once open, the user can click Hide/Show; we *don’t* re-run * on every keystroke, so the effect won’t fight the button. *********************************************************/ useEffect(() => { if (!show) return; setShowCollegeForm( ['currently_enrolled', 'prospective_student'] .includes(formData.college_enrollment_status) ); }, [show]); /********************************************************* * 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) Whenever the **modal is shown** *or* **scenario.id changes** * → hydrate the form + careerSearch box. *********************************************************/ useEffect(() => { if (!show || !scenario) return; const s = scenario || {}; const c = collegeProfile || {}; const safe = v => v === null || v === undefined ? '' : v; setFormData({ // scenario portion scenario_title : safe(s.scenario_title), career_name : safe(s.career_name), status : safe(s.status || 'planned'), start_date : safe(s.start_date), retirement_start_date: safe(s.retirement_start_date), desired_retirement_income_monthly : safe( s.desired_retirement_income_monthly ), planned_monthly_expenses : safe(s.planned_monthly_expenses), planned_monthly_debt_payments : safe(s.planned_monthly_debt_payments), planned_monthly_retirement_contribution: safe(s.planned_monthly_retirement_contribution), planned_monthly_emergency_contribution : safe(s.planned_monthly_emergency_contribution), planned_surplus_emergency_pct : safe(s.planned_surplus_emergency_pct), planned_surplus_retirement_pct : safe(s.planned_surplus_retirement_pct), planned_additional_income : safe(s.planned_additional_income), // college portion college_profile_id: safe(c.id || null), selected_school: safe(c.selected_school || ''), selected_program: safe(c.selected_program || ''), program_type: safe(c.program_type || ''), academic_calendar: safe(c.academic_calendar || 'monthly'), is_in_state: safe(!!c.is_in_state), is_in_district: safe(!!c.is_in_district), is_online: safe(!!c.is_online), college_enrollment_status_db: safe(c.college_enrollment_status || 'not_enrolled'), annual_financial_aid : safe(c.annual_financial_aid), existing_college_debt : safe(c.existing_college_debt), tuition_paid : safe(c.tuition_paid), loan_term : safe(c.loan_term ?? 10), interest_rate : safe(c.interest_rate ?? 5), extra_payment : safe(c.extra_payment), credit_hours_per_year: safe(c.credit_hours_per_year ?? ''), hours_completed: safe(c.hours_completed ?? ''), program_length: safe(c.program_length ?? ''), credit_hours_required: safe(c.credit_hours_required ?? ''), enrollment_date: safe(c.enrollment_date ? c.enrollment_date.substring(0, 10): ''), expected_graduation: safe(c.expected_graduation ? c.expected_graduation.substring(0, 10): ''), expected_salary: safe(c.expected_salary ?? '') }); // Manual / auto tuition if (c.tuition != null && c.tuition !== 0) { setManualTuition(String(c.tuition)); setAutoTuition(''); } else { const autoCalc = 12000; setAutoTuition(String(autoCalc)); setManualTuition(''); } // Manual / auto program length if (c.program_length != null && c.program_length !== 0) { setManualProgLength(String(c.program_length)); setAutoProgLength(''); } else { const autoLen = 2.0; setAutoProgLength(String(autoLen)); setManualProgLength(''); } setCareerSearchInput(s.career_name || ''); }, [show, scenario?.id, collegeProfile]); /********************************************************* * 8) Auto-calc placeholders (stubbed out) *********************************************************/ useEffect(() => { if (!show) return; // IPEDS-based logic or other auto-calculation }, [ 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; // Possibly recalc program length }, [ show, formData.program_type, formData.hours_completed, formData.credit_hours_per_year, formData.credit_hours_required ]); /********************************************************* * 9) Career auto-suggest *********************************************************/ useEffect(() => { if (!show) return; // 1️⃣ trim once, reuse everywhere const typed = careerSearchInput.trim(); // Nothing typed → clear list if (!typed) { setCareerMatches([]); return; } /* 2️⃣ Exact match (case-insensitive) → suppress dropdown */ if (allCareers.some(t => t.toLowerCase() === typed.toLowerCase())) { setCareerMatches([]); return; } // 3️⃣ Otherwise show up to 15 partial matches const lower = typed.toLowerCase(); const partials = allCareers .filter(title => title.toLowerCase().includes(lower)) .slice(0, 15); setCareerMatches(partials); }, [show, careerSearchInput, allCareers]); /********************************************************* * 9.5) Program Type from CIP *********************************************************/ 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) Simulation aggregator *********************************************************/ const [projectionData, setProjectionData] = useState([]); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); function buildMergedUserProfile(scenarioRow, collegeRow, financialData) { 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, enrollmentDate: collegeRow.enrollment_date || null, 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().slice(0, 10), simulationYears: 20, milestoneImpacts: [] }; } /********************************************************* * 12) handleSave => upsert scenario & college => re-fetch => simulate *********************************************************/ async function handleSave() { try { /* ─── helpers ───────────────────────────────────────────── */ const n = v => (v === "" || v == null ? undefined : Number(v)); const s = v => { if (v == null) return undefined; const t = String(v).trim(); return t === "" ? undefined : t; }; /* ─── 0) did the user change the title? ─────────────────── */ const originalName = scenario?.career_name?.trim() || ""; const editedName = (formData.career_name || "").trim(); const titleChanged = editedName && editedName !== originalName; /* ─── 1) build scenario payload ─────────────────────────── */ const scenarioPayload = { scenario_title : s(formData.scenario_title), career_name : editedName, // always include college_enrollment_status : formData.college_enrollment_status, currently_working : formData.currently_working || "no", status : s(formData.status), start_date : s(formData.start_date), retirement_start_date : s(formData.retirement_start_date), desired_retirement_income_monthly : n(formData.desired_retirement_income_monthly), planned_monthly_expenses : n(formData.planned_monthly_expenses), planned_monthly_debt_payments : n(formData.planned_monthly_debt_payments), planned_monthly_retirement_contribution: n(formData.planned_monthly_retirement_contribution), planned_monthly_emergency_contribution : n(formData.planned_monthly_emergency_contribution), planned_surplus_emergency_pct : n(formData.planned_surplus_emergency_pct), planned_surplus_retirement_pct : n(formData.planned_surplus_retirement_pct), planned_additional_income : n(formData.planned_additional_income) }; /* If the title did NOT change, keep the id so the UPSERT updates the existing row. Otherwise omit id → new row */ if (!titleChanged && scenario?.id) { scenarioPayload.id = scenario.id; } /* ─── 2) POST (always) ─────────────────────────────────── */ const scenRes = await authFetch("/api/premium/career-profile", { method : "POST", headers: { "Content-Type": "application/json" }, body : JSON.stringify(scenarioPayload) }); if (!scenRes.ok) throw new Error(await scenRes.text()); const { career_profile_id } = await scenRes.json(); // ─── AUTO-CREATE / UPDATE “Retirement” milestone ────────────────── if (formData.retirement_start_date) { const payload = { title : 'Retirement', description : 'User-defined retirement date (auto-generated)', date : formData.retirement_start_date, career_profile_id: career_profile_id, progress : 0, status : 'planned', is_universal : 0 }; // Ask the backend if one already exists for this scenario const check = await authFetch( `/api/premium/milestones?careerProfileId=${career_profile_id}` ).then(r => r.json()); const existing = (check.milestones || []).find(m => m.title === 'Retirement'); await authFetch( existing ? `/api/premium/milestones/${existing.id}` : '/api/premium/milestone', { method : existing ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify(payload) } ); } /* ─── 3) (optional) upsert college profile – keep yours… ─ */ /* ─── 4) update localStorage so CareerRoadmap re-hydrates ─ */ localStorage.setItem( "selectedCareer", JSON.stringify({ title: editedName }) ); localStorage.setItem( "lastSelectedCareerProfileId", String(career_profile_id) ); /* ─── 5) close modal + tell parent to refetch ───────────── */ onClose(true); // CareerRoadmap’s onClose(true) triggers reload } catch (err) { console.error("handleSave", err); alert(err.message || "Failed to save scenario"); } } /********************************************************* * 13) Render *********************************************************/ if (!show) return null; const displayedTuition = manualTuition.trim() === '' ? autoTuition : manualTuition; const displayedProgLength = manualProgLength.trim() === '' ? autoProgLength : manualProgLength; return (

Edit Scenario: {scenario?.scenario_title || scenario?.career_name || '(untitled)'}

{/* SECTION: Scenario & Career */}

Scenario & Career

{/* Scenario Title */}
{/* Career Search */}
{careerMatches.length > 0 && (
    {careerMatches.map((c, idx) => (
  • handleSelectCareer(c)} > {c}
  • ))}
)}

Current Career: {formData.career_name || '(none)'}

{/* Status */}
{/* Dates */}
{/* Retirement date */}
{/* Desired retirement income (monthly) */}
{formData.desired_retirement_income_monthly && (

≈ $ {(formData.desired_retirement_income_monthly*12) .toLocaleString()} per year

)}
{/* College Enrollment Status */}
{/* Currently Working */}
{/* SECTION: Scenario Financial Overrides */}

Scenario Financial Overrides

{/* ───────── COLLEGE PROFILE heading + Add plan ───────── */}
{/* left cluster (title + info badge) */}

College Profile

{/* right cluster (toggle button) */}
{/* Collapse / expand the full set of inputs */} {showCollegeForm ? ( <> {/* District / State / Online check-boxes ------------------ */}
{[ { name: 'is_in_district', label: 'In District' }, { name: 'is_in_state', label: 'In State' }, { name: 'is_online', label: 'Fully Online' } ].map(({ name, label }) => ( ))}
{/* Loan Deferral */}
{/* School */}
{schoolSuggestions.length > 0 && (
    {schoolSuggestions.map((sch, i) => (
  • handleSchoolSelect(sch)} > {sch}
  • ))}
)}
{/* Program */}
{programSuggestions.length > 0 && (
    {programSuggestions.map((prog, idx) => (
  • handleProgramSelect(prog)} > {prog}
  • ))}
)}
{/* Program Type */}
{/* Academic Calendar */}
{/* Credit Hours per Year */}
{/* Yearly Tuition */}
{/* Annual Financial Aid */}
{/* Existing College Debt */}
{/* Currently Enrolled Only Fields */} {formData.college_enrollment_status === 'currently_enrolled' && ( <>
)} {/* Enrollment Date */}
{/* Expected Graduation */}
{/* Interest Rate */}
{/* Loan Term */}
{/* Extra Payment */}
{/* Expected Salary */}
) : (

Not currently enrolled or prospective. Add a plan to enter this info.

)} {/* ACTIONS */}
{!formData.retirement_start_date && (

Pick a Planned Retirement Date to run the simulation.

)}
{/* Show a preview if we have simulation data */} {projectionData.length > 0 && (

Simulation Preview (first 5 months):

              {JSON.stringify(projectionData.slice(0, 5), null, 2)}
            
{loanPayoffMonth && (

Loan Payoff Month: {loanPayoffMonth}

)}
)}
); }