1162 lines
40 KiB
JavaScript
1162 lines
40 KiB
JavaScript
// 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 (
|
||
<div
|
||
style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
width: '100vw',
|
||
height: '100vh',
|
||
background: 'rgba(0,0,0,0.6)',
|
||
zIndex: 9999,
|
||
overflow: 'hidden'
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
top: '5%',
|
||
left: '50%',
|
||
transform: 'translateX(-50%)',
|
||
width: '90%',
|
||
maxWidth: '900px',
|
||
maxHeight: '85vh',
|
||
background: '#fff',
|
||
borderRadius: '6px',
|
||
padding: '1rem',
|
||
overflowY: 'auto'
|
||
}}
|
||
>
|
||
<h2>
|
||
Edit Scenario: {scenario?.scenario_title || scenario?.career_name || '(untitled)'}
|
||
</h2>
|
||
|
||
{/* -- SCENARIO FIELDS -- */}
|
||
<h3>Scenario & Career</h3>
|
||
<label>Scenario Title</label>
|
||
<input
|
||
type="text"
|
||
name="scenario_title"
|
||
value={formData.scenario_title}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%', marginBottom: '0.5rem' }}
|
||
/>
|
||
|
||
<label>Career Search</label>
|
||
<input
|
||
type="text"
|
||
value={careerSearchInput}
|
||
onChange={handleCareerInputChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
{careerMatches.length > 0 && (
|
||
<ul
|
||
ref={careerDropdownRef}
|
||
style={{
|
||
border: '1px solid #ccc',
|
||
background: '#fff',
|
||
maxHeight: '150px',
|
||
overflowY: 'auto',
|
||
position: 'absolute'
|
||
}}
|
||
>
|
||
{careerMatches.map((c, idx) => (
|
||
<li
|
||
key={idx}
|
||
style={{ cursor: 'pointer' }}
|
||
onClick={() => handleSelectCareer(c)}
|
||
>
|
||
{c}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
<p>
|
||
<em>Current Career:</em> {formData.career_name || '(none)'}
|
||
</p>
|
||
|
||
<label>Status</label>
|
||
<select
|
||
name="status"
|
||
value={formData.status}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%', marginBottom: '0.5rem' }}
|
||
>
|
||
<option value="planned">Planned</option>
|
||
<option value="current">Current</option>
|
||
<option value="completed">Completed</option>
|
||
<option value="exploring">Exploring</option>
|
||
</select>
|
||
|
||
<label>Start Date</label>
|
||
<input
|
||
type="date"
|
||
name="start_date"
|
||
value={formData.start_date || ''}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%', marginBottom: '0.5rem' }}
|
||
/>
|
||
|
||
<label>Projected End Date</label>
|
||
<input
|
||
type="date"
|
||
name="projected_end_date"
|
||
value={formData.projected_end_date || ''}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%', marginBottom: '0.5rem' }}
|
||
/>
|
||
|
||
<label>College Enrollment Status (scenario row)</label>
|
||
<select
|
||
name="college_enrollment_status"
|
||
value={formData.college_enrollment_status}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%', marginBottom: '0.5rem' }}
|
||
>
|
||
<option value="not_enrolled">Not Enrolled</option>
|
||
<option value="currently_enrolled">Currently Enrolled</option>
|
||
<option value="prospective_student">Prospective</option>
|
||
</select>
|
||
|
||
<label>Currently Working?</label>
|
||
<select
|
||
name="currently_working"
|
||
value={formData.currently_working}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%', marginBottom: '0.5rem' }}
|
||
>
|
||
<option value="yes">Yes</option>
|
||
<option value="no">No</option>
|
||
</select>
|
||
|
||
{/* -- SCENARIO FINANCIAL OVERRIDES -- */}
|
||
<h3>Scenario Financial Overwrites</h3>
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fill, minmax(220px,1fr))',
|
||
gap: '1rem'
|
||
}}
|
||
>
|
||
<div>
|
||
<label>Monthly Expenses</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_expenses"
|
||
value={formData.planned_monthly_expenses}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label>Monthly Debt Payments</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_debt_payments"
|
||
value={formData.planned_monthly_debt_payments}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label>Retirement Contrib</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_retirement_contribution"
|
||
value={formData.planned_monthly_retirement_contribution}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label>Emergency Contrib</label>
|
||
<input
|
||
type="number"
|
||
name="planned_monthly_emergency_contribution"
|
||
value={formData.planned_monthly_emergency_contribution}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label>Surplus % to Emergency</label>
|
||
<input
|
||
type="number"
|
||
name="planned_surplus_emergency_pct"
|
||
value={formData.planned_surplus_emergency_pct}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label>Surplus % to Retirement</label>
|
||
<input
|
||
type="number"
|
||
name="planned_surplus_retirement_pct"
|
||
value={formData.planned_surplus_retirement_pct}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label>Additional Income</label>
|
||
<input
|
||
type="number"
|
||
name="planned_additional_income"
|
||
value={formData.planned_additional_income}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* -- COLLEGE PROFILE FIELDS -- */}
|
||
<h3 style={{ marginTop: '1rem' }}>College Profile</h3>
|
||
{(formData.college_enrollment_status === 'currently_enrolled'
|
||
|| formData.college_enrollment_status === 'prospective_student'
|
||
) ? (
|
||
<>
|
||
<div style={{ marginBottom: '1rem', marginTop: '0.5rem' }}>
|
||
<label style={{ marginRight: '1rem' }}>
|
||
<input
|
||
type="checkbox"
|
||
name="is_in_district"
|
||
checked={!!formData.is_in_district}
|
||
onChange={handleFormChange}
|
||
/>
|
||
In District
|
||
</label>
|
||
<label style={{ marginRight: '1rem' }}>
|
||
<input
|
||
type="checkbox"
|
||
name="is_in_state"
|
||
checked={!!formData.is_in_state}
|
||
onChange={handleFormChange}
|
||
/>
|
||
In State
|
||
</label>
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
name="is_online"
|
||
checked={!!formData.is_online}
|
||
onChange={handleFormChange}
|
||
/>
|
||
Fully Online
|
||
</label>
|
||
</div>
|
||
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
name="loan_deferral_until_graduation"
|
||
checked={!!formData.loan_deferral_until_graduation}
|
||
onChange={handleFormChange}
|
||
/>
|
||
{' '}Defer Loan Payments until Graduation?
|
||
</label>
|
||
|
||
<div style={{ marginTop: '0.5rem' }}>
|
||
<label>School</label>
|
||
<input
|
||
type="text"
|
||
value={formData.selected_school}
|
||
onChange={handleSchoolChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
{schoolSuggestions.length > 0 && (
|
||
<ul
|
||
style={{
|
||
border: '1px solid #ccc',
|
||
background: '#fff',
|
||
maxHeight: '150px',
|
||
overflowY: 'auto',
|
||
position: 'absolute'
|
||
}}
|
||
>
|
||
{schoolSuggestions.map((sch, i) => (
|
||
<li
|
||
key={i}
|
||
style={{ cursor: 'pointer' }}
|
||
onClick={() => handleSchoolSelect(sch)}
|
||
>
|
||
{sch}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
|
||
<label>Program</label>
|
||
<input
|
||
type="text"
|
||
value={formData.selected_program}
|
||
onChange={handleProgramChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
{programSuggestions.length > 0 && (
|
||
<ul
|
||
style={{
|
||
border: '1px solid #ccc',
|
||
background: '#fff',
|
||
maxHeight: '150px',
|
||
overflowY: 'auto',
|
||
position: 'absolute'
|
||
}}
|
||
>
|
||
{programSuggestions.map((prog, idx) => (
|
||
<li
|
||
key={idx}
|
||
style={{ cursor: 'pointer' }}
|
||
onClick={() => handleProgramSelect(prog)}
|
||
>
|
||
{prog}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
|
||
<label>Program Type</label>
|
||
<select
|
||
name="program_type"
|
||
value={formData.program_type}
|
||
onChange={handleProgramTypeSelect}
|
||
style={{ width: '100%' }}
|
||
>
|
||
<option value="">(none)</option>
|
||
{availableProgramTypes.map((pt, i) => (
|
||
<option key={i} value={pt}>{pt}</option>
|
||
))}
|
||
</select>
|
||
|
||
<label>Academic Calendar</label>
|
||
<select
|
||
name="academic_calendar"
|
||
value={formData.academic_calendar}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%', marginBottom: '0.5rem' }}
|
||
>
|
||
<option value="monthly">Monthly</option>
|
||
<option value="semester">Semester</option>
|
||
<option value="quarter">Quarter</option>
|
||
<option value="trimester">Trimester</option>
|
||
</select>
|
||
|
||
<label>Credit Hours per Year</label>
|
||
<input
|
||
type="number"
|
||
name="credit_hours_per_year"
|
||
value={formData.credit_hours_per_year}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Yearly Tuition (auto or override)</label>
|
||
<input
|
||
type="number"
|
||
value={displayedTuition}
|
||
onChange={handleManualTuitionChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Annual Financial Aid</label>
|
||
<input
|
||
type="number"
|
||
name="annual_financial_aid"
|
||
value={formData.annual_financial_aid}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Existing College Debt</label>
|
||
<input
|
||
type="number"
|
||
name="existing_college_debt"
|
||
value={formData.existing_college_debt}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
{formData.college_enrollment_status === 'currently_enrolled' && (
|
||
<>
|
||
<label>Tuition Paid</label>
|
||
<input
|
||
type="number"
|
||
name="tuition_paid"
|
||
value={formData.tuition_paid}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Hours Completed</label>
|
||
<input
|
||
type="number"
|
||
name="hours_completed"
|
||
value={formData.hours_completed}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Program Length (auto or override)</label>
|
||
<input
|
||
type="number"
|
||
value={displayedProgLength}
|
||
onChange={handleManualProgLengthChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
<label>Expected Graduation</label>
|
||
<input
|
||
type="date"
|
||
name="expected_graduation"
|
||
value={formData.expected_graduation}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Interest Rate (%)</label>
|
||
<input
|
||
type="number"
|
||
name="interest_rate"
|
||
value={formData.interest_rate}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Loan Term (years)</label>
|
||
<input
|
||
type="number"
|
||
name="loan_term"
|
||
value={formData.loan_term}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Extra Payment (monthly)</label>
|
||
<input
|
||
type="number"
|
||
name="extra_payment"
|
||
value={formData.extra_payment}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
|
||
<label>Expected Salary After Graduation</label>
|
||
<input
|
||
type="number"
|
||
name="expected_salary"
|
||
value={formData.expected_salary}
|
||
onChange={handleFormChange}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p style={{ marginTop: '0.5rem' }}>
|
||
Not currently enrolled or prospective. Minimal college fields only.
|
||
</p>
|
||
)}
|
||
|
||
{/* final actions */}
|
||
<div style={{ marginTop: '1rem', textAlign: 'right' }}>
|
||
<button
|
||
onClick={() => onClose(null, null)}
|
||
style={{ marginRight: '0.5rem' }}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button onClick={handleSave}>
|
||
Save
|
||
</button>
|
||
</div>
|
||
|
||
{/* Show a preview if we have simulation data */}
|
||
{projectionData.length > 0 && (
|
||
<div style={{ marginTop: '1rem', border:'1px solid #ccc', padding:'0.5rem' }}>
|
||
<h4>Simulation Preview (first 5 months):</h4>
|
||
<pre style={{ maxHeight:'200px', overflow:'auto', background:'#f9f9f9' }}>
|
||
{JSON.stringify(projectionData.slice(0,5), null, 2)}
|
||
</pre>
|
||
{loanPayoffMonth && (
|
||
<p>Loan Payoff Month: {loanPayoffMonth}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|